Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/auth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ import { pickVerifiedGitHubEmail, type GitHubEmail } from "@/lib/auth/githubEmai
* middleware only ever *reads* an existing session.
*/

/**
* WHY `allowDangerousEmailAccountLinking` IS SET, AND WHAT MAKES IT SAFE
*
* Without it, Auth.js refuses outright to attach an OAuth identity to an
* existing account with the same email, and answers OAuthAccountNotLinked. That
* is the correct default for an app with no opinion of its own — but it is
* fatal here, because every account that predates this migration already exists
* with an email address, so every one of those users would be permanently
* locked out of Google and GitHub sign-in.
*
* The flag is named "dangerous" because Auth.js cannot tell whether the
* provider verified the address. Turning it on alone would mean anyone able to
* add a victim's address to their own Google or GitHub account inherits the
* victim's Commit account and its role.
*
* What makes it safe is that we answer that question ourselves, before Auth.js
* acts on it. The `signIn` callback in src/auth.ts applies
* `mayLinkToExistingUser`, which refuses unless the provider positively asserts
* the address is verified — and @auth/core runs `handleAuthorized` (the signIn
* callback) before `handleLoginOrRegister` (the linking), so returning false
* there is a prevention rather than a post-mortem.
*
* These two are therefore a pair. Removing the signIn callback, or weakening
* mayLinkToExistingUser, silently turns this flag back into what its name says
* it is. The GitHub userinfo override below exists for the same reason: the
* stock provider discards the verified flag, and a guess in that argument
* defeats the guard entirely.
*/

const GITHUB_API = "https://api.github.com";

export const authConfig = {
Expand All @@ -40,9 +69,11 @@ export const authConfig = {
providers: [
// Google is an OIDC provider, so `email_verified` arrives as a standard
// claim and needs no help.
Google,
Google({ allowDangerousEmailAccountLinking: true }),

GitHub({
allowDangerousEmailAccountLinking: true,

userinfo: {
url: `${GITHUB_API}/user`,

Expand Down
7 changes: 7 additions & 0 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,13 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
* `handleLoginOrRegister` in the @auth/core callback route, which is what
* makes returning false here a prevention rather than a post-mortem.
*
* THIS FUNCTION IS LOAD-BEARING. Both providers in src/auth.config.ts set
* `allowDangerousEmailAccountLinking: true`, which switches off Auth.js's
* own refusal to attach an OAuth identity to an existing account. That flag
* is only safe because this callback answers the question Auth.js cannot:
* whether the provider actually verified the address. Delete or weaken this
* and the flag becomes exactly what its name says.
*
* The rule itself lives in src/lib/auth/linking.ts with its own tests. This
* function's only job is to feed it honest inputs — which is also why
* src/auth.config.ts overrides the GitHub userinfo request. The stock
Expand Down
Loading