mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
BREAKING CHANGE: The `session.jwt: boolean` option has been renamed to `session.strategy: "jwt" | "database"`. The goal is to make the user's options more intuitive: 1. No adapter, `strategy: "jwt"`: This is the default. The session is saved in a cookie and never persisted anywhere. 2. With Adapter, `strategy: "database"`: If an Adapter is defined, this will be the implicit setting. No user config is needed. 3. With Adapter, `strategy: "jwt"`: The user can explicitly instruct `next-auth` to use JWT even if a database is available. This can result in faster lookups in compromise of lowered security. Read more about: https://next-auth.js.org/faq#json-web-tokens Example: ```diff session: { - jwt: true, + strategy: "jwt", } ```
226 lines
8.7 KiB
TypeScript
226 lines
8.7 KiB
TypeScript
import { AccountNotLinkedError } from "../errors"
|
|
import { fromDate } from "./utils"
|
|
import { randomBytes, randomUUID } from "crypto"
|
|
import { InternalOptions } from "../../lib/types"
|
|
import { AdapterSession, AdapterUser } from "../../adapters"
|
|
import { JWT } from "../../jwt"
|
|
import { Account, User } from "../.."
|
|
import { SessionToken } from "./cookie"
|
|
|
|
/**
|
|
* 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
|
|
account: Account
|
|
options: InternalOptions
|
|
}) {
|
|
const { sessionToken, 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 },
|
|
} = 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, account, session: {} }
|
|
}
|
|
|
|
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 = profile.email
|
|
? await getUserByEmail(profile.email)
|
|
: null
|
|
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 newUser = { ...profile, emailVerified: new Date() }
|
|
delete (newUser as Omit<AdapterUser, "id">).id
|
|
// Create user account if there isn't one for the email address already
|
|
user = await createUser(newUser)
|
|
await events.createUser?.({ user })
|
|
isNewUser = true
|
|
}
|
|
|
|
// Create new session
|
|
session = useJwtSession
|
|
? {}
|
|
: await createSession({
|
|
sessionToken: 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: 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.
|
|
await linkAccount({ ...account, userId: user.id })
|
|
await events.linkAccount?.({ user, account })
|
|
|
|
// 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) {
|
|
// 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"
|
|
)
|
|
}
|
|
// 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 newUser = { ...profile, emailVerified: null }
|
|
delete (newUser as Omit<AdapterUser, "id">).id
|
|
user = await createUser(newUser)
|
|
await events.createUser?.({ user })
|
|
|
|
await linkAccount({ ...account, userId: user.id })
|
|
await events.linkAccount?.({ user, account })
|
|
|
|
session = useJwtSession
|
|
? {}
|
|
: await createSession({
|
|
sessionToken: generateSessionToken(),
|
|
userId: user.id,
|
|
expires: fromDate(options.session.maxAge),
|
|
})
|
|
|
|
return { session, user, isNewUser: true }
|
|
}
|
|
}
|
|
}
|
|
|
|
function generateSessionToken() {
|
|
// Use `randomUUID` if available. (Node 15.6++)
|
|
return randomUUID?.() ?? randomBytes(32).toString("hex")
|
|
}
|