Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add isNewUser arg on the signIn callback #8478

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
69 changes: 63 additions & 6 deletions packages/next-auth/src/core/lib/callback-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,66 @@ import type { Account, User } from "../.."
import type { SessionToken } from "./cookie"
import { OAuthConfig } from "src/providers"

/**
* This function validates the account object
*/
function accountValidation(account: Account | null) {
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")

return account
}

/**
* This function checks if a user is new and needs to be signed up, or if they
* are an existing user and need to be signed in. This is useful because it
* allows us to use this value when providing the isNewUser argument to the
* signIn callback.
*/
export async function checkIfUserIsNew(params: {
profile: User | AdapterUser | { email: string }
account: Account | null
options: InternalOptions
}) {
const { profile: _profile, account: _account, options } = params

const account = accountValidation(_account)

const { adapter } = options

if (!adapter) {
return undefined
}

const { getUserByAccount, getUserByEmail } = adapter

const profile = _profile as AdapterUser
const providerType = account.type as "oauth" | "email"

const userByEmail = await getUserByEmail(profile.email)

// if the user's email exists, it's not a new user
if (userByEmail) return false

// if the user's email doesn't exist and they are using the email
// provider, then they are a new user
if (providerType === "email") return true

// if they are using an OAuth provider, check if the account is already
// associated with a user account, if it is, then they are not a new user
const userByAccount = await getUserByAccount({
providerAccountId: account.providerAccountId,
provider: account.provider,
})
if (userByAccount) return false

// if they are using an OAuth provider and the account is not associated
// with a user account, then they are a new user
return true
}

/**
* 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
Expand All @@ -26,12 +86,9 @@ export default async function callbackHandler(params: {
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 { sessionToken, profile: _profile, account: _account, options } = params

const account = accountValidation(_account)

const {
adapter,
Expand Down
17 changes: 16 additions & 1 deletion packages/next-auth/src/core/routes/callback.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import oAuthCallback from "../lib/oauth/callback"
import callbackHandler from "../lib/callback-handler"
import callbackHandler, { checkIfUserIsNew } from "../lib/callback-handler"
import { hashToken } from "../lib/utils"
import getAdapterUserFromEmail from "../lib/email/getUserFromEmail"

Expand Down Expand Up @@ -90,10 +90,17 @@ export default async function callback(params: {
}

try {
const isNewUser = await checkIfUserIsNew({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be stored as let isNewUser = true on line 81 and set to false on line 89, couldn't it?

profile,
account,
options,
})

const isAllowed = await callbacks.signIn({
user: userOrProfile,
account,
profile: OAuthProfile,
isNewUser,
})
if (!isAllowed) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
Expand Down Expand Up @@ -232,9 +239,16 @@ export default async function callback(params: {

// Check if user is allowed to sign in
try {
const isNewUser = await checkIfUserIsNew({
profile,
account,
options,
})

const signInCallbackResponse = await callbacks.signIn({
user: profile,
account,
isNewUser,
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied`, cookies }
Expand Down Expand Up @@ -363,6 +377,7 @@ export default async function callback(params: {
// @ts-expect-error
account,
credentials,
isNewUser: false,
})
if (!isAllowed) {
return {
Expand Down
8 changes: 8 additions & 0 deletions packages/next-auth/src/core/routes/signin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import getAuthorizationUrl from "../lib/oauth/authorization-url"
import emailSignin from "../lib/email/signin"
import getAdapterUserFromEmail from "../lib/email/getUserFromEmail"
import { checkIfUserIsNew } from "../lib/callback-handler"
import type { RequestInternal, ResponseInternal } from ".."
import type { InternalOptions } from "../types"
import type { Account } from "../.."
Expand Down Expand Up @@ -70,10 +71,17 @@ export default async function signin(params: {

// Check if user is allowed to sign in
try {
const isNewUser = await checkIfUserIsNew({
profile: user,
account,
options,
})

const signInCallbackResponse = await callbacks.signIn({
user,
account,
email: { verificationRequest: true },
isNewUser,
})
if (!signInCallbackResponse) {
return { redirect: `${url}/error?error=AccessDenied` }
Expand Down
3 changes: 3 additions & 0 deletions packages/next-auth/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ export interface CallbacksOptions<P = Profile, A = Account> {
}
/** If Credentials provider is used, it contains the user credentials */
credentials?: Record<string, CredentialInput>

/** If the user doesn't already have an account, this field becomes true */
isNewUser?: boolean
}) => Awaitable<string | boolean>
/**
* This callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout).
Expand Down