Files
archived-next-auth/src/server/lib/callback-handler.js
Iain Collins b4bb8bda26 Hotfix for email_verified bug
Not being saved by default on sign in. Discovered in #477
2020-07-27 05:20:34 +01:00

224 lines
9.0 KiB
JavaScript

// 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.
import { AccountNotLinkedError } from '../../lib/errors'
import dispatchEvent from '../lib/dispatch-event'
export default async (sessionToken, profile, providerAccount, options) => {
try {
// Input validation
if (!profile) { throw new Error('Missing profile') }
if (!providerAccount || !providerAccount.id || !providerAccount.type) { throw new Error('Missing or invalid provider account') }
const { adapter, jwt, events } = options
const useJwtSession = options.session.jwt
// 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: providerAccount,
session: {}
}
}
const {
createUser,
updateUser,
getUser,
getUserByProviderAccountId,
getUserByEmail,
linkAccount,
createSession,
getSession,
deleteSession
} = await adapter.getAdapter(options)
let session = null
let user = null
let isSignedIn = null
let isNewUser = false
if (sessionToken) {
if (useJwtSession) {
try {
session = await jwt.decode({ ...jwt, token: sessionToken })
if (session && session.user) {
user = await getUser(session.user.id)
isSignedIn = !!user
}
} catch (e) {
// If session can't be verified, treat as no session
}
} else {
session = await getSession(sessionToken)
if (session && session.userId) {
user = await getUser(session.userId)
isSignedIn = !!user
}
}
}
if (providerAccount.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 (isSignedIn) {
if (user.id !== userByEmail.id && !useJwtSession) {
// 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
const currentDate = new Date()
user = await updateUser({ ...userByEmail, emailVerified: currentDate })
await dispatchEvent(events.updateUser, user)
} else {
// Create user account if there isn't one for the email address already
const currentDate = new Date()
user = await createUser({ ...profile, emailVerified: currentDate })
await dispatchEvent(events.createUser, user)
isNewUser = true
}
// Create new session
session = useJwtSession ? {} : await createSession(user)
return {
session,
user,
isNewUser
}
} else if (providerAccount.type === 'oauth') {
// If signing in with oauth account, check to see if the account exists already
const userByProviderAccountId = await getUserByProviderAccountId(providerAccount.provider, providerAccount.id)
if (userByProviderAccountId) {
if (isSignedIn) {
// If the user is already signed in with this account, we don't need to do anything
// Note: These are cast as strings here to ensure they match as in
// some flows (e.g. JWT with a database) one of the values might be a
// string and the other might be an ObjectID and would otherwise fail.
if (`${userByProviderAccountId.id}` === `${user.id}`) {
return {
session,
user,
isNewUser
}
} else {
// If the user is currently signed in, but the new account they are signing in
// with is already associated with another account, then we cannot link them
// and need to return an error.
throw new AccountNotLinkedError()
}
} else {
// 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(userByProviderAccountId)
return {
session,
user: userByProviderAccountId,
isNewUser
}
}
} else {
if (isSignedIn) {
// 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(
user.id,
providerAccount.provider,
providerAccount.type,
providerAccount.id,
providerAccount.refreshToken,
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount })
// 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()
} 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.
user = await createUser(profile)
await dispatchEvent(events.createUser, user)
await linkAccount(
user.id,
providerAccount.provider,
providerAccount.type,
providerAccount.id,
providerAccount.refreshToken,
providerAccount.accessToken,
providerAccount.accessTokenExpires
)
await dispatchEvent(events.linkAccount, { user, providerAccount })
session = useJwtSession ? {} : await createSession(user)
isNewUser = true
return {
session,
user,
isNewUser
}
}
}
} else {
return Promise.reject(new Error('Provider not supported'))
}
} catch (error) {
return Promise.reject(error)
}
}