mirror of
https://github.com/SrIzan10/next-auth.git
synced 2026-05-01 10:55:20 +00:00
- Cleans up logging. Logs are now color-coded, added more debug logs, and errors can include some simple metadata (like provider id) to know which provider caused an issue. - All errors are exposed via `@auth/core/errors`. Each error has a URL like: https://errors.authjs.dev#errorcode in the terminal, which points to the documentation explaining the problem in detail, suggesting a fix. - Added a bunch of documentation that autogenerates the pages under https://authjs.dev/reference/core/modules/main - Renames `AuthHandler` to `Auth` and `AuthOptions` to `AuthConfig` - Throwing an error in `signIn` callback will now be caught as a general error and will redirect to `/error?error=Configuration`. If the callback returns `false`, it will redirect to `/error?error=AccessDenied`.
228 lines
9.0 KiB
TypeScript
228 lines
9.0 KiB
TypeScript
import { AccountNotLinked } from "../errors.js"
|
|
import { fromDate } from "./utils/date.js"
|
|
|
|
import type { AdapterSession, AdapterUser } from "../adapters.js"
|
|
import type { Account, InternalOptions, User } from "../types.js"
|
|
import type { JWT } from "../jwt.js"
|
|
import type { OAuthConfig } from "../providers/index.js"
|
|
import type { SessionToken } from "./cookie.js"
|
|
|
|
/**
|
|
* 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 async function handleLogin(
|
|
sessionToken: SessionToken,
|
|
_profile: User | AdapterUser | { email: string },
|
|
account: Account | null,
|
|
options: InternalOptions
|
|
) {
|
|
// Input validation
|
|
if (!account?.providerAccountId || !account.type)
|
|
throw new Error("Missing or invalid provider account")
|
|
if (!["email", "oauth", "oidc"].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
|
|
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" || account.type === "oidc") {
|
|
// 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 AccountNotLinked(
|
|
"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, 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 AccountNotLinked(
|
|
"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 }
|
|
user = await createUser(newUser)
|
|
}
|
|
await events.createUser?.({ user })
|
|
|
|
await linkAccount({ ...account, userId: user.id })
|
|
await events.linkAccount?.({ user, account, profile })
|
|
|
|
session = useJwtSession
|
|
? {}
|
|
: await createSession({
|
|
sessionToken: generateSessionToken(),
|
|
userId: user.id,
|
|
expires: fromDate(options.session.maxAge),
|
|
})
|
|
|
|
return { session, user, isNewUser: true }
|
|
}
|
|
}
|
|
|
|
throw new Error("Unsupported account type")
|
|
}
|