@auth/core
Advanced tools
Comparing version 0.7.1 to 0.8.0
@@ -218,2 +218,6 @@ /** | ||
deleteUser?(userId: string): Promise<void> | Awaitable<AdapterUser | null | undefined>; | ||
/** | ||
* This method is invoked internally (but optionally can be used for manual linking). | ||
* It creates an [Account](https://authjs.dev/reference/adapters#models) in the database. | ||
*/ | ||
linkAccount?(account: AdapterAccount): Promise<void> | Awaitable<AdapterAccount | null | undefined>; | ||
@@ -220,0 +224,0 @@ /** @todo This method is currently not invoked yet. */ |
@@ -8,9 +8,2 @@ interface ErrorCause extends Record<string, unknown> { | ||
* @todo | ||
* Thrown when an Email address is already associated with an account | ||
* but the user is trying an OAuth account that is not linked to it. | ||
*/ | ||
export declare class AccountNotLinked extends AuthError { | ||
} | ||
/** | ||
* @todo | ||
* One of the database `Adapter` methods failed. | ||
@@ -24,4 +17,4 @@ */ | ||
/** | ||
* There was an error while trying to finish up authenticating the user. | ||
* Depending on the type of provider, this could be for multiple reasons. | ||
* This error occurs when the user cannot finish the sign-in process. | ||
* Depending on the provider type, this could have happened for multiple reasons. | ||
* | ||
@@ -36,3 +29,3 @@ * :::tip | ||
* | ||
* For an **OAuth provider**, possible causes are: | ||
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are: | ||
* - The user denied access to the application | ||
@@ -45,3 +38,3 @@ * - There was an error parsing the OAuth Profile: | ||
* | ||
* For an **Email provider**, possible causes are: | ||
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are: | ||
* - The provided email/token combination was invalid/missing: | ||
@@ -54,3 +47,3 @@ * Check if the provider's `sendVerificationRequest` method correctly sends the email. | ||
* | ||
* For a **Credentials provider**, possible causes are: | ||
* For a [Credentials provider](https://authjs.dev/reference/core/providers_credentials), possible causes are: | ||
* - The `authorize` method threw an uncaught error: | ||
@@ -98,9 +91,33 @@ * Check the provider's `authorize` method. | ||
} | ||
/** @todo */ | ||
/** | ||
* Auth.js requires a secret to be set, but none was not found. This is used to encrypt cookies, JWTs and other sensitive data. | ||
* | ||
* :::note | ||
* If you are using a framework like Next.js, we try to automatically infer the secret from the `AUTH_SECRET` environment variable. | ||
* Alternatively, you can also explicitly set the [`AuthConfig.secret`](https://authjs.dev/reference/core#secret). | ||
* ::: | ||
* | ||
* | ||
* :::tip | ||
* You can generate a good secret value: | ||
* - On Unix systems: type `openssl rand -hex 32` in the terminal | ||
* - Or generate one [online](https://generate-secret.vercel.app/32) | ||
* | ||
* ::: | ||
*/ | ||
export declare class MissingSecret extends AuthError { | ||
} | ||
/** @todo */ | ||
export declare class OAuthSignInError extends AuthError { | ||
/** | ||
* @todo | ||
* Thrown when an Email address is already associated with an account | ||
* but the user is trying an OAuth account that is not linked to it. | ||
*/ | ||
export declare class OAuthAccountNotLinked extends AuthError { | ||
} | ||
/** @todo */ | ||
/** | ||
* Thrown when an OAuth provider returns an error during the sign in process. | ||
* This could happen for example if the user denied access to the application or there was a configuration error. | ||
* | ||
* For a full list of possible reasons, check out the specification [Authorization Code Grant: Error Response](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1) | ||
*/ | ||
export declare class OAuthCallbackError extends AuthError { | ||
@@ -111,3 +128,7 @@ } | ||
} | ||
/** @todo */ | ||
/** | ||
* This error occurs during an OAuth sign in attempt when the provdier's | ||
* response could not be parsed. This could for example happen if the provider's API | ||
* changed, or the [`OAuth2Config.profile`](https://authjs.dev/reference/core/providers_oauth#profile) method is not implemented correctly. | ||
*/ | ||
export declare class OAuthProfileParseError extends AuthError { | ||
@@ -118,3 +139,27 @@ } | ||
} | ||
/** @todo */ | ||
/** | ||
* This error occurs when the user cannot initiate the sign-in process. | ||
* Depending on the provider type, this could have happened for multiple reasons. | ||
* | ||
* :::tip | ||
* Check out `[auth][details]` in the error message to know which provider failed. | ||
* @example | ||
* ```sh | ||
* [auth][details]: { "provider": "github" } | ||
* ``` | ||
* ::: | ||
* | ||
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are: | ||
* - The Authorization Server is not compliant with the [OAuth 2.0 specifcation](https://www.ietf.org/rfc/rfc6749.html) | ||
* Check the details in the error message. | ||
* - A runtime error occurred in Auth.js. This should be reported as a bug. | ||
* | ||
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are: | ||
* - The email sent from the client is invalid, could not be normalized by [`EmailConfig.normalizeIdentifier`](https://authjs.dev/reference/core/providers_email#normalizeidentifier) | ||
* - The provided email/token combination has expired: | ||
* Ask the user to log in again. | ||
* - There was an error with the database: | ||
* Check the database logs. | ||
* | ||
*/ | ||
export declare class SignInError extends AuthError { | ||
@@ -125,3 +170,7 @@ } | ||
} | ||
/** @todo */ | ||
/** | ||
* Auth.js was requested to handle an operation that it does not support. | ||
* | ||
* See [`AuthAction`](https://authjs.dev/reference/core/types#authaction) for the supported actions. | ||
*/ | ||
export declare class UnknownAction extends AuthError { | ||
@@ -128,0 +177,0 @@ } |
@@ -24,9 +24,2 @@ export class AuthError extends Error { | ||
* @todo | ||
* Thrown when an Email address is already associated with an account | ||
* but the user is trying an OAuth account that is not linked to it. | ||
*/ | ||
export class AccountNotLinked extends AuthError { | ||
} | ||
/** | ||
* @todo | ||
* One of the database `Adapter` methods failed. | ||
@@ -40,4 +33,4 @@ */ | ||
/** | ||
* There was an error while trying to finish up authenticating the user. | ||
* Depending on the type of provider, this could be for multiple reasons. | ||
* This error occurs when the user cannot finish the sign-in process. | ||
* Depending on the provider type, this could have happened for multiple reasons. | ||
* | ||
@@ -52,3 +45,3 @@ * :::tip | ||
* | ||
* For an **OAuth provider**, possible causes are: | ||
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are: | ||
* - The user denied access to the application | ||
@@ -61,3 +54,3 @@ * - There was an error parsing the OAuth Profile: | ||
* | ||
* For an **Email provider**, possible causes are: | ||
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are: | ||
* - The provided email/token combination was invalid/missing: | ||
@@ -70,3 +63,3 @@ * Check if the provider's `sendVerificationRequest` method correctly sends the email. | ||
* | ||
* For a **Credentials provider**, possible causes are: | ||
* For a [Credentials provider](https://authjs.dev/reference/core/providers_credentials), possible causes are: | ||
* - The `authorize` method threw an uncaught error: | ||
@@ -114,9 +107,33 @@ * Check the provider's `authorize` method. | ||
} | ||
/** @todo */ | ||
/** | ||
* Auth.js requires a secret to be set, but none was not found. This is used to encrypt cookies, JWTs and other sensitive data. | ||
* | ||
* :::note | ||
* If you are using a framework like Next.js, we try to automatically infer the secret from the `AUTH_SECRET` environment variable. | ||
* Alternatively, you can also explicitly set the [`AuthConfig.secret`](https://authjs.dev/reference/core#secret). | ||
* ::: | ||
* | ||
* | ||
* :::tip | ||
* You can generate a good secret value: | ||
* - On Unix systems: type `openssl rand -hex 32` in the terminal | ||
* - Or generate one [online](https://generate-secret.vercel.app/32) | ||
* | ||
* ::: | ||
*/ | ||
export class MissingSecret extends AuthError { | ||
} | ||
/** @todo */ | ||
export class OAuthSignInError extends AuthError { | ||
/** | ||
* @todo | ||
* Thrown when an Email address is already associated with an account | ||
* but the user is trying an OAuth account that is not linked to it. | ||
*/ | ||
export class OAuthAccountNotLinked extends AuthError { | ||
} | ||
/** @todo */ | ||
/** | ||
* Thrown when an OAuth provider returns an error during the sign in process. | ||
* This could happen for example if the user denied access to the application or there was a configuration error. | ||
* | ||
* For a full list of possible reasons, check out the specification [Authorization Code Grant: Error Response](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1) | ||
*/ | ||
export class OAuthCallbackError extends AuthError { | ||
@@ -127,3 +144,7 @@ } | ||
} | ||
/** @todo */ | ||
/** | ||
* This error occurs during an OAuth sign in attempt when the provdier's | ||
* response could not be parsed. This could for example happen if the provider's API | ||
* changed, or the [`OAuth2Config.profile`](https://authjs.dev/reference/core/providers_oauth#profile) method is not implemented correctly. | ||
*/ | ||
export class OAuthProfileParseError extends AuthError { | ||
@@ -134,3 +155,27 @@ } | ||
} | ||
/** @todo */ | ||
/** | ||
* This error occurs when the user cannot initiate the sign-in process. | ||
* Depending on the provider type, this could have happened for multiple reasons. | ||
* | ||
* :::tip | ||
* Check out `[auth][details]` in the error message to know which provider failed. | ||
* @example | ||
* ```sh | ||
* [auth][details]: { "provider": "github" } | ||
* ``` | ||
* ::: | ||
* | ||
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are: | ||
* - The Authorization Server is not compliant with the [OAuth 2.0 specifcation](https://www.ietf.org/rfc/rfc6749.html) | ||
* Check the details in the error message. | ||
* - A runtime error occurred in Auth.js. This should be reported as a bug. | ||
* | ||
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are: | ||
* - The email sent from the client is invalid, could not be normalized by [`EmailConfig.normalizeIdentifier`](https://authjs.dev/reference/core/providers_email#normalizeidentifier) | ||
* - The provided email/token combination has expired: | ||
* Ask the user to log in again. | ||
* - There was an error with the database: | ||
* Check the database logs. | ||
* | ||
*/ | ||
export class SignInError extends AuthError { | ||
@@ -141,3 +186,7 @@ } | ||
} | ||
/** @todo */ | ||
/** | ||
* Auth.js was requested to handle an operation that it does not support. | ||
* | ||
* See [`AuthAction`](https://authjs.dev/reference/core/types#authaction) for the supported actions. | ||
*/ | ||
export class UnknownAction extends AuthError { | ||
@@ -144,0 +193,0 @@ } |
@@ -98,3 +98,3 @@ /** | ||
* | ||
* @default 30 * 24 * 30 * 60 // 30 days | ||
* @default 30 * 24 * 60 * 60 // 30 days | ||
*/ | ||
@@ -120,3 +120,3 @@ maxAge?: number; | ||
* | ||
* @default 30 * 24 * 30 * 60 // 30 days | ||
* @default 30 * 24 * 60 * 60 // 30 days | ||
*/ | ||
@@ -123,0 +123,0 @@ maxAge: number; |
@@ -1,2 +0,2 @@ | ||
import { AccountNotLinked } from "../errors.js"; | ||
import { OAuthAccountNotLinked } from "../errors.js"; | ||
import { fromDate } from "./utils/date.js"; | ||
@@ -28,3 +28,3 @@ /** | ||
const profile = _profile; | ||
const account = _account; | ||
let account = _account; | ||
const { createUser, updateUser, getUser, getUserByAccount, getUserByEmail, linkAccount, createSession, getSessionAndUser, deleteSession, } = adapter; | ||
@@ -88,101 +88,102 @@ let session = null; | ||
} | ||
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", { provider: account.provider }); | ||
// 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 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 }; | ||
// 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 OAuthAccountNotLinked("The account is already associated with another user", { provider: account.provider }); | ||
} | ||
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 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 { | ||
const { provider: p } = options; | ||
const { type, provider, providerAccountId, userId, ...tokenSet } = account; | ||
const defaults = { providerAccountId, provider, type, userId }; | ||
account = Object.assign(p.account(tokenSet) ?? {}, defaults); | ||
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; | ||
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; | ||
} | ||
// 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; | ||
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", { provider: account.provider }); | ||
} | ||
} | ||
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)... | ||
// 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. | ||
// | ||
// 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 account and | ||
// create a new session for them so they are signed in with it. | ||
const { id: _, ...newUser } = { ...profile, emailVerified: null }; | ||
user = await createUser(newUser); | ||
// 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 OAuthAccountNotLinked("Another account already exists with the same e-mail address", { provider: account.provider }); | ||
} | ||
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 }; | ||
} | ||
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 account 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"); | ||
} |
@@ -29,3 +29,3 @@ import { UnknownAction } from "../errors.js"; | ||
case "session": { | ||
const session = await routes.session(sessionStore, options); | ||
const session = await routes.session({ sessionStore, options }); | ||
if (session.cookies) | ||
@@ -91,4 +91,2 @@ cookies.push(...session.cookies); | ||
"Signin", | ||
"OAuthSignin", | ||
"OAuthCallback", | ||
"OAuthCreateAccount", | ||
@@ -98,4 +96,2 @@ "EmailCreateAccount", | ||
"OAuthAccountNotLinked", | ||
"EmailSignin", | ||
"CredentialsSignin", | ||
"SessionRequired", | ||
@@ -155,2 +151,18 @@ ].includes(error)) { | ||
break; | ||
case "session": { | ||
if (options.csrfTokenVerified) { | ||
const session = await routes.session({ | ||
options, | ||
sessionStore, | ||
newSession: request.body?.data, | ||
isUpdate: true, | ||
}); | ||
if (session.cookies) | ||
cookies.push(...session.cookies); | ||
return { ...session, cookies }; | ||
} | ||
// If CSRF token is invalid, return a 400 status code | ||
// we should not redirect to a page as this is an API route | ||
return { status: 400, cookies }; | ||
} | ||
default: | ||
@@ -157,0 +169,0 @@ } |
@@ -13,4 +13,5 @@ import type { InternalOptions, Profile, RequestInternal } from "../../types.js"; | ||
export declare function handleOAuth(query: RequestInternal["query"], cookies: RequestInternal["cookies"], options: InternalOptions<"oauth" | "oidc">, randomState?: string): Promise<{ | ||
profile: Profile; | ||
cookies: Cookie[]; | ||
profile?: import("../../types.js").User | undefined; | ||
user?: import("../../types.js").User | undefined; | ||
account?: { | ||
@@ -23,2 +24,3 @@ access_token?: string | undefined; | ||
token_type?: string | undefined; | ||
expires_at?: number | undefined; | ||
provider: string; | ||
@@ -34,2 +36,3 @@ type: "oauth" | "oidc"; | ||
token_type?: string | undefined; | ||
expires_at?: number | undefined; | ||
provider: string; | ||
@@ -39,4 +42,3 @@ type: "oauth" | "oidc"; | ||
} | undefined; | ||
OAuthProfile?: Profile | undefined; | ||
}>; | ||
//# sourceMappingURL=callback.d.ts.map |
@@ -48,7 +48,5 @@ import * as checks from "./checks.js"; | ||
if (o.isOAuth2Error(codeGrantParams)) { | ||
logger.debug("OAuthCallbackError", { | ||
providerId: provider.id, | ||
...codeGrantParams, | ||
}); | ||
throw new OAuthCallbackError(codeGrantParams.error); | ||
const cause = { providerId: provider.id, ...codeGrantParams }; | ||
logger.debug("OAuthCallbackError", cause); | ||
throw new OAuthCallbackError("OAuth Provider returned an error", cause); | ||
} | ||
@@ -74,3 +72,3 @@ const codeVerifier = await checks.pkce.use(cookies, resCookies, options); | ||
} | ||
let profile = {}; | ||
let profile; | ||
let tokens; | ||
@@ -100,23 +98,29 @@ if (provider.type === "oidc") { | ||
} | ||
else { | ||
throw new TypeError("No userinfo endpoint configured"); | ||
} | ||
} | ||
const profileResult = await getProfile(profile, provider, tokens, logger); | ||
return { ...profileResult, cookies: resCookies }; | ||
if (tokens.expires_in) { | ||
tokens.expires_at = | ||
Math.floor(Date.now() / 1000) + Number(tokens.expires_in); | ||
} | ||
const profileResult = await getUserAndAccount(profile, provider, tokens, logger); | ||
return { ...profileResult, profile, cookies: resCookies }; | ||
} | ||
/** Returns profile, raw profile and auth provider details */ | ||
async function getProfile(OAuthProfile, provider, tokens, logger) { | ||
/** Returns the user and account that is going to be created in the database. */ | ||
async function getUserAndAccount(OAuthProfile, provider, tokens, logger) { | ||
try { | ||
const profile = await provider.profile(OAuthProfile, tokens); | ||
profile.email = profile.email?.toLowerCase(); | ||
if (!profile.id) { | ||
throw new TypeError(`Profile id is missing in ${provider.name} OAuth profile response`); | ||
const user = await provider.profile(OAuthProfile, tokens); | ||
user.email = user.email?.toLowerCase(); | ||
if (!user.id) { | ||
throw new TypeError(`User id is missing in ${provider.name} OAuth profile response`); | ||
} | ||
return { | ||
profile, | ||
user, | ||
account: { | ||
provider: provider.id, | ||
type: provider.type, | ||
providerAccountId: profile.id.toString(), | ||
providerAccountId: user.id.toString(), | ||
...tokens, | ||
}, | ||
OAuthProfile, | ||
}; | ||
@@ -133,4 +137,4 @@ } | ||
logger.debug("getProfile error details", OAuthProfile); | ||
logger.error(new OAuthProfileParseError(e)); | ||
logger.error(new OAuthProfileParseError(e, { provider: provider.id })); | ||
} | ||
} |
@@ -6,3 +6,3 @@ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime"; | ||
oauthsignin: "Try signing in with a different account.", | ||
oauthcallback: "Try signing in with a different account.", | ||
oauthcallbackerror: "Try signing in with a different account.", | ||
oauthcreateaccount: "Try signing in with a different account.", | ||
@@ -9,0 +9,0 @@ emailcreateaccount: "Try signing in with a different account.", |
@@ -0,1 +1,2 @@ | ||
import { OAuthProfileParseError } from "../errors.js"; | ||
import { merge } from "./utils/merge.js"; | ||
@@ -51,11 +52,44 @@ /** | ||
profile: c.profile ?? defaultProfile, | ||
account: c.account ?? defaultAccount, | ||
}; | ||
} | ||
function defaultProfile(profile) { | ||
return { | ||
id: profile.sub ?? profile.id, | ||
name: profile.name ?? profile.nickname ?? profile.preferred_username ?? null, | ||
email: profile.email ?? null, | ||
image: profile.picture ?? null, | ||
}; | ||
/** | ||
* Returns basic user profile from the userinfo response/`id_token` claims. | ||
* @see https://authjs.dev/reference/adapters#user | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo | ||
*/ | ||
const defaultProfile = (profile) => { | ||
const id = profile.sub ?? profile.id; | ||
if (!id) | ||
throw new OAuthProfileParseError("Missing user id"); | ||
return stripUndefined({ | ||
id: id.toString(), | ||
name: profile.name ?? profile.nickname ?? profile.preferred_username, | ||
email: profile.email, | ||
image: profile.picture, | ||
}); | ||
}; | ||
/** | ||
* Returns basic OAuth/OIDC values from the token response. | ||
* @see https://www.ietf.org/rfc/rfc6749.html#section-5.1 | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse | ||
* @see https://authjs.dev/reference/adapters#account | ||
*/ | ||
const defaultAccount = (account) => { | ||
return stripUndefined({ | ||
access_token: account.access_token, | ||
id_token: account.id_token, | ||
refresh_token: account.refresh_token, | ||
expires_at: account.expires_at, | ||
scope: account.scope, | ||
token_type: account.token_type, | ||
session_state: account.session_state, | ||
}); | ||
}; | ||
function stripUndefined(o) { | ||
const result = {}; | ||
for (let [k, v] of Object.entries(o)) | ||
v !== undefined && (result[k] = v); | ||
return result; | ||
} | ||
@@ -62,0 +96,0 @@ function normalizeEndpoint(e, issuer) { |
@@ -1,2 +0,2 @@ | ||
import { CallbackRouteError, Verification } from "../../errors.js"; | ||
import { CallbackRouteError, OAuthCallbackError, Verification, } from "../../errors.js"; | ||
import { handleLogin } from "../callback-handler.js"; | ||
@@ -25,3 +25,3 @@ import { handleOAuth } from "../oauth/callback.js"; | ||
logger.debug("authorization result", authorizationResult); | ||
const { profile, account, OAuthProfile } = authorizationResult; | ||
const { user: userFromProvider, account, profile: OAuthProfile, } = authorizationResult; | ||
// If we don't have a profile object then either something went wrong | ||
@@ -32,3 +32,3 @@ // or the user cancelled signing in. We don't know which, so we just | ||
// TODO: Handle user cancelling signin | ||
if (!profile || !account || !OAuthProfile) { | ||
if (!userFromProvider || !account || !OAuthProfile) { | ||
return { redirect: `${url}/signin`, cookies }; | ||
@@ -40,3 +40,3 @@ } | ||
// (that just means it's a new user signing in for the first time). | ||
let userOrProfile = profile; | ||
let userByAccountOrFromProvider; | ||
if (adapter) { | ||
@@ -49,9 +49,13 @@ const { getUserByAccount } = adapter; | ||
if (userByAccount) | ||
userOrProfile = userByAccount; | ||
userByAccountOrFromProvider = userByAccount; | ||
} | ||
const unauthorizedOrError = await handleAuthorized({ user: userOrProfile, account, profile: OAuthProfile }, options); | ||
const unauthorizedOrError = await handleAuthorized({ | ||
user: userByAccountOrFromProvider, | ||
account, | ||
profile: OAuthProfile, | ||
}, options); | ||
if (unauthorizedOrError) | ||
return { ...unauthorizedOrError, cookies }; | ||
// Sign user in | ||
const { user, session, isNewUser } = await handleLogin(sessionStore.value, profile, account, options); | ||
const { user, session, isNewUser } = await handleLogin(sessionStore.value, userFromProvider, account, options); | ||
if (useJwtSession) { | ||
@@ -70,2 +74,3 @@ const defaultToken = { | ||
isNewUser, | ||
trigger: isNewUser ? "signUp" : "signIn", | ||
}); | ||
@@ -99,3 +104,3 @@ // Clear cookies if token is null | ||
} | ||
await events.signIn?.({ user, account, profile, isNewUser }); | ||
await events.signIn?.({ user, account, profile: OAuthProfile, isNewUser }); | ||
// Handle first logins on new accounts | ||
@@ -160,2 +165,3 @@ // e.g. option to send users to a new account landing page on initial login | ||
isNewUser, | ||
trigger: isNewUser ? "signUp" : "signIn", | ||
}); | ||
@@ -240,2 +246,3 @@ // Clear cookies if token is null | ||
isNewUser: false, | ||
trigger: "signIn", | ||
}); | ||
@@ -268,3 +275,12 @@ // Clear cookies if token is null | ||
catch (e) { | ||
if (e instanceof OAuthCallbackError) { | ||
logger.error(e); | ||
// REVIEW: Should we expose original error= and error_description= | ||
// Should we use a different name for error= then, since we already use it for all kind of errors? | ||
url.searchParams.set("error", OAuthCallbackError.name); | ||
url.pathname += "/signin"; | ||
return { redirect: url.toString(), cookies }; | ||
} | ||
const error = new CallbackRouteError(e, { provider: provider.id }); | ||
logger.debug("callback route error details", { method, query, body }); | ||
logger.error(error); | ||
@@ -271,0 +287,0 @@ url.searchParams.set("error", CallbackRouteError.name); |
import type { InternalOptions, ResponseInternal, Session } from "../../types.js"; | ||
import type { SessionStore } from "../cookie.js"; | ||
/** Return a session object filtered via `callbacks.session` */ | ||
export declare function session(sessionStore: SessionStore, options: InternalOptions): Promise<ResponseInternal<Session | null>>; | ||
export declare function session(params: { | ||
options: InternalOptions; | ||
sessionStore: SessionStore; | ||
isUpdate?: boolean; | ||
newSession?: any; | ||
}): Promise<ResponseInternal<Session | null>>; | ||
//# sourceMappingURL=session.d.ts.map |
import { JWTSessionError, SessionTokenError } from "../../errors.js"; | ||
import { fromDate } from "../utils/date.js"; | ||
/** Return a session object filtered via `callbacks.session` */ | ||
export async function session(sessionStore, options) { | ||
export async function session(params) { | ||
const { options, sessionStore, newSession, isUpdate } = params; | ||
const { adapter, jwt, events, callbacks, logger, session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, } = options; | ||
@@ -17,16 +18,18 @@ const response = { | ||
const decodedToken = await jwt.decode({ ...jwt, token: sessionToken }); | ||
if (!decodedToken) | ||
throw new Error("Invalid JWT"); | ||
// @ts-expect-error | ||
const token = await callbacks.jwt({ | ||
token: decodedToken, | ||
...(isUpdate && { trigger: "update" }), | ||
session: newSession, | ||
}); | ||
const newExpires = fromDate(sessionMaxAge); | ||
// By default, only exposes a limited subset of information to the client | ||
// as needed for presentation purposes (e.g. "you are logged in as..."). | ||
const session = { | ||
user: { | ||
name: decodedToken?.name, | ||
email: decodedToken?.email, | ||
image: decodedToken?.picture, | ||
}, | ||
expires: newExpires.toISOString(), | ||
}; | ||
// @ts-expect-error | ||
const token = await callbacks.jwt({ token: decodedToken }); | ||
if (token !== null) { | ||
// By default, only exposes a limited subset of information to the client | ||
// as needed for presentation purposes (e.g. "you are logged in as..."). | ||
const session = { | ||
user: { name: token.name, email: token.email, image: token.picture }, | ||
expires: newExpires.toISOString(), | ||
}; | ||
// @ts-expect-error | ||
@@ -93,10 +96,8 @@ const newSession = await callbacks.session({ session, token }); | ||
session: { | ||
user: { | ||
name: user.name, | ||
email: user.email, | ||
image: user.image, | ||
}, | ||
user: { name: user.name, email: user.email, image: user.image }, | ||
expires: session.expires.toISOString(), | ||
}, | ||
user, | ||
newSession, | ||
...(isUpdate ? { trigger: "update" } : {}), | ||
}); | ||
@@ -103,0 +104,0 @@ // Return session payload as response |
@@ -41,4 +41,5 @@ import emailSignin from "../email/signin.js"; | ||
logger.error(error); | ||
url.searchParams.set("error", error.name); | ||
url.pathname += "/error"; | ||
const code = provider.type === "email" ? "EmailSignin" : "OAuthSignin"; | ||
url.searchParams.set("error", code); | ||
url.pathname += "/signin"; | ||
return { redirect: url.toString() }; | ||
@@ -45,0 +46,0 @@ } |
@@ -30,2 +30,4 @@ import { parse as parseCookie, serialize } from "cookie"; | ||
const url = new URL(req.url.replace(/\/$/, "")); | ||
// FIXME: Upstream issue in Next.js, pathname segments get included as part of the query string | ||
url.searchParams.delete("nextauth"); | ||
const { pathname } = url; | ||
@@ -32,0 +34,0 @@ const action = actions.find((a) => pathname.includes(a)); |
{ | ||
"name": "@auth/core", | ||
"version": "0.7.1", | ||
"version": "0.8.0", | ||
"description": "Authentication for the Web.", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -27,3 +27,4 @@ import type { CommonProviderOptions } from "./index.js"; | ||
* //... | ||
* async authorize(, request) { | ||
* async authorize(credentials, request) { | ||
* if(!isValidCredentials(credentials)) return null | ||
* const response = await fetch(request) | ||
@@ -34,2 +35,3 @@ * if(!response.ok) return null | ||
* //... | ||
* ``` | ||
*/ | ||
@@ -36,0 +38,0 @@ authorize: ( |
@@ -27,3 +27,3 @@ import type { CommonProviderOptions } from "./index.js"; | ||
type: "email"; | ||
server: string | SMTPTransportOptions; | ||
server?: string | SMTPTransportOptions; | ||
/** @default `"Auth.js <no-reply@authjs.dev>"` */ | ||
@@ -69,3 +69,3 @@ from?: string; | ||
* | ||
* [Documentation](https://authjs.dev/guides/providers/email#normalizing-the-e-mail-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax) | ||
* [Normalizing the email address](https://authjs.dev/reference/core/providers_email#normalizing-the-email-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax) | ||
*/ | ||
@@ -281,3 +281,3 @@ normalizeIdentifier?: (identifier: string) => string; | ||
* | ||
* By default, NextAuth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior: | ||
* By default, Auth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior: | ||
* ```ts | ||
@@ -296,3 +296,3 @@ * EmailProvider({ | ||
* // You can also throw an error, which will redirect the user | ||
* // to the error page with error=EmailSignin in the URL | ||
* // to the sign-in page with error=EmailSignin in the URL | ||
* // if (identifier.split("@").length > 2) { | ||
@@ -299,0 +299,0 @@ * // throw new Error("Only one email allowed") |
@@ -208,3 +208,3 @@ import { createTransport } from "nodemailer"; | ||
* | ||
* By default, NextAuth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior: | ||
* By default, Auth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior: | ||
* ```ts | ||
@@ -223,3 +223,3 @@ * EmailProvider({ | ||
* // You can also throw an error, which will redirect the user | ||
* // to the error page with error=EmailSignin in the URL | ||
* // to the sign-in page with error=EmailSignin in the URL | ||
* // if (identifier.split("@").length > 2) { | ||
@@ -226,0 +226,0 @@ * // throw new Error("Only one email allowed") |
@@ -53,3 +53,3 @@ /** | ||
* :::tip | ||
* If using JWT for the session, you can add the `CharacterID` to the JWT token and session. Example: | ||
* If using JWT for the session, you can add the `CharacterID` to the JWT and session. Example: | ||
* ```js | ||
@@ -56,0 +56,0 @@ * options: { |
@@ -33,3 +33,3 @@ /** | ||
* :::tip | ||
* If using JWT for the session, you can add the `CharacterID` to the JWT token and session. Example: | ||
* If using JWT for the session, you can add the `CharacterID` to the JWT and session. Example: | ||
* ```js | ||
@@ -36,0 +36,0 @@ * options: { |
@@ -65,2 +65,3 @@ /** | ||
}; | ||
[claim: string]: unknown; | ||
} | ||
@@ -67,0 +68,0 @@ /** |
@@ -13,27 +13,2 @@ import type { Client } from "oauth4webapi"; | ||
type UrlParams = Record<string, unknown>; | ||
type EndpointRequest<C, R, P> = (context: C & { | ||
/** Provider is passed for convenience, and also contains the `callbackUrl`. */ | ||
provider: OAuthConfigInternal<P> & { | ||
signinUrl: string; | ||
callbackUrl: string; | ||
}; | ||
}) => Awaitable<R>; | ||
/** Gives granular control of the request to the given endpoint */ | ||
interface AdvancedEndpointHandler<P extends UrlParams, C, R> { | ||
/** Endpoint URL. Can contain parameters. Optionally, you can use `params` */ | ||
url?: string; | ||
/** These will be prepended to the `url` */ | ||
params?: P; | ||
/** | ||
* Control the corresponding OAuth endpoint request completely. | ||
* Useful if your provider relies on some custom behaviour | ||
* or it diverges from the OAuth spec. | ||
* | ||
* - ⚠ **This is an advanced option.** | ||
* You should **try to avoid using advanced options** unless you are very comfortable using them. | ||
*/ | ||
request?: EndpointRequest<C, R, P>; | ||
} | ||
/** Either an URL (containing all the parameters) or an object with more granular control. */ | ||
export type EndpointHandler<P extends UrlParams, C = any, R = any> = AdvancedEndpointHandler<P, C, R>; | ||
export type AuthorizationEndpointHandler = EndpointHandler<AuthorizationParameters>; | ||
@@ -58,2 +33,3 @@ export type TokenEndpointHandler = EndpointHandler<UrlParams, { | ||
export type ProfileCallback<Profile> = (profile: Profile, tokens: TokenSet) => Awaitable<User>; | ||
export type AccountCallback = (tokens: TokenSet) => TokenSet | undefined | void; | ||
export interface OAuthProviderButtonStyles { | ||
@@ -102,10 +78,46 @@ logo: string; | ||
/** | ||
* Receives the profile object returned by the OAuth provider, and returns the user object. | ||
* This will be used to create the user in the database. | ||
* Receives the full {@link Profile} returned by the OAuth provider, and returns a subset. | ||
* It is used to create the user in the database. | ||
* | ||
* Defaults to: `id`, `email`, `name`, `image` | ||
* | ||
* [Documentation](https://authjs.dev/reference/adapters/models#user) | ||
* @see [Database Adapter: User model](https://authjs.dev/reference/adapters#user) | ||
*/ | ||
profile?: ProfileCallback<Profile>; | ||
/** | ||
* Receives the full {@link TokenSet} returned by the OAuth provider, and returns a subset. | ||
* It is used to create the account associated with a user in the database. | ||
* | ||
* :::note | ||
* You need to adjust your database's [Account model](https://authjs.dev/reference/adapters#account) to match the returned properties. | ||
* Check out the documentation of your [database adapter](https://authjs.dev/reference/adapters) for more information. | ||
* ::: | ||
* | ||
* Defaults to: `access_token`, `id_token`, `refresh_token`, `expires_at`, `scope`, `token_type`, `session_state` | ||
* | ||
* @example | ||
* ```ts | ||
* import GitHub from "@auth/core/providers/github" | ||
* // ... | ||
* GitHub({ | ||
* account(account) { | ||
* // https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens#refreshing-a-user-access-token-with-a-refresh-token | ||
* const refresh_token_expires_at = | ||
* Math.floor(Date.now() / 1000) + Number(account.refresh_token_expires_in) | ||
* return { | ||
* access_token: account.access_token, | ||
* expires_at: account.expires_at, | ||
* refresh_token: account.refresh_token, | ||
* refresh_token_expires_at | ||
* } | ||
* } | ||
* }) | ||
* ``` | ||
* | ||
* @see [Database Adapter: Account model](https://authjs.dev/reference/adapters#account) | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse | ||
* @see https://www.ietf.org/rfc/rfc6749.html#section-5.1 | ||
*/ | ||
account?: AccountCallback; | ||
/** | ||
* The CSRF protection performed on the callback endpoint. | ||
@@ -146,3 +158,7 @@ * @default ["pkce"] | ||
} | ||
/** TODO: Document */ | ||
/** | ||
* Extension of the {@link OAuth2Config}. | ||
* | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html | ||
*/ | ||
export interface OIDCConfig<Profile> extends Omit<OAuth2Config<Profile>, "type" | "checks"> { | ||
@@ -154,36 +170,7 @@ type: "oidc"; | ||
export type OAuthEndpointType = "authorization" | "token" | "userinfo"; | ||
/** | ||
* We parsed `authorization`, `token` and `userinfo` | ||
* to always contain a valid `URL`, with the params | ||
*/ | ||
export type OAuthConfigInternal<Profile> = Omit<OAuthConfig<Profile>, OAuthEndpointType | "redirectProxyUrl"> & { | ||
authorization?: { | ||
url: URL; | ||
}; | ||
token?: { | ||
url: URL; | ||
request?: TokenEndpointHandler["request"]; | ||
}; | ||
userinfo?: { | ||
url: URL; | ||
request?: UserinfoEndpointHandler["request"]; | ||
}; | ||
/** | ||
* Reconstructed from {@link OAuth2Config.redirectProxyUrl}, | ||
* adding the callback action and provider id onto the URL. | ||
* | ||
* If defined, it is favoured over {@link OAuthConfigInternal.callbackUrl} in the authorization request. | ||
* | ||
* When {@link InternalOptions.isOnRedirectProxy} is set, the actual value is saved in the decoded `state.origin` parameter. | ||
* | ||
* @example `"https://auth.example.com/api/auth/callback/:provider"` | ||
* | ||
*/ | ||
redirectProxyUrl?: OAuth2Config<Profile>["redirectProxyUrl"]; | ||
} & Pick<Required<OAuthConfig<Profile>>, "clientId" | "checks" | "profile">; | ||
export type OIDCConfigInternal<Profile> = OAuthConfigInternal<Profile> & { | ||
checks: OIDCConfig<Profile>["checks"]; | ||
}; | ||
export type OAuthUserConfig<Profile> = Omit<Partial<OAuthConfig<Profile>>, "options" | "type"> & Required<Pick<OAuthConfig<Profile>, "clientId" | "clientSecret">>; | ||
export type OIDCUserConfig<Profile> = Omit<Partial<OIDCConfig<Profile>>, "options" | "type"> & Required<Pick<OIDCConfig<Profile>, "clientId" | "clientSecret">>; | ||
export type OAuthUserConfig<Profile> = Omit<Partial<OAuthConfig<Profile>>, "options" | "type">; | ||
export type OIDCUserConfig<Profile> = Omit<Partial<OIDCConfig<Profile>>, "options" | "type">; | ||
//# sourceMappingURL=oauth.d.ts.map |
@@ -101,2 +101,3 @@ /** | ||
}; | ||
[claims: string]: unknown; | ||
} | ||
@@ -103,0 +104,0 @@ /** |
@@ -231,2 +231,6 @@ /** | ||
): Promise<void> | Awaitable<AdapterUser | null | undefined> | ||
/** | ||
* This method is invoked internally (but optionally can be used for manual linking). | ||
* It creates an [Account](https://authjs.dev/reference/adapters#models) in the database. | ||
*/ | ||
linkAccount?( | ||
@@ -233,0 +237,0 @@ account: AdapterAccount |
@@ -25,9 +25,2 @@ interface ErrorCause extends Record<string, unknown> {} | ||
* @todo | ||
* Thrown when an Email address is already associated with an account | ||
* but the user is trying an OAuth account that is not linked to it. | ||
*/ | ||
export class AccountNotLinked extends AuthError {} | ||
/** | ||
* @todo | ||
* One of the database `Adapter` methods failed. | ||
@@ -41,4 +34,4 @@ */ | ||
/** | ||
* There was an error while trying to finish up authenticating the user. | ||
* Depending on the type of provider, this could be for multiple reasons. | ||
* This error occurs when the user cannot finish the sign-in process. | ||
* Depending on the provider type, this could have happened for multiple reasons. | ||
* | ||
@@ -53,3 +46,3 @@ * :::tip | ||
* | ||
* For an **OAuth provider**, possible causes are: | ||
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are: | ||
* - The user denied access to the application | ||
@@ -62,3 +55,3 @@ * - There was an error parsing the OAuth Profile: | ||
* | ||
* For an **Email provider**, possible causes are: | ||
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are: | ||
* - The provided email/token combination was invalid/missing: | ||
@@ -71,3 +64,3 @@ * Check if the provider's `sendVerificationRequest` method correctly sends the email. | ||
* | ||
* For a **Credentials provider**, possible causes are: | ||
* For a [Credentials provider](https://authjs.dev/reference/core/providers_credentials), possible causes are: | ||
* - The `authorize` method threw an uncaught error: | ||
@@ -115,9 +108,33 @@ * Check the provider's `authorize` method. | ||
/** @todo */ | ||
/** | ||
* Auth.js requires a secret to be set, but none was not found. This is used to encrypt cookies, JWTs and other sensitive data. | ||
* | ||
* :::note | ||
* If you are using a framework like Next.js, we try to automatically infer the secret from the `AUTH_SECRET` environment variable. | ||
* Alternatively, you can also explicitly set the [`AuthConfig.secret`](https://authjs.dev/reference/core#secret). | ||
* ::: | ||
* | ||
* | ||
* :::tip | ||
* You can generate a good secret value: | ||
* - On Unix systems: type `openssl rand -hex 32` in the terminal | ||
* - Or generate one [online](https://generate-secret.vercel.app/32) | ||
* | ||
* ::: | ||
*/ | ||
export class MissingSecret extends AuthError {} | ||
/** @todo */ | ||
export class OAuthSignInError extends AuthError {} | ||
/** | ||
* @todo | ||
* Thrown when an Email address is already associated with an account | ||
* but the user is trying an OAuth account that is not linked to it. | ||
*/ | ||
export class OAuthAccountNotLinked extends AuthError {} | ||
/** @todo */ | ||
/** | ||
* Thrown when an OAuth provider returns an error during the sign in process. | ||
* This could happen for example if the user denied access to the application or there was a configuration error. | ||
* | ||
* For a full list of possible reasons, check out the specification [Authorization Code Grant: Error Response](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2.1) | ||
*/ | ||
export class OAuthCallbackError extends AuthError {} | ||
@@ -128,3 +145,7 @@ | ||
/** @todo */ | ||
/** | ||
* This error occurs during an OAuth sign in attempt when the provdier's | ||
* response could not be parsed. This could for example happen if the provider's API | ||
* changed, or the [`OAuth2Config.profile`](https://authjs.dev/reference/core/providers_oauth#profile) method is not implemented correctly. | ||
*/ | ||
export class OAuthProfileParseError extends AuthError {} | ||
@@ -135,3 +156,27 @@ | ||
/** @todo */ | ||
/** | ||
* This error occurs when the user cannot initiate the sign-in process. | ||
* Depending on the provider type, this could have happened for multiple reasons. | ||
* | ||
* :::tip | ||
* Check out `[auth][details]` in the error message to know which provider failed. | ||
* @example | ||
* ```sh | ||
* [auth][details]: { "provider": "github" } | ||
* ``` | ||
* ::: | ||
* | ||
* For an [OAuth provider](https://authjs.dev/reference/core/providers_oauth), possible causes are: | ||
* - The Authorization Server is not compliant with the [OAuth 2.0 specifcation](https://www.ietf.org/rfc/rfc6749.html) | ||
* Check the details in the error message. | ||
* - A runtime error occurred in Auth.js. This should be reported as a bug. | ||
* | ||
* For an [Email provider](https://authjs.dev/reference/core/providers_email), possible causes are: | ||
* - The email sent from the client is invalid, could not be normalized by [`EmailConfig.normalizeIdentifier`](https://authjs.dev/reference/core/providers_email#normalizeidentifier) | ||
* - The provided email/token combination has expired: | ||
* Ask the user to log in again. | ||
* - There was an error with the database: | ||
* Check the database logs. | ||
* | ||
*/ | ||
export class SignInError extends AuthError {} | ||
@@ -142,3 +187,7 @@ | ||
/** @todo */ | ||
/** | ||
* Auth.js was requested to handle an operation that it does not support. | ||
* | ||
* See [`AuthAction`](https://authjs.dev/reference/core/types#authaction) for the supported actions. | ||
*/ | ||
export class UnknownAction extends AuthError {} | ||
@@ -145,0 +194,0 @@ |
@@ -193,3 +193,3 @@ /** | ||
* | ||
* @default 30 * 24 * 30 * 60 // 30 days | ||
* @default 30 * 24 * 60 * 60 // 30 days | ||
*/ | ||
@@ -217,3 +217,3 @@ maxAge?: number | ||
* | ||
* @default 30 * 24 * 30 * 60 // 30 days | ||
* @default 30 * 24 * 60 * 60 // 30 days | ||
*/ | ||
@@ -220,0 +220,0 @@ maxAge: number |
@@ -1,2 +0,2 @@ | ||
import { AccountNotLinked } from "../errors.js" | ||
import { OAuthAccountNotLinked } from "../errors.js" | ||
import { fromDate } from "./utils/date.js" | ||
@@ -52,3 +52,3 @@ | ||
const profile = _profile as AdapterUser | ||
const account = _account as AdapterAccount | ||
let account = _account as AdapterAccount | ||
@@ -126,111 +126,114 @@ const { | ||
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", | ||
{ provider: account.provider } | ||
) | ||
} | ||
// 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 | ||
// 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 OAuthAccountNotLinked( | ||
"The account is already associated with another user", | ||
{ provider: account.provider } | ||
) | ||
} | ||
// 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), | ||
}) | ||
// 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", | ||
{ provider: account.provider } | ||
) | ||
} | ||
return { session, user: userByAccount, isNewUser } | ||
} else { | ||
const { provider: p } = options as InternalOptions<"oauth" | "oidc"> | ||
const { type, provider, providerAccountId, userId, ...tokenSet } = account | ||
const defaults = { providerAccountId, provider, type, userId } | ||
account = Object.assign(p.account(tokenSet) ?? {}, defaults) | ||
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 { | ||
// If the current user is not logged in and the profile isn't linked to any user | ||
// accounts (by email or provider account id)... | ||
// 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. | ||
// | ||
// 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 account and | ||
// create a new session for them so they are signed in with it. | ||
const { id: _, ...newUser } = { ...profile, emailVerified: null } | ||
user = await createUser(newUser) | ||
// 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 OAuthAccountNotLinked( | ||
"Another account already exists with the same e-mail address", | ||
{ provider: account.provider } | ||
) | ||
} | ||
await events.createUser?.({ user }) | ||
} 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 account 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 }) | ||
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), | ||
}) | ||
session = useJwtSession | ||
? {} | ||
: await createSession({ | ||
sessionToken: generateSessionToken(), | ||
userId: user.id, | ||
expires: fromDate(options.session.maxAge), | ||
}) | ||
return { session, user, isNewUser: true } | ||
} | ||
return { session, user, isNewUser: true } | ||
} | ||
throw new Error("Unsupported account type") | ||
} |
@@ -50,3 +50,3 @@ import { UnknownAction } from "../errors.js" | ||
case "session": { | ||
const session = await routes.session(sessionStore, options) | ||
const session = await routes.session({ sessionStore, options }) | ||
if (session.cookies) cookies.push(...session.cookies) | ||
@@ -114,4 +114,2 @@ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion | ||
"Signin", | ||
"OAuthSignin", | ||
"OAuthCallback", | ||
"OAuthCreateAccount", | ||
@@ -121,4 +119,2 @@ "EmailCreateAccount", | ||
"OAuthAccountNotLinked", | ||
"EmailSignin", | ||
"CredentialsSignin", | ||
"SessionRequired", | ||
@@ -187,2 +183,18 @@ ].includes(error as string) | ||
break | ||
case "session": { | ||
if (options.csrfTokenVerified) { | ||
const session = await routes.session({ | ||
options, | ||
sessionStore, | ||
newSession: request.body?.data, | ||
isUpdate: true, | ||
}) | ||
if (session.cookies) cookies.push(...session.cookies) | ||
return { ...session, cookies } as any | ||
} | ||
// If CSRF token is invalid, return a 400 status code | ||
// we should not redirect to a page as this is an API route | ||
return { status: 400, cookies } | ||
} | ||
default: | ||
@@ -189,0 +201,0 @@ } |
@@ -6,2 +6,3 @@ import * as checks from "./checks.js" | ||
import type { | ||
Account, | ||
InternalOptions, | ||
@@ -92,7 +93,5 @@ LoggerInstance, | ||
if (o.isOAuth2Error(codeGrantParams)) { | ||
logger.debug("OAuthCallbackError", { | ||
providerId: provider.id, | ||
...codeGrantParams, | ||
}) | ||
throw new OAuthCallbackError(codeGrantParams.error) | ||
const cause = { providerId: provider.id, ...codeGrantParams } | ||
logger.debug("OAuthCallbackError", cause) | ||
throw new OAuthCallbackError("OAuth Provider returned an error", cause) | ||
} | ||
@@ -128,4 +127,4 @@ | ||
let profile: Profile = {} | ||
let tokens: TokenSet | ||
let profile: Profile | ||
let tokens: TokenSet & Pick<Account, "expires_at"> | ||
@@ -168,12 +167,24 @@ if (provider.type === "oidc") { | ||
profile = await userinfoResponse.json() | ||
} else { | ||
throw new TypeError("No userinfo endpoint configured") | ||
} | ||
} | ||
const profileResult = await getProfile(profile, provider, tokens, logger) | ||
if (tokens.expires_in) { | ||
tokens.expires_at = | ||
Math.floor(Date.now() / 1000) + Number(tokens.expires_in) | ||
} | ||
return { ...profileResult, cookies: resCookies } | ||
const profileResult = await getUserAndAccount( | ||
profile, | ||
provider, | ||
tokens, | ||
logger | ||
) | ||
return { ...profileResult, profile, cookies: resCookies } | ||
} | ||
/** Returns profile, raw profile and auth provider details */ | ||
async function getProfile( | ||
/** Returns the user and account that is going to be created in the database. */ | ||
async function getUserAndAccount( | ||
OAuthProfile: Profile, | ||
@@ -185,8 +196,8 @@ provider: OAuthConfigInternal<any>, | ||
try { | ||
const profile = await provider.profile(OAuthProfile, tokens) | ||
profile.email = profile.email?.toLowerCase() | ||
const user = await provider.profile(OAuthProfile, tokens) | ||
user.email = user.email?.toLowerCase() | ||
if (!profile.id) { | ||
if (!user.id) { | ||
throw new TypeError( | ||
`Profile id is missing in ${provider.name} OAuth profile response` | ||
`User id is missing in ${provider.name} OAuth profile response` | ||
) | ||
@@ -196,10 +207,9 @@ } | ||
return { | ||
profile, | ||
user, | ||
account: { | ||
provider: provider.id, | ||
type: provider.type, | ||
providerAccountId: profile.id.toString(), | ||
providerAccountId: user.id.toString(), | ||
...tokens, | ||
}, | ||
OAuthProfile, | ||
} | ||
@@ -215,4 +225,6 @@ } catch (e) { | ||
logger.debug("getProfile error details", OAuthProfile) | ||
logger.error(new OAuthProfileParseError(e as Error)) | ||
logger.error( | ||
new OAuthProfileParseError(e as Error, { provider: provider.id }) | ||
) | ||
} | ||
} |
@@ -0,4 +1,6 @@ | ||
import { OAuthProfileParseError } from "../errors.js" | ||
import { merge } from "./utils/merge.js" | ||
import type { | ||
AccountCallback, | ||
OAuthConfig, | ||
@@ -8,5 +10,6 @@ OAuthConfigInternal, | ||
OAuthUserConfig, | ||
ProfileCallback, | ||
Provider, | ||
} from "../providers/index.js" | ||
import type { AuthConfig, InternalProvider } from "../types.js" | ||
import type { AuthConfig, InternalProvider, Profile } from "../types.js" | ||
@@ -81,14 +84,47 @@ /** | ||
profile: c.profile ?? defaultProfile, | ||
account: c.account ?? defaultAccount, | ||
} | ||
} | ||
function defaultProfile(profile: any) { | ||
return { | ||
id: profile.sub ?? profile.id, | ||
name: | ||
profile.name ?? profile.nickname ?? profile.preferred_username ?? null, | ||
email: profile.email ?? null, | ||
image: profile.picture ?? null, | ||
} | ||
/** | ||
* Returns basic user profile from the userinfo response/`id_token` claims. | ||
* @see https://authjs.dev/reference/adapters#user | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#IDToken | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo | ||
*/ | ||
const defaultProfile: ProfileCallback<Profile> = (profile) => { | ||
const id = profile.sub ?? profile.id | ||
if (!id) throw new OAuthProfileParseError("Missing user id") | ||
return stripUndefined({ | ||
id: id.toString(), | ||
name: profile.name ?? profile.nickname ?? profile.preferred_username, | ||
email: profile.email, | ||
image: profile.picture, | ||
}) | ||
} | ||
/** | ||
* Returns basic OAuth/OIDC values from the token response. | ||
* @see https://www.ietf.org/rfc/rfc6749.html#section-5.1 | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse | ||
* @see https://authjs.dev/reference/adapters#account | ||
*/ | ||
const defaultAccount: AccountCallback = (account) => { | ||
return stripUndefined({ | ||
access_token: account.access_token, | ||
id_token: account.id_token, | ||
refresh_token: account.refresh_token, | ||
expires_at: account.expires_at, | ||
scope: account.scope, | ||
token_type: account.token_type, | ||
session_state: account.session_state, | ||
}) | ||
} | ||
function stripUndefined<T extends object>(o: T): T { | ||
const result = {} as any | ||
for (let [k, v] of Object.entries(o)) v !== undefined && (result[k] = v) | ||
return result as T | ||
} | ||
function normalizeEndpoint( | ||
@@ -95,0 +131,0 @@ e?: OAuthConfig<any>[OAuthEndpointType], |
@@ -1,2 +0,6 @@ | ||
import { CallbackRouteError, Verification } from "../../errors.js" | ||
import { | ||
CallbackRouteError, | ||
OAuthCallbackError, | ||
Verification, | ||
} from "../../errors.js" | ||
import { handleLogin } from "../callback-handler.js" | ||
@@ -71,3 +75,7 @@ import { handleOAuth } from "../oauth/callback.js" | ||
const { profile, account, OAuthProfile } = authorizationResult | ||
const { | ||
user: userFromProvider, | ||
account, | ||
profile: OAuthProfile, | ||
} = authorizationResult | ||
@@ -79,3 +87,3 @@ // If we don't have a profile object then either something went wrong | ||
// TODO: Handle user cancelling signin | ||
if (!profile || !account || !OAuthProfile) { | ||
if (!userFromProvider || !account || !OAuthProfile) { | ||
return { redirect: `${url}/signin`, cookies } | ||
@@ -88,3 +96,3 @@ } | ||
// (that just means it's a new user signing in for the first time). | ||
let userOrProfile = profile | ||
let userByAccountOrFromProvider | ||
if (adapter) { | ||
@@ -97,7 +105,11 @@ const { getUserByAccount } = adapter | ||
if (userByAccount) userOrProfile = userByAccount | ||
if (userByAccount) userByAccountOrFromProvider = userByAccount | ||
} | ||
const unauthorizedOrError = await handleAuthorized( | ||
{ user: userOrProfile, account, profile: OAuthProfile }, | ||
{ | ||
user: userByAccountOrFromProvider, | ||
account, | ||
profile: OAuthProfile, | ||
}, | ||
options | ||
@@ -111,3 +123,3 @@ ) | ||
sessionStore.value, | ||
profile, | ||
userFromProvider, | ||
account, | ||
@@ -130,2 +142,3 @@ options | ||
isNewUser, | ||
trigger: isNewUser ? "signUp" : "signIn", | ||
}) | ||
@@ -161,3 +174,3 @@ | ||
await events.signIn?.({ user, account, profile, isNewUser }) | ||
await events.signIn?.({ user, account, profile: OAuthProfile, isNewUser }) | ||
@@ -242,2 +255,3 @@ // Handle first logins on new accounts | ||
isNewUser, | ||
trigger: isNewUser ? "signUp" : "signIn", | ||
}) | ||
@@ -339,2 +353,3 @@ | ||
isNewUser: false, | ||
trigger: "signIn", | ||
}) | ||
@@ -372,4 +387,14 @@ | ||
} catch (e) { | ||
if (e instanceof OAuthCallbackError) { | ||
logger.error(e) | ||
// REVIEW: Should we expose original error= and error_description= | ||
// Should we use a different name for error= then, since we already use it for all kind of errors? | ||
url.searchParams.set("error", OAuthCallbackError.name) | ||
url.pathname += "/signin" | ||
return { redirect: url.toString(), cookies } | ||
} | ||
const error = new CallbackRouteError(e as Error, { provider: provider.id }) | ||
logger.debug("callback route error details", { method, query, body }) | ||
logger.error(error) | ||
@@ -376,0 +401,0 @@ url.searchParams.set("error", CallbackRouteError.name) |
@@ -9,6 +9,9 @@ import { JWTSessionError, SessionTokenError } from "../../errors.js" | ||
/** Return a session object filtered via `callbacks.session` */ | ||
export async function session( | ||
sessionStore: SessionStore, | ||
export async function session(params: { | ||
options: InternalOptions | ||
): Promise<ResponseInternal<Session | null>> { | ||
sessionStore: SessionStore | ||
isUpdate?: boolean | ||
newSession?: any | ||
}): Promise<ResponseInternal<Session | null>> { | ||
const { options, sessionStore, newSession, isUpdate } = params | ||
const { | ||
@@ -37,19 +40,20 @@ adapter, | ||
const newExpires = fromDate(sessionMaxAge) | ||
if (!decodedToken) throw new Error("Invalid JWT") | ||
// By default, only exposes a limited subset of information to the client | ||
// as needed for presentation purposes (e.g. "you are logged in as..."). | ||
const session = { | ||
user: { | ||
name: decodedToken?.name, | ||
email: decodedToken?.email, | ||
image: decodedToken?.picture, | ||
}, | ||
expires: newExpires.toISOString(), | ||
} | ||
// @ts-expect-error | ||
const token = await callbacks.jwt({ token: decodedToken }) | ||
const token = await callbacks.jwt({ | ||
token: decodedToken, | ||
...(isUpdate && { trigger: "update" }), | ||
session: newSession, | ||
}) | ||
const newExpires = fromDate(sessionMaxAge) | ||
if (token !== null) { | ||
// By default, only exposes a limited subset of information to the client | ||
// as needed for presentation purposes (e.g. "you are logged in as..."). | ||
const session = { | ||
user: { name: token.name, email: token.email, image: token.picture }, | ||
expires: newExpires.toISOString(), | ||
} | ||
// @ts-expect-error | ||
@@ -130,10 +134,8 @@ const newSession = await callbacks.session({ session, token }) | ||
session: { | ||
user: { | ||
name: user.name, | ||
email: user.email, | ||
image: user.image, | ||
}, | ||
user: { name: user.name, email: user.email, image: user.image }, | ||
expires: session.expires.toISOString(), | ||
}, | ||
user, | ||
newSession, | ||
...(isUpdate ? { trigger: "update" } : {}), | ||
}) | ||
@@ -140,0 +142,0 @@ |
@@ -58,4 +58,5 @@ import emailSignin from "../email/signin.js" | ||
logger.error(error) | ||
url.searchParams.set("error", error.name) | ||
url.pathname += "/error" | ||
const code = provider.type === "email" ? "EmailSignin" : "OAuthSignin" | ||
url.searchParams.set("error", code) | ||
url.pathname += "/signin" | ||
return { redirect: url.toString() } | ||
@@ -62,0 +63,0 @@ } |
@@ -36,2 +36,4 @@ import { parse as parseCookie, serialize } from "cookie" | ||
const url = new URL(req.url.replace(/\/$/, "")) | ||
// FIXME: Upstream issue in Next.js, pathname segments get included as part of the query string | ||
url.searchParams.delete("nextauth") | ||
const { pathname } = url | ||
@@ -38,0 +40,0 @@ |
@@ -35,3 +35,4 @@ import type { CommonProviderOptions } from "./index.js" | ||
* //... | ||
* async authorize(, request) { | ||
* async authorize(credentials, request) { | ||
* if(!isValidCredentials(credentials)) return null | ||
* const response = await fetch(request) | ||
@@ -42,2 +43,3 @@ * if(!response.ok) return null | ||
* //... | ||
* ``` | ||
*/ | ||
@@ -44,0 +46,0 @@ authorize: ( |
@@ -32,3 +32,3 @@ import { createTransport } from "nodemailer" | ||
// TODO: Make use of https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html | ||
server: string | SMTPTransportOptions | ||
server?: string | SMTPTransportOptions | ||
/** @default `"Auth.js <no-reply@authjs.dev>"` */ | ||
@@ -76,3 +76,3 @@ from?: string | ||
* | ||
* [Documentation](https://authjs.dev/guides/providers/email#normalizing-the-e-mail-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax) | ||
* [Normalizing the email address](https://authjs.dev/reference/core/providers_email#normalizing-the-email-address) | [RFC 2821](https://tools.ietf.org/html/rfc2821) | [Email syntax](https://en.wikipedia.org/wiki/Email_address#Syntax) | ||
*/ | ||
@@ -292,3 +292,3 @@ normalizeIdentifier?: (identifier: string) => string | ||
* | ||
* By default, NextAuth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior: | ||
* By default, Auth.js will normalize the email address. It treats values as case-insensitive (which is technically not compliant to the [RFC 2821 spec](https://datatracker.ietf.org/doc/html/rfc2821), but in practice this causes more problems than it solves, eg. when looking up users by e-mail from databases.) and also removes any secondary email address that was passed in as a comma-separated list. You can apply your own normalization via the `normalizeIdentifier` method on the `EmailProvider`. The following example shows the default behavior: | ||
* ```ts | ||
@@ -307,3 +307,3 @@ * EmailProvider({ | ||
* // You can also throw an error, which will redirect the user | ||
* // to the error page with error=EmailSignin in the URL | ||
* // to the sign-in page with error=EmailSignin in the URL | ||
* // if (identifier.split("@").length > 2) { | ||
@@ -310,0 +310,0 @@ * // throw new Error("Only one email allowed") |
@@ -55,3 +55,3 @@ /** | ||
* :::tip | ||
* If using JWT for the session, you can add the `CharacterID` to the JWT token and session. Example: | ||
* If using JWT for the session, you can add the `CharacterID` to the JWT and session. Example: | ||
* ```js | ||
@@ -58,0 +58,0 @@ * options: { |
@@ -68,2 +68,3 @@ /** | ||
} | ||
[claim: string]: unknown | ||
} | ||
@@ -70,0 +71,0 @@ |
@@ -55,3 +55,6 @@ import type { Client } from "oauth4webapi" | ||
/** Either an URL (containing all the parameters) or an object with more granular control. */ | ||
/** | ||
* Either an URL (containing all the parameters) or an object with more granular control. | ||
* @internal | ||
*/ | ||
export type EndpointHandler< | ||
@@ -96,2 +99,4 @@ P extends UrlParams, | ||
export type AccountCallback = (tokens: TokenSet) => TokenSet | undefined | void | ||
export interface OAuthProviderButtonStyles { | ||
@@ -143,10 +148,46 @@ logo: string | ||
/** | ||
* Receives the profile object returned by the OAuth provider, and returns the user object. | ||
* This will be used to create the user in the database. | ||
* Receives the full {@link Profile} returned by the OAuth provider, and returns a subset. | ||
* It is used to create the user in the database. | ||
* | ||
* Defaults to: `id`, `email`, `name`, `image` | ||
* | ||
* [Documentation](https://authjs.dev/reference/adapters/models#user) | ||
* @see [Database Adapter: User model](https://authjs.dev/reference/adapters#user) | ||
*/ | ||
profile?: ProfileCallback<Profile> | ||
/** | ||
* Receives the full {@link TokenSet} returned by the OAuth provider, and returns a subset. | ||
* It is used to create the account associated with a user in the database. | ||
* | ||
* :::note | ||
* You need to adjust your database's [Account model](https://authjs.dev/reference/adapters#account) to match the returned properties. | ||
* Check out the documentation of your [database adapter](https://authjs.dev/reference/adapters) for more information. | ||
* ::: | ||
* | ||
* Defaults to: `access_token`, `id_token`, `refresh_token`, `expires_at`, `scope`, `token_type`, `session_state` | ||
* | ||
* @example | ||
* ```ts | ||
* import GitHub from "@auth/core/providers/github" | ||
* // ... | ||
* GitHub({ | ||
* account(account) { | ||
* // https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/refreshing-user-access-tokens#refreshing-a-user-access-token-with-a-refresh-token | ||
* const refresh_token_expires_at = | ||
* Math.floor(Date.now() / 1000) + Number(account.refresh_token_expires_in) | ||
* return { | ||
* access_token: account.access_token, | ||
* expires_at: account.expires_at, | ||
* refresh_token: account.refresh_token, | ||
* refresh_token_expires_at | ||
* } | ||
* } | ||
* }) | ||
* ``` | ||
* | ||
* @see [Database Adapter: Account model](https://authjs.dev/reference/adapters#account) | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse | ||
* @see https://www.ietf.org/rfc/rfc6749.html#section-5.1 | ||
*/ | ||
account?: AccountCallback | ||
/** | ||
* The CSRF protection performed on the callback endpoint. | ||
@@ -196,3 +237,7 @@ * @default ["pkce"] | ||
/** TODO: Document */ | ||
/** | ||
* Extension of the {@link OAuth2Config}. | ||
* | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html | ||
*/ | ||
export interface OIDCConfig<Profile> | ||
@@ -211,2 +256,3 @@ extends Omit<OAuth2Config<Profile>, "type" | "checks"> { | ||
* to always contain a valid `URL`, with the params | ||
* @internal | ||
*/ | ||
@@ -237,3 +283,6 @@ export type OAuthConfigInternal<Profile> = Omit< | ||
redirectProxyUrl?: OAuth2Config<Profile>["redirectProxyUrl"] | ||
} & Pick<Required<OAuthConfig<Profile>>, "clientId" | "checks" | "profile"> | ||
} & Pick< | ||
Required<OAuthConfig<Profile>>, | ||
"clientId" | "checks" | "profile" | "account" | ||
> | ||
@@ -247,4 +296,3 @@ export type OIDCConfigInternal<Profile> = OAuthConfigInternal<Profile> & { | ||
"options" | "type" | ||
> & | ||
Required<Pick<OAuthConfig<Profile>, "clientId" | "clientSecret">> | ||
> | ||
@@ -254,3 +302,2 @@ export type OIDCUserConfig<Profile> = Omit< | ||
"options" | "type" | ||
> & | ||
Required<Pick<OIDCConfig<Profile>, "clientId" | "clientSecret">> | ||
> |
@@ -102,2 +102,3 @@ /** | ||
} | ||
[claims: string]: unknown | ||
} | ||
@@ -104,0 +105,0 @@ |
185
src/types.ts
@@ -101,3 +101,11 @@ /** | ||
OAuth2TokenEndpointResponse | OpenIDTokenEndpointResponse | ||
> | ||
> & { | ||
/** | ||
* Date of when the `access_token` expires in seconds. | ||
* This value is calculated from the `expires_in` value. | ||
* | ||
* @see https://www.ietf.org/rfc/rfc6749.html#section-4.2.2 | ||
*/ | ||
expires_at?: number | ||
} | ||
@@ -120,12 +128,54 @@ /** | ||
type: ProviderType | ||
/** id of the user this account belongs to */ | ||
/** | ||
* id of the user this account belongs to | ||
* | ||
* @see https://authjs.dev/reference/adapters#user | ||
*/ | ||
userId?: string | ||
/** | ||
* Calculated value based on {@link OAuth2TokenEndpointResponse.expires_in}. | ||
* | ||
* It is the absolute timestamp (in seconds) when the {@link OAuth2TokenEndpointResponse.access_token} expires. | ||
* | ||
* This value can be used for implementing token rotation together with {@link OAuth2TokenEndpointResponse.refresh_token}. | ||
* | ||
* @see https://authjs.dev/guides/basics/refresh-token-rotation#database-strategy | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1 | ||
*/ | ||
expires_at?: number | ||
} | ||
/** The OAuth profile returned from your provider */ | ||
/** | ||
* The user info returned from your OAuth provider. | ||
* | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims | ||
*/ | ||
export interface Profile { | ||
sub?: string | null | ||
name?: string | null | ||
given_name?: string | null | ||
family_name?: string | null | ||
middle_name?: string | null | ||
nickname?: string | null | ||
preferred_username?: string | null | ||
profile?: string | null | ||
picture?: string | null | any | ||
website?: string | null | ||
email?: string | null | ||
image?: string | null | ||
email_verified?: boolean | null | ||
gender?: string | null | ||
birthdate?: string | null | ||
zoneinfo?: string | null | ||
locale?: string | null | ||
phone_number?: string | null | ||
updated_at?: Date | string | number | null | ||
address?: { | ||
formatted?: string | null | ||
street_address?: string | null | ||
locality?: string | null | ||
region?: string | null | ||
postal_code?: string | null | ||
country?: string | null | ||
} | null | ||
[claim: string]: unknown | ||
} | ||
@@ -193,13 +243,24 @@ | ||
* | ||
* [Documentation](https://authjs.dev/guides/basics/callbacks#session-callback) | | ||
* [`jwt` callback](https://authjs.dev/guides/basics/callbacks#jwt-callback) | | ||
* [`useSession`](https://authjs.dev/reference/react/#usesession) | | ||
* [`getSession`](https://authjs.dev/reference/utilities/#getsession) | | ||
* | ||
* @see [`jwt` callback](https://authjs.dev/reference/core/types#jwt) | ||
*/ | ||
session: (params: { | ||
session: Session | ||
user: User | AdapterUser | ||
token: JWT | ||
}) => Awaitable<Session> | ||
session: ( | ||
params: | ||
| { | ||
session: Session | ||
/** Available when {@link AuthConfig.session} is set to `strategy: "jwt"` */ | ||
token: JWT | ||
/** Available when {@link AuthConfig.session} is set to `strategy: "database"`. */ | ||
user: AdapterUser | ||
} & { | ||
/** | ||
* Available when using {@link AuthConfig.session} `strategy: "database"` and an update is triggered for the session. | ||
* | ||
* :::note | ||
* You should validate this data before using it. | ||
* ::: | ||
*/ | ||
newSession: any | ||
trigger: "update" | ||
} | ||
) => Awaitable<Session | DefaultSession> | ||
/** | ||
@@ -210,20 +271,55 @@ * This callback is called whenever a JSON Web Token is created (i.e. at sign in) | ||
* where you can control what should be returned to the client. | ||
* Anything else will be kept inaccessible from the client. | ||
* Anything else will be kept from your front-end. | ||
* | ||
* Returning `null` will invalidate the JWT session by clearing | ||
* the user's cookies. You'll still have to monitor and invalidate | ||
* unexpired tokens from future requests yourself to prevent | ||
* unauthorized access. | ||
* The JWT is encrypted by default. | ||
* | ||
* By default the JWT is encrypted. | ||
* | ||
* [Documentation](https://authjs.dev/guides/basics/callbacks#jwt-callback) | | ||
* [`session` callback](https://authjs.dev/guides/basics/callbacks#session-callback) | ||
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) | | ||
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback) | ||
*/ | ||
jwt: (params: { | ||
/** | ||
* When `trigger` is `"signIn"` or `"signUp"`, it will be a subset of {@link JWT}, | ||
* `name`, `email` and `image` will be included. | ||
* | ||
* Otherwise, it will be the full {@link JWT} for subsequent calls. | ||
*/ | ||
token: JWT | ||
user?: User | AdapterUser | ||
account?: A | null | ||
/** | ||
* Either the result of the {@link OAuthConfig.profile} or the {@link CredentialsConfig.authorize} callback. | ||
* @note available when `trigger` is `"signIn"` or `"signUp"`. | ||
* | ||
* Resources: | ||
* - [Credentials Provider](https://authjs.dev/reference/core/providers_credentials) | ||
* - [User database model](https://authjs.dev/reference/adapters#user) | ||
*/ | ||
user: User | AdapterUser | ||
/** | ||
* Contains information about the provider that was used to sign in. | ||
* Also includes {@link TokenSet} | ||
* @note available when `trigger` is `"signIn"` or `"signUp"` | ||
*/ | ||
account: A | null | ||
/** | ||
* The OAuth profile returned from your provider. | ||
* (In case of OIDC it will be the decoded ID Token or /userinfo response) | ||
* @note available when `trigger` is `"signIn"`. | ||
*/ | ||
profile?: P | ||
/** | ||
* Check why was the jwt callback invoked. Possible reasons are: | ||
* - user sign-in: First time the callback is invoked, `user`, `profile` and `account` will be present. | ||
* - user sign-up: a user is created for the first time in the database (when {@link AuthConfig.session}.strategy is set to `"database"`) | ||
* - update event: Triggered by the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method. | ||
* In case of the latter, `trigger` will be `undefined`. | ||
*/ | ||
trigger?: "signIn" | "signUp" | "update" | ||
/** @deprecated use `trigger === "signUp"` instead */ | ||
isNewUser?: boolean | ||
/** | ||
* When using {@link AuthConfig.session} `strategy: "jwt"`, this is the data | ||
* sent from the client via the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method. | ||
* | ||
* ⚠ Note, you should validate this data before using it. | ||
*/ | ||
session?: any | ||
}) => Awaitable<JWT | null> | ||
@@ -269,3 +365,3 @@ } | ||
* if you use JWT or database persisted sessions: | ||
* - `token`: The JWT token for this session. | ||
* - `token`: The JWT for this session. | ||
* - `session`: The session object from your adapter that is being ended. | ||
@@ -288,3 +384,3 @@ */ | ||
* if you use JWT or database persisted sessions: | ||
* - `token`: The JWT token for this session. | ||
* - `token`: The JWT for this session. | ||
* - `session`: The session object from your adapter. | ||
@@ -304,3 +400,3 @@ */ | ||
| "OAuthSignin" | ||
| "OAuthCallback" | ||
| "OAuthCallbackError" | ||
| "OAuthCreateAccount" | ||
@@ -395,11 +491,38 @@ | "EmailCreateAccount" | ||
/** | ||
* Supported actions by Auth.js. Each action map to a REST API endpoint. | ||
* Some actions have a `GET` and `POST` variant, depending on if the action | ||
* changes the state of the server. | ||
* | ||
* - **`"callback"`**: | ||
* - **`GET`**: Handles the callback from an [OAuth provider](https://authjs.dev/reference/core/providers_oauth). | ||
* - **`POST`**: Handles the callback from a [Credentials provider](https://authjs.dev/reference/core/providers_credentials). | ||
* - **`"csrf"`**: Returns the raw CSRF token, which is saved in a cookie (encrypted). | ||
* It is used for CSRF protection, implementing the [double submit cookie](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie) technique. | ||
* :::note | ||
* Some frameworks have built-in CSRF protection and can therefore disable this action. In this case, the corresponding endpoint will return a 404 response. Read more at [`skipCSRFCheck`](https://authjs.dev/reference/core#skipcsrfcheck). | ||
* _⚠ We don't recommend manually disabling CSRF protection, unless you know what you're doing._ | ||
* ::: | ||
* - **`"error"`**: Renders the built-in error page. | ||
* - **`"providers"`**: Returns a client-safe list of all configured providers. | ||
* - **`"session"`**: | ||
* - **`GET**`: Returns the user's session if it exists, otherwise `null`. | ||
* - **`POST**`: Updates the user's session and returns the updated session. | ||
* - **`"signin"`**: | ||
* - **`GET`**: Renders the built-in sign-in page. | ||
* - **`POST`**: Initiates the sign-in flow. | ||
* - **`"signout"`**: | ||
* - **`GET`**: Renders the built-in sign-out page. | ||
* - **`POST`**: Initiates the sign-out flow. This will invalidate the user's session (deleting the cookie, and if there is a session in the database, it will be deleted as well). | ||
* - **`"verify-request"`**: Renders the built-in verification request page. | ||
*/ | ||
export type AuthAction = | ||
| "callback" | ||
| "csrf" | ||
| "error" | ||
| "providers" | ||
| "session" | ||
| "csrf" | ||
| "signin" | ||
| "signout" | ||
| "callback" | ||
| "verify-request" | ||
| "error" | ||
@@ -406,0 +529,0 @@ /** @internal */ |
172
types.d.ts
@@ -83,3 +83,11 @@ /** | ||
*/ | ||
export type TokenSet = Partial<OAuth2TokenEndpointResponse | OpenIDTokenEndpointResponse>; | ||
export type TokenSet = Partial<OAuth2TokenEndpointResponse | OpenIDTokenEndpointResponse> & { | ||
/** | ||
* Date of when the `access_token` expires in seconds. | ||
* This value is calculated from the `expires_in` value. | ||
* | ||
* @see https://www.ietf.org/rfc/rfc6749.html#section-4.2.2 | ||
*/ | ||
expires_at?: number; | ||
}; | ||
/** | ||
@@ -101,11 +109,53 @@ * Usually contains information about the provider being used | ||
type: ProviderType; | ||
/** id of the user this account belongs to */ | ||
/** | ||
* id of the user this account belongs to | ||
* | ||
* @see https://authjs.dev/reference/adapters#user | ||
*/ | ||
userId?: string; | ||
/** | ||
* Calculated value based on {@link OAuth2TokenEndpointResponse.expires_in}. | ||
* | ||
* It is the absolute timestamp (in seconds) when the {@link OAuth2TokenEndpointResponse.access_token} expires. | ||
* | ||
* This value can be used for implementing token rotation together with {@link OAuth2TokenEndpointResponse.refresh_token}. | ||
* | ||
* @see https://authjs.dev/guides/basics/refresh-token-rotation#database-strategy | ||
* @see https://www.rfc-editor.org/rfc/rfc6749#section-5.1 | ||
*/ | ||
expires_at?: number; | ||
} | ||
/** The OAuth profile returned from your provider */ | ||
/** | ||
* The user info returned from your OAuth provider. | ||
* | ||
* @see https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims | ||
*/ | ||
export interface Profile { | ||
sub?: string | null; | ||
name?: string | null; | ||
given_name?: string | null; | ||
family_name?: string | null; | ||
middle_name?: string | null; | ||
nickname?: string | null; | ||
preferred_username?: string | null; | ||
profile?: string | null; | ||
picture?: string | null | any; | ||
website?: string | null; | ||
email?: string | null; | ||
image?: string | null; | ||
email_verified?: boolean | null; | ||
gender?: string | null; | ||
birthdate?: string | null; | ||
zoneinfo?: string | null; | ||
locale?: string | null; | ||
phone_number?: string | null; | ||
updated_at?: Date | string | number | null; | ||
address?: { | ||
formatted?: string | null; | ||
street_address?: string | null; | ||
locality?: string | null; | ||
region?: string | null; | ||
postal_code?: string | null; | ||
country?: string | null; | ||
} | null; | ||
[claim: string]: unknown; | ||
} | ||
@@ -172,13 +222,21 @@ /** [Documentation](https://authjs.dev/guides/basics/callbacks) */ | ||
* | ||
* [Documentation](https://authjs.dev/guides/basics/callbacks#session-callback) | | ||
* [`jwt` callback](https://authjs.dev/guides/basics/callbacks#jwt-callback) | | ||
* [`useSession`](https://authjs.dev/reference/react/#usesession) | | ||
* [`getSession`](https://authjs.dev/reference/utilities/#getsession) | | ||
* | ||
* @see [`jwt` callback](https://authjs.dev/reference/core/types#jwt) | ||
*/ | ||
session: (params: { | ||
session: Session; | ||
user: User | AdapterUser; | ||
/** Available when {@link AuthConfig.session} is set to `strategy: "jwt"` */ | ||
token: JWT; | ||
}) => Awaitable<Session>; | ||
/** Available when {@link AuthConfig.session} is set to `strategy: "database"`. */ | ||
user: AdapterUser; | ||
} & { | ||
/** | ||
* Available when using {@link AuthConfig.session} `strategy: "database"` and an update is triggered for the session. | ||
* | ||
* :::note | ||
* You should validate this data before using it. | ||
* ::: | ||
*/ | ||
newSession: any; | ||
trigger: "update"; | ||
}) => Awaitable<Session | DefaultSession>; | ||
/** | ||
@@ -189,20 +247,55 @@ * This callback is called whenever a JSON Web Token is created (i.e. at sign in) | ||
* where you can control what should be returned to the client. | ||
* Anything else will be kept inaccessible from the client. | ||
* Anything else will be kept from your front-end. | ||
* | ||
* Returning `null` will invalidate the JWT session by clearing | ||
* the user's cookies. You'll still have to monitor and invalidate | ||
* unexpired tokens from future requests yourself to prevent | ||
* unauthorized access. | ||
* The JWT is encrypted by default. | ||
* | ||
* By default the JWT is encrypted. | ||
* | ||
* [Documentation](https://authjs.dev/guides/basics/callbacks#jwt-callback) | | ||
* [`session` callback](https://authjs.dev/guides/basics/callbacks#session-callback) | ||
* [Documentation](https://next-auth.js.org/configuration/callbacks#jwt-callback) | | ||
* [`session` callback](https://next-auth.js.org/configuration/callbacks#session-callback) | ||
*/ | ||
jwt: (params: { | ||
/** | ||
* When `trigger` is `"signIn"` or `"signUp"`, it will be a subset of {@link JWT}, | ||
* `name`, `email` and `image` will be included. | ||
* | ||
* Otherwise, it will be the full {@link JWT} for subsequent calls. | ||
*/ | ||
token: JWT; | ||
user?: User | AdapterUser; | ||
account?: A | null; | ||
/** | ||
* Either the result of the {@link OAuthConfig.profile} or the {@link CredentialsConfig.authorize} callback. | ||
* @note available when `trigger` is `"signIn"` or `"signUp"`. | ||
* | ||
* Resources: | ||
* - [Credentials Provider](https://authjs.dev/reference/core/providers_credentials) | ||
* - [User database model](https://authjs.dev/reference/adapters#user) | ||
*/ | ||
user: User | AdapterUser; | ||
/** | ||
* Contains information about the provider that was used to sign in. | ||
* Also includes {@link TokenSet} | ||
* @note available when `trigger` is `"signIn"` or `"signUp"` | ||
*/ | ||
account: A | null; | ||
/** | ||
* The OAuth profile returned from your provider. | ||
* (In case of OIDC it will be the decoded ID Token or /userinfo response) | ||
* @note available when `trigger` is `"signIn"`. | ||
*/ | ||
profile?: P; | ||
/** | ||
* Check why was the jwt callback invoked. Possible reasons are: | ||
* - user sign-in: First time the callback is invoked, `user`, `profile` and `account` will be present. | ||
* - user sign-up: a user is created for the first time in the database (when {@link AuthConfig.session}.strategy is set to `"database"`) | ||
* - update event: Triggered by the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method. | ||
* In case of the latter, `trigger` will be `undefined`. | ||
*/ | ||
trigger?: "signIn" | "signUp" | "update"; | ||
/** @deprecated use `trigger === "signUp"` instead */ | ||
isNewUser?: boolean; | ||
/** | ||
* When using {@link AuthConfig.session} `strategy: "jwt"`, this is the data | ||
* sent from the client via the [`useSession().update`](https://next-auth.js.org/getting-started/client#update-session) method. | ||
* | ||
* ⚠ Note, you should validate this data before using it. | ||
*/ | ||
session?: any; | ||
}) => Awaitable<JWT | null>; | ||
@@ -245,3 +338,3 @@ } | ||
* if you use JWT or database persisted sessions: | ||
* - `token`: The JWT token for this session. | ||
* - `token`: The JWT for this session. | ||
* - `session`: The session object from your adapter that is being ended. | ||
@@ -268,3 +361,3 @@ */ | ||
* if you use JWT or database persisted sessions: | ||
* - `token`: The JWT token for this session. | ||
* - `token`: The JWT for this session. | ||
* - `session`: The session object from your adapter. | ||
@@ -281,3 +374,3 @@ */ | ||
/** TODO: Check if all these are used/correct */ | ||
export type SignInPageErrorParam = "Signin" | "OAuthSignin" | "OAuthCallback" | "OAuthCreateAccount" | "EmailCreateAccount" | "Callback" | "OAuthAccountNotLinked" | "EmailSignin" | "CredentialsSignin" | "SessionRequired"; | ||
export type SignInPageErrorParam = "Signin" | "OAuthSignin" | "OAuthCallbackError" | "OAuthCreateAccount" | "EmailCreateAccount" | "Callback" | "OAuthAccountNotLinked" | "EmailSignin" | "CredentialsSignin" | "SessionRequired"; | ||
export interface PagesOptions { | ||
@@ -343,3 +436,30 @@ /** | ||
} | ||
export type AuthAction = "providers" | "session" | "csrf" | "signin" | "signout" | "callback" | "verify-request" | "error"; | ||
/** | ||
* Supported actions by Auth.js. Each action map to a REST API endpoint. | ||
* Some actions have a `GET` and `POST` variant, depending on if the action | ||
* changes the state of the server. | ||
* | ||
* - **`"callback"`**: | ||
* - **`GET`**: Handles the callback from an [OAuth provider](https://authjs.dev/reference/core/providers_oauth). | ||
* - **`POST`**: Handles the callback from a [Credentials provider](https://authjs.dev/reference/core/providers_credentials). | ||
* - **`"csrf"`**: Returns the raw CSRF token, which is saved in a cookie (encrypted). | ||
* It is used for CSRF protection, implementing the [double submit cookie](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie) technique. | ||
* :::note | ||
* Some frameworks have built-in CSRF protection and can therefore disable this action. In this case, the corresponding endpoint will return a 404 response. Read more at [`skipCSRFCheck`](https://authjs.dev/reference/core#skipcsrfcheck). | ||
* _⚠ We don't recommend manually disabling CSRF protection, unless you know what you're doing._ | ||
* ::: | ||
* - **`"error"`**: Renders the built-in error page. | ||
* - **`"providers"`**: Returns a client-safe list of all configured providers. | ||
* - **`"session"`**: | ||
* - **`GET**`: Returns the user's session if it exists, otherwise `null`. | ||
* - **`POST**`: Updates the user's session and returns the updated session. | ||
* - **`"signin"`**: | ||
* - **`GET`**: Renders the built-in sign-in page. | ||
* - **`POST`**: Initiates the sign-in flow. | ||
* - **`"signout"`**: | ||
* - **`GET`**: Renders the built-in sign-out page. | ||
* - **`POST`**: Initiates the sign-out flow. This will invalidate the user's session (deleting the cookie, and if there is a session in the database, it will be deleted as well). | ||
* - **`"verify-request"`**: Renders the built-in verification request page. | ||
*/ | ||
export type AuthAction = "callback" | "csrf" | "error" | "providers" | "session" | "signin" | "signout" | "verify-request"; | ||
//# sourceMappingURL=types.d.ts.map |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1254284
31953