diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index a9e8d38b98..3edc51e76a 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -47,7 +47,7 @@ export async function AuthInternal< case "providers": return (await routes.providers(options.providers)) as any case "session": { - const session = await routes.session(sessionStore, options) + const session = await routes.session({ sessionStore, options }) if (session.cookies) cookies.push(...session.cookies) // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion return { ...session, cookies } as any @@ -177,6 +177,22 @@ export async function AuthInternal< return { ...callback, cookies } } 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: } } diff --git a/packages/core/src/lib/routes/callback.ts b/packages/core/src/lib/routes/callback.ts index 2cb41f4253..94945855e0 100644 --- a/packages/core/src/lib/routes/callback.ts +++ b/packages/core/src/lib/routes/callback.ts @@ -134,6 +134,7 @@ export async function callback(params: { account, profile: OAuthProfile, isNewUser, + trigger: isNewUser ? "signUp" : "signIn", }) // Clear cookies if token is null @@ -244,6 +245,7 @@ export async function callback(params: { user: loggedInUser, account, isNewUser, + trigger: isNewUser ? "signUp" : "signIn", }) // Clear cookies if token is null @@ -340,6 +342,7 @@ export async function callback(params: { // @ts-expect-error account, isNewUser: false, + trigger: "signIn", }) // Clear cookies if token is null diff --git a/packages/core/src/lib/routes/session.ts b/packages/core/src/lib/routes/session.ts index eff5ec1bc8..2556975959 100644 --- a/packages/core/src/lib/routes/session.ts +++ b/packages/core/src/lib/routes/session.ts @@ -6,10 +6,13 @@ import type { InternalOptions, ResponseInternal, Session } from "../../types.js" import type { SessionStore } from "../cookie.js" /** Return a session object filtered via `callbacks.session` */ -export async function session( - sessionStore: SessionStore, +export async function session(params: { options: InternalOptions -): Promise> { + sessionStore: SessionStore + isUpdate?: boolean + newSession?: any +}): Promise> { + const { options, sessionStore, newSession, isUpdate } = params const { adapter, jwt, @@ -33,22 +36,26 @@ export async function session( try { const decodedToken = await jwt.decode({ ...jwt, token: sessionToken }) + const token = await callbacks.jwt({ + // @ts-expect-error + 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, + name: token?.name, + email: token?.email, + image: token?.picture, }, expires: newExpires.toISOString(), } - // @ts-expect-error - const token = await callbacks.jwt({ token: decodedToken }) - if (token !== null) { // @ts-expect-error const newSession = await callbacks.session({ session, token }) @@ -128,11 +135,13 @@ export async function session( user: { name: user.name, email: user.email, - image: user.image, + picture: user.image, }, expires: session.expires.toISOString(), }, user, + newSession, + ...(isUpdate ? { trigger: "update" } : {}), }) // Return session payload as response diff --git a/packages/core/src/providers/credentials.ts b/packages/core/src/providers/credentials.ts index 0d92ec1105..598413487d 100644 --- a/packages/core/src/providers/credentials.ts +++ b/packages/core/src/providers/credentials.ts @@ -32,12 +32,14 @@ export interface CredentialsConfig< * @example * ```ts * //... - * async authorize(, request) { + * async authorize(credentials, request) { + * if(!isValidCredentials(credentials)) return null * const response = await fetch(request) * if(!response.ok) return null * return await response.json() ?? null * } * //... + * ``` */ authorize: ( /** diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 7ced87efc2..9b6197c229 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -238,40 +238,86 @@ export interface CallbacksOptions

{ * If you want to make something available you added to the token through the `jwt` callback, * you have to explicitly forward it here to make it available to the client. * - * [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: ( + 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 /** * This callback is called whenever a JSON Web Token is created (i.e. at sign in) * or updated (i.e whenever a session is accessed in the client). * Its content is forwarded to the `session` callback, * 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 } @@ -451,7 +497,9 @@ export type InternalProvider = (T extends "oauth" * ::: * - **`"error"`**: Renders the built-in error page. * - **`"providers"`**: Returns a client-safe list of all configured providers. - * - **`"session"`**: Returns the user's session if it exists, otherwise `null`. + * - **`"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.