diff --git a/.gitignore b/.gitignore index 9540bdfe..d9af12b8 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,6 @@ next-env.d.ts # codeql .codeql/ -/scripts/* \ No newline at end of file +/scripts/* +# worktrees +.worktrees/ diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index d63a7fe9..5a4afc75 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { signIn } from "next-auth/react"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { Shield, KeyRound, Loader2 } from "lucide-react"; import { Card, @@ -17,14 +17,30 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; +const SSO_ERROR_MESSAGES: Record = { + local_account: "This email is registered as a local account. Ask an admin to link it to SSO before signing in.", + OAuthAccountNotLinked: "This email is already associated with another account. Ask an admin to link it to SSO.", + OAuthCallback: "SSO sign-in failed. Please try again or contact your administrator.", + AccessDenied: "Access denied. You may not have permission to sign in via SSO.", +}; + export default function LoginPage() { const router = useRouter(); + const searchParams = useSearchParams(); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [totpCode, setTotpCode] = useState(""); const [totpRequired, setTotpRequired] = useState(false); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); + + useEffect(() => { + const errorParam = searchParams.get("error"); + if (errorParam) { + setError(SSO_ERROR_MESSAGES[errorParam] ?? "An error occurred during sign-in. Please try again."); + window.history.replaceState({}, "", "/login"); + } + }, [searchParams]); const [oidcStatus, setOidcStatus] = useState<{ enabled: boolean; displayName: string; diff --git a/src/auth.ts b/src/auth.ts index 7ea33a6d..3ec4b822 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -178,7 +178,7 @@ async function getAuthInstance() { issuer: oidc.issuer, clientId: oidc.clientId, clientSecret: oidc.clientSecret, - allowDangerousEmailAccountLinking: false, + allowDangerousEmailAccountLinking: true, client: { token_endpoint_auth_method: oidc.tokenEndpointAuthMethod, }, @@ -201,6 +201,7 @@ async function getAuthInstance() { const groupsClaim = settings?.oidcGroupsClaim ?? "groups"; const profileData = profile as Record | undefined; const userGroups = (profileData?.[groupsClaim] as string[] | undefined) ?? []; + console.log(`[oidc] User ${user.email} groups (claim "${groupsClaim}"):`, userGroups); // Ensure user exists in the database let dbUser = await prisma.user.findUnique({ @@ -231,7 +232,7 @@ async function getAuthInstance() { console.warn( `OIDC login blocked: local account exists for ${dbUser.email}. Admin must explicitly link accounts.`, ); - return false; + return "/login?error=local_account"; } // Refresh profile picture on each OIDC sign-in