From 353c98ae09bc020a48af91de94cd0e4220d7f5df Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 12:06:36 +0000 Subject: [PATCH 1/2] chore: add .worktrees/ to gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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/ From 05e9050c3b692b637c88d02a16c2f4c9852a6d3d Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Sat, 7 Mar 2026 12:58:18 +0000 Subject: [PATCH 2/2] fix: resolve OIDC account linking and show SSO error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1: OAuthAccountNotLinked after deleting a local user and re-logging via SSO. Root cause: the signIn callback creates the User record before Auth.js's handleLoginOrRegister runs. Auth.js then finds the user by email, sees no linked Account, and throws OAuthAccountNotLinked because allowDangerousEmailAccountLinking was false. Fix: set it to true — this is safe because the signIn callback already guards against local account hijacking (returns redirect for LOCAL authMethod users). Bug 2: When a local user attempts SSO login, the generic AccessDenied error gave no indication of what went wrong. Fix: return a redirect URL with a specific error code instead of returning false, and display a clear message on the login page explaining the user needs admin linking. Also adds debug logging for OIDC group claims to help diagnose team mapping issues. --- src/app/(auth)/login/page.tsx | 18 +++++++++++++++++- src/auth.ts | 5 +++-- 2 files changed, 20 insertions(+), 3 deletions(-) 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