Only allow single NextAuth provider, Login middleware updates

This commit is contained in:
Darren Clarke 2024-09-27 14:52:44 +02:00
parent f86ce1e835
commit 8736b576a7
6 changed files with 111 additions and 99 deletions

View file

@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
transpilePackages: ["@link-stack/ui", "@link-stack/bridge-common", "@link-stack/bridge-ui"], transpilePackages: ["@link-stack/ui", "@link-stack/bridge-common", "@link-stack/bridge-ui"],
poweredByHeader: false,
}; };
export default nextConfig; export default nextConfig;

View file

@ -1,16 +1,9 @@
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self';
child-src example.com;
style-src 'self' example.com;
font-src 'self';
`;
module.exports = { module.exports = {
transpilePackages: ["@link-stack/leafcutter-ui", "@link-stack/opensearch-common"], transpilePackages: ["@link-stack/leafcutter-ui", "@link-stack/opensearch-common"],
experimental: { experimental: {
missingSuspenseWithCSRBailout: false, missingSuspenseWithCSRBailout: false,
}, },
poweredByHeader: false,
rewrites: async () => ({ rewrites: async () => ({
fallback: [ fallback: [
{ {

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import { FC, useState } from "react"; import { FC, useState, useEffect } from "react";
import { import {
Box, Box,
Grid, Grid,
@ -14,7 +14,7 @@ import {
Google as GoogleIcon, Google as GoogleIcon,
Key as KeyIcon, Key as KeyIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { signIn } from "next-auth/react"; import { signIn, getProviders } from "next-auth/react";
import Image from "next/image"; import Image from "next/image";
import LinkLogo from "public/link-logo-small.png"; import LinkLogo from "public/link-logo-small.png";
import { colors, fonts } from "@link-stack/ui"; import { colors, fonts } from "@link-stack/ui";
@ -29,6 +29,7 @@ export const Login: FC<LoginProps> = ({ session }) => {
typeof window !== "undefined" && window.location.origin typeof window !== "undefined" && window.location.origin
? window.location.origin ? window.location.origin
: ""; : "";
const [provider, setProvider] = useState(undefined);
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const params = useSearchParams(); const params = useSearchParams();
@ -63,6 +64,14 @@ export const Login: FC<LoginProps> = ({ session }) => {
}, },
}; };
useEffect(() => {
const fetchProviders = async () => {
const providers = await getProviders();
setProvider(Object.keys(providers)?.pop());
};
fetchProviders();
}, []);
return ( return (
<Box sx={{ backgroundColor: darkGray, height: "100vh" }}> <Box sx={{ backgroundColor: darkGray, height: "100vh" }}>
<Container maxWidth="md" sx={{ p: 10 }}> <Container maxWidth="md" sx={{ p: 10 }}>
@ -142,84 +151,79 @@ export const Login: FC<LoginProps> = ({ session }) => {
</Box> </Box>
</Grid> </Grid>
) : null} ) : null}
<Grid item sx={{ width: "100%" }}> {provider === "google" && (
<IconButton <Grid item sx={{ width: "100%" }}>
sx={buttonStyles} <IconButton
onClick={() => sx={buttonStyles}
signIn("google", { onClick={() =>
callbackUrl: `${origin}`, signIn("google", {
}) callbackUrl: `${origin}`,
} })
> }
<GoogleIcon sx={{ mr: 1 }} /> >
Sign in with Google <GoogleIcon sx={{ mr: 1 }} />
</IconButton> Sign in with Google
</Grid> </IconButton>
<Grid item sx={{ width: "100%" }}> </Grid>
<IconButton )}
aria-label="Sign in with Apple" {provider === "apple" && (
sx={buttonStyles} <Grid item sx={{ width: "100%" }}>
onClick={() => <IconButton
signIn("apple", { aria-label="Sign in with Apple"
callbackUrl: `${window.location.origin}`, sx={buttonStyles}
}) onClick={() =>
} signIn("apple", {
> callbackUrl: `${window.location.origin}`,
<AppleIcon sx={{ mr: 1 }} /> })
Sign in with Apple }
</IconButton> >
</Grid> <AppleIcon sx={{ mr: 1 }} />
<Grid> Sign in with Apple
<Typography </IconButton>
variant="body1" </Grid>
sx={{ )}
fontSize: 18, {provider === "credentials" && (
color: white, <Grid item container spacing={3}>
textAlign: "center", <Grid item sx={{ width: "100%" }}>
mt: 3, <TextField
}} value={email}
> onChange={(e) => setEmail(e.target.value)}
or label="Email"
</Typography> variant="filled"
</Grid> size="small"
<Grid item sx={{ width: "100%" }}> fullWidth
<TextField sx={{ ...fieldStyles, backgroundColor: white }}
value={email} />
onChange={(e) => setEmail(e.target.value)} </Grid>
label="Email" <Grid item sx={{ ...fieldStyles, width: "100%" }}>
variant="filled" <TextField
size="small" value={password}
fullWidth onChange={(e) => setPassword(e.target.value)}
sx={{ ...fieldStyles, backgroundColor: white }} label="Password"
/> variant="filled"
</Grid> size="small"
<Grid item sx={{ ...fieldStyles, width: "100%" }}> fullWidth
<TextField sx={{ backgroundColor: white }}
value={password} type="password"
onChange={(e) => setPassword(e.target.value)} />
label="Password" </Grid>
variant="filled" <Grid item sx={{ width: "100%" }}>
size="small" <IconButton
fullWidth sx={buttonStyles}
sx={{ backgroundColor: white }} onClick={() =>
type="password" signIn("credentials", {
/> email,
</Grid> password,
<Grid item sx={{ width: "100%" }}> callbackUrl: `${origin}/setup`,
<IconButton })
sx={buttonStyles} }
onClick={() => >
signIn("credentials", { <KeyIcon sx={{ mr: 1 }} />
email, Sign in with Zammad credentials
password, </IconButton>
callbackUrl: `${origin}/setup`, </Grid>
}) </Grid>
} )}
>
<KeyIcon sx={{ mr: 1 }} />
Sign in with Zammad credentials
</IconButton>
</Grid>
</Grid> </Grid>
</Container> </Container>
) : null} ) : null}

View file

@ -66,21 +66,24 @@ const login = async (email: string, password: string) => {
} }
}; };
export const authOptions: NextAuthOptions = { const providers = [];
pages: {
signIn: "/login", if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
error: "/login", providers.push(
signOut: "/logout",
},
providers: [
Google({ Google({
clientId: process.env.GOOGLE_CLIENT_ID, clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET, clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}), }),
);
} else if (process.env.APPLE_CLIENT_ID && process.env.APPLE_CLIENT_SECRET) {
providers.push(
Apple({ Apple({
clientId: process.env.APPLE_CLIENT_ID, clientId: process.env.APPLE_CLIENT_ID,
clientSecret: process.env.APPLE_CLIENT_SECRET, clientSecret: process.env.APPLE_CLIENT_SECRET,
}), }),
);
} else {
providers.push(
Credentials({ Credentials({
name: "Zammad", name: "Zammad",
credentials: { credentials: {
@ -96,7 +99,16 @@ export const authOptions: NextAuthOptions = {
} }
}, },
}), }),
], );
}
export const authOptions: NextAuthOptions = {
pages: {
signIn: "/login",
error: "/login",
signOut: "/logout",
},
providers,
secret: process.env.NEXTAUTH_SECRET, secret: process.env.NEXTAUTH_SECRET,
callbacks: { callbacks: {
signIn: async ({ user }) => { signIn: async ({ user }) => {

View file

@ -85,9 +85,6 @@ const checkRewrites = async (request: NextRequestWithAuth) => {
}; };
export default withAuth(checkRewrites, { export default withAuth(checkRewrites, {
pages: {
signIn: `/login`,
},
callbacks: { callbacks: {
authorized: ({ token, req }) => { authorized: ({ token, req }) => {
if (process.env.SETUP_MODE === "true") { if (process.env.SETUP_MODE === "true") {
@ -97,6 +94,10 @@ export default withAuth(checkRewrites, {
const path = req.nextUrl.pathname; const path = req.nextUrl.pathname;
const roles: any = token?.roles ?? []; const roles: any = token?.roles ?? [];
if (path.startsWith("/login")) {
return true;
}
if (path.startsWith("/admin") && !roles.includes("admin")) { if (path.startsWith("/admin") && !roles.includes("admin")) {
return false; return false;
} }

View file

@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
poweredByHeader: false,
transpilePackages: [ transpilePackages: [
"@link-stack/leafcutter-ui", "@link-stack/leafcutter-ui",
"@link-stack/opensearch-common", "@link-stack/opensearch-common",