({ account, user })
| 289 | }, |
| 290 | callbacks: { |
| 291 | async signIn({ account, user }) { |
| 292 | const matchingProvider = account |
| 293 | ? (await getProviders()).find((p) => p.id === account.provider) |
| 294 | : undefined; |
| 295 | |
| 296 | // Refuse OAuth signin for providers configured purely for account |
| 297 | // linking when no authenticated user is present on the request. |
| 298 | // |
| 299 | // Background: @auth/core's handleLoginOrRegister (callback/handle-login.js) |
| 300 | // reads the session token from the request and, if it can't decode it |
| 301 | // (e.g., the session cookie expired browser-side mid auth flow, or it |
| 302 | // never made it across the cross-site redirect), |
| 303 | // falls through to `createUser({ ...profile })`, silently spawning a |
| 304 | // new orphan User row from the OAuth profile. That's correct behavior |
| 305 | // for `purpose: "sso"` providers (an unauthenticated user logging in |
| 306 | // via SSO should become a new Sourcebot user). It's wrong for |
| 307 | // `purpose: "account_linking"` providers: by definition, those should |
| 308 | // only ever attach an upstream identity to an *existing* signed-in |
| 309 | // user, never mint a new Sourcebot user. |
| 310 | // |
| 311 | // Returning `false` here short-circuits the callback action with an |
| 312 | // `AccessDenied` before handleLoginOrRegister can run, redirecting |
| 313 | // the user to the error page instead of leaving them stranded as a |
| 314 | // new orphan identity with no UserToOrg row. |
| 315 | const isAccountLinkingAttempt = matchingProvider?.purpose === 'account_linking'; |
| 316 | const session = await auth(); |
| 317 | if (isAccountLinkingAttempt && session === null) { |
| 318 | return false; |
| 319 | } |
| 320 | |
| 321 | // Reject any sign-in that arrives without an email. |
| 322 | // @see 20260616000000_make_user_email_required/migration.sql |
| 323 | if (!user.email) { |
| 324 | return '/login/error?error=EmailRequired'; |
| 325 | } |
| 326 | |
| 327 | return true; |
| 328 | }, |
| 329 | // Restrict post-auth redirects (sign-in / sign-out, `callbackUrl`, |
| 330 | // `redirectTo`) to the same origin as the application. This mirrors |
| 331 | // Auth.js's documented default; we set it explicitly so the protection |
no test coverage detected