UNPKG

next-auth

Version:

Authentication for Next.js

234 lines (213 loc) 9.55 kB
import { AccountNotLinkedError } from "../errors" import { fromDate } from "./utils" import type { InternalOptions } from "../types" import type { AdapterSession, AdapterUser } from "../../adapters" import type { JWT } from "../../jwt" import type { Account, User } from "../.." import type { SessionToken } from "./cookie" import { OAuthConfig } from "src/providers" /** * This function handles the complex flow of signing users in, and either creating, * linking (or not linking) accounts depending on if the user is currently logged * in, if they have account already and the authentication mechanism they are using. * * It prevents insecure behaviour, such as linking OAuth accounts unless a user is * signed in and authenticated with an existing valid account. * * All verification (e.g. OAuth flows or email address verificaiton flows) are * done prior to this handler being called to avoid additonal complexity in this * handler. */ export default async function callbackHandler(params: { sessionToken?: SessionToken profile: User | AdapterUser | { email: string } account: Account | null options: InternalOptions }) { const { sessionToken, profile: _profile, account, options } = params // Input validation if (!account?.providerAccountId || !account.type) throw new Error("Missing or invalid provider account") if (!["email", "oauth"].includes(account.type)) throw new Error("Provider not supported") const { adapter, jwt, events, session: { strategy: sessionStrategy, generateSessionToken }, } = options // If no adapter is configured then we don't have a database and cannot // persist data; in this mode we just return a dummy session object. if (!adapter) { return { user: _profile as User, account } } const profile = _profile as AdapterUser const { createUser, updateUser, getUser, getUserByAccount, getUserByEmail, linkAccount, createSession, getSessionAndUser, deleteSession, } = adapter let session: AdapterSession | JWT | null = null let user: AdapterUser | null = null let isNewUser = false const useJwtSession = sessionStrategy === "jwt" if (sessionToken) { if (useJwtSession) { try { session = await jwt.decode({ ...jwt, token: sessionToken }) if (session && "sub" in session && session.sub) { user = await getUser(session.sub) } } catch { // If session can't be verified, treat as no session } } else { const userAndSession = await getSessionAndUser(sessionToken) if (userAndSession) { session = userAndSession.session user = userAndSession.user } } } if (account.type === "email") { // If signing in with an email, check if an account with the same email address exists already const userByEmail = await getUserByEmail(profile.email) if (userByEmail) { // If they are not already signed in as the same user, this flow will // sign them out of the current session and sign them in as the new user if (user?.id !== userByEmail.id && !useJwtSession && sessionToken) { // Delete existing session if they are currently signed in as another user. // This will switch user accounts for the session in cases where the user was // already logged in with a different account. await deleteSession(sessionToken) } // Update emailVerified property on the user object user = await updateUser({ id: userByEmail.id, emailVerified: new Date() }) await events.updateUser?.({ user }) } else { const { id: _, ...newUser } = { ...profile, emailVerified: new Date() } // Create user account if there isn't one for the email address already // @ts-expect-error see adapters.ts' FutureAdapter["createUser"] user = await createUser(newUser) await events.createUser?.({ user }) isNewUser = true } // Create new session session = useJwtSession ? {} : await createSession({ sessionToken: await generateSessionToken(), userId: user.id, expires: fromDate(options.session.maxAge), }) return { session, user, isNewUser } } else if (account.type === "oauth") { // If signing in with OAuth account, check to see if the account exists already const userByAccount = await getUserByAccount({ providerAccountId: account.providerAccountId, provider: account.provider, }) if (userByAccount) { if (user) { // If the user is already signed in with this account, we don't need to do anything if (userByAccount.id === user.id) { return { session, user, isNewUser } } // If the user is currently signed in, but the new account they are signing in // with is already associated with another user, then we cannot link them // and need to return an error. throw new AccountNotLinkedError( "The account is already associated with another user" ) } // If there is no active session, but the account being signed in with is already // associated with a valid user then create session to sign the user in. session = useJwtSession ? {} : await createSession({ sessionToken: await generateSessionToken(), userId: userByAccount.id, expires: fromDate(options.session.maxAge), }) return { session, user: userByAccount, isNewUser } } else { if (user) { // If the user is already signed in and the OAuth account isn't already associated // with another user account then we can go ahead and link the accounts safely. // @ts-expect-error see adapters.ts' FutureAdapter["linkAccount"] await linkAccount({ ...account, userId: user.id }) await events.linkAccount?.({ user, account, profile }) // As they are already signed in, we don't need to do anything after linking them return { session, user, isNewUser } } // If the user is not signed in and it looks like a new OAuth account then we // check there also isn't an user account already associated with the same // email address as the one in the OAuth profile. // // This step is often overlooked in OAuth implementations, but covers the following cases: // // 1. It makes it harder for someone to accidentally create two accounts. // e.g. by signin in with email, then again with an oauth account connected to the same email. // 2. It makes it harder to hijack a user account using a 3rd party OAuth account. // e.g. by creating an oauth account then changing the email address associated with it. // // It's quite common for services to automatically link accounts in this case, but it's // better practice to require the user to sign in *then* link accounts to be sure // someone is not exploiting a problem with a third party OAuth service. // // OAuth providers should require email address verification to prevent this, but in // practice that is not always the case; this helps protect against that. const userByEmail = profile.email ? await getUserByEmail(profile.email) : null if (userByEmail) { const provider = options.provider as OAuthConfig<any> if (provider?.allowDangerousEmailAccountLinking) { // If you trust the oauth provider to correctly verify email addresses, you can opt-in to // account linking even when the user is not signed-in. user = userByEmail } else { // We end up here when we don't have an account with the same [provider].id *BUT* // we do already have an account with the same email address as the one in the // OAuth profile the user has just tried to sign in with. // // We don't want to have two accounts with the same email address, and we don't // want to link them in case it's not safe to do so, so instead we prompt the user // to sign in via email to verify their identity and then link the accounts. throw new AccountNotLinkedError( "Another account already exists with the same e-mail address" ) } } else { // If the current user is not logged in and the profile isn't linked to any user // accounts (by email or provider account id)... // // If no account matching the same [provider].id or .email exists, we can // create a new account for the user, link it to the OAuth acccount and // create a new session for them so they are signed in with it. const { id: _, ...newUser } = { ...profile, emailVerified: null } // @ts-expect-error see adapters.ts' FutureAdapter["createUser"] user = await createUser(newUser) } await events.createUser?.({ user }) // @ts-expect-error see adapters.ts' FutureAdapter["linkAccount"] await linkAccount({ ...account, userId: user.id }) await events.linkAccount?.({ user, account, profile }) session = useJwtSession ? {} : await createSession({ sessionToken: await generateSessionToken(), userId: user.id, expires: fromDate(options.session.maxAge), }) return { session, user, isNewUser: true } } } throw new Error("Unsupported account type") }