From fd261441dd0365796abd617b0e6d589fbd0ef375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 15:18:26 +0200 Subject: [PATCH 01/15] init --- packages/core/src/index.ts | 191 ++++++++++++++++++-------- packages/core/src/jwt.ts | 12 +- packages/core/src/lib/utils/logger.ts | 30 ++-- packages/core/src/types.ts | 10 +- 4 files changed, 169 insertions(+), 74 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 33b5cdd14b..c2baeecff7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -213,54 +213,76 @@ export async function Auth( * * @see [Initialization](https://authjs.dev/reference/core/types#authconfig) */ -export interface AuthConfig { +export interface AuthConfig + extends IntegrationAuthConfig, + DeprecatedAuthConfig { /** - * List of authentication providers for signing in + * The only required option. + * + * The list of authentication providers for signing in * (e.g. Google, Facebook, Twitter, GitHub, Email, etc) in any order. * This can be one of the built-in providers or an object with a custom provider. - * - * @default [] */ providers: Provider[] /** - * A random string used to hash tokens, sign cookies and generate cryptographic keys. + * A cryptographically random string or list of strings that is used to hash tokens, + * seal cookies (JWT encryption by default) and generate other cryptographic keys. + * + * You can generate a random string with our CLI: `npx auth secret` or use a tool like `openssl`. * - * To generate a random string, you can use the Auth.js CLI: `npx auth secret` + * If you pass an array of secrets, we will iterate over them from first-to-last, trying to unseal JWT encrypted cookies. * * @note - * You can also pass an array of secrets, in which case the first secret that successfully - * decrypts the JWT will be used. This is useful for rotating secrets without invalidating existing sessions. - * The newer secret should be added to the start of the array, which will be used for all new sessions. + * This is useful for rotating secrets without invalidating existing sessions. + * The newer secret should be added to the start of the array. This will be used for all new sessions. + * + * We support inferring up to 4 secrets from the environment variables `AUTH_SECRET`, `AUTH_SECRET_1`, `AUTH_SECRET_2`, `AUTH_SECRET_3`, + * in which case, this option is optional. * */ secret?: string | string[] - /** - * Configure your session like if you want to use JWT or a database, - * how long until an idle session expires, or to throttle write operations in case you are using a database. - */ + /** Configure how you want to persist your session, how often it should be updated, or in what format it should be saved. */ session?: { /** * Choose how you want to save the user session. - * The default is `"jwt"`, an encrypted JWT (JWE) in the session cookie. * - * If you use an `adapter` however, we default it to `"database"` instead. - * You can still force a JWT session by explicitly defining `"jwt"`. + * The default is `"cookie"` (Previously called "jwt", but same behavior). This saves the session information as an [encrypted JWT](https://datatracker.ietf.org/doc/html/rfc7516) in cookies. + * + * @note Even if the persisted information would exceed the 4kb cookie limit most browsers impose, Auth.js + * will chunk the cookie into multiple cookies to avoid this limitation. + * + * If you use an {@link AuthConfig.adapter} however, the default is set to `"database"` instead. * - * When using `"database"`, the session cookie will only contain a `sessionToken` value, - * which is used to look up the session in the database. + * Note, that you can still force a JWT session by explicitly defining `"jwt"`. * - * [Documentation](https://authjs.dev/reference/core#authconfig#session) | [Adapter](https://authjs.dev/reference/core#authconfig#adapter) | [About JSON Web Tokens](https://authjs.dev/concepts/session-strategies#jwt-session) + * Learn more about the different [session strategies](https://authjs.dev/concepts/session-strategies), + * their advantages and disadvantages. */ - strategy?: "jwt" | "database" + strategy?: "cookie" | "database" | "jwt" /** - * Relative time from now in seconds when to expire the session + * Either a relative time in seconds, or an absolute `Date` when to expire the session. + * + * - If a relative time is set, the session expiry is updated when the session is accessed, + * but at most at the rate of `updateAge` value. + * + * @note This strikes a balance between updating the session too often + * or letting it expire mid-action while the user is interacting with the site. + * + * - If an absolute `Date` is set, the session will expire at that time, regardless of activity. + * + * @note Currently, there is no way to expire a session when the browser is closed, as most browsers + * keep running in the background and keep the session alive indefinitely, which would give a false sense of security, + * as the session would still be valid if the browser is reopened. + * For this reason, we recommend: + * 1. setting a short `maxAge` + * 2. using a database session strategy that you can revoke server-side + * 3. set an absolute `Date` for the session expiry * * @default 2592000 // 30 days */ - maxAge?: number + maxAge?: number | Date /** - * How often the session should be updated in seconds. - * If set to `0`, session is updated every time. + * How often the session should be updated in seconds. If set to `0`, the session is updated every time. * * @default 86400 // 1 day */ @@ -270,20 +292,33 @@ export interface AuthConfig { * By default, a random UUID or string is generated depending on the Node.js version. * However, you can specify your own custom string (such as CUID) to be used. * + * @note this is not equivalent to the ID of the session in the database, to avoid leaking information, + * eg. if the database creates predictable IDs. + * * @default `randomUUID` or `randomBytes.toHex` depending on the Node.js version */ generateSessionToken?: () => string + /** + * Seals the session payload in the cookie, to obscure the data from the client. + * + * By default, the cookie is sealed using an encrypted JWT. It uses the _A256CBC-HS512_ algorithm ({@link https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5 JWE}). + * {@link AuthConfig.session.secret} is used to derive a suitable encryption key. + */ + seal?: () => Awaitable + /** + * Unseals the session payload from the cookie, to read the data on the server. + * + * By default, the cookie is sealed using an encrypted JWT. It uses the _A256CBC-HS512_ algorithm ({@link https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5 JWE}). + * {@link AuthConfig.session.secret} is used to derive the encryption key. + * + * If you passed an array of secrets, we will iterate over them from first-to-last, trying to unseal the data. + */ + unseal?: () => Awaitable } - /** - * JSON Web Tokens are enabled by default if you have not specified an {@link AuthConfig.adapter}. - * JSON Web Tokens are encrypted (JWE) by default. We recommend you keep this behaviour. - */ - jwt?: Partial /** * Specify URLs to be used if you want to create custom sign in, sign out and error pages. * Pages specified will override the corresponding built-in page. * - * @default {} * @example * * ```ts @@ -296,7 +331,7 @@ export interface AuthConfig { * } * ``` */ - pages?: Partial + pages?: PagesOptions /** * Callbacks are asynchronous functions you can use to control what happens when an action is performed. * Callbacks are *extremely powerful*, especially in scenarios involving JSON Web Tokens @@ -530,16 +565,17 @@ export interface AuthConfig { /** You can use the adapter option to pass in your database adapter. */ adapter?: Adapter /** - * Set debug to true to enable debug messages for authentication and database operations. + * Set the log level for the built-in logger. * - * - ⚠ If you added a custom {@link AuthConfig.logger}, this setting is ignored. + * If any of the log levels are overriden in {@link AuthConfig.logger}, + * this setting is ignored for that level. * - * @default false + * @default "error" */ - debug?: boolean + logLevel?: "verbose" | "warn" | "error" | "silent" /** * Override any of the logger levels (`undefined` levels will use the built-in logger), - * and intercept logs in NextAuth. You can use this option to send NextAuth logs to a third-party logging service. + * and intercept logs in Auth.js. You can use this option to send Auth.js logs to a third-party logging service. * * @example * @@ -598,17 +634,21 @@ export interface AuthConfig { */ cookies?: Partial /** - * Auth.js relies on the incoming request's `host` header to function correctly. For this reason this property needs to be set to `true`. + * Auth.js relies on the incoming request's `host` header to function correctly. For this reason this property needs to be set to `true` explicitly. * * Make sure that your deployment platform sets the `host` header safely. * - * :::note - * Official Auth.js-based libraries will attempt to set this value automatically for some deployment platforms (eg.: Vercel) that are known to set the `host` header safely. - * ::: + * @note + * Auth.js will attempt to set this value automatically for some cases, eg.: if it detects a trusted platform's environment variable, + * or if the host value can be inferred from the environment, instead of the incoming request. + * + * The following conditions will enable this automatically: + * + * ```ts + * AUTH_URL ?? AUTH_TRUST_HOST ?? VERCEL ?? CF_PAGES ?? NODE_ENV !== "production" + * ``` */ trustHost?: boolean - skipCSRFCheck?: typeof skipCSRFCheck - raw?: typeof raw /** * When set, during an OAuth sign-in flow, * the `redirect_uri` of the authorization request @@ -641,25 +681,64 @@ export interface AuthConfig { * See also: [Guide: Securing a Preview Deployment](https://authjs.dev/getting-started/deployment#securing-a-preview-deployment) */ redirectProxyUrl?: string - /** - * Use this option to enable experimental features. - * When enabled, it will print a warning message to the console. - * @note Experimental features are not guaranteed to be stable and may change or be removed without notice. Please use with caution. - * @default {} + * Enable/disable experimental features. + * + * @note Experimental features are not guaranteed to be stable and may change or be removed without notice. */ - experimental?: { - /** - * Enable WebAuthn support. - * - * @default false - */ - enableWebAuthn?: boolean - } + experimental?: ExperimentalOptions /** * The base path of the Auth.js API endpoints. * - * @default "/api/auth" in "next-auth"; "/auth" with all other frameworks + * @default `"/api/auth"` in "next-auth" (for historical reasons only); `"/auth"` for all other frameworks */ basePath?: string } + +interface ExperimentalOptions { + /** + * Enable [WebAuthn](https://authjs.dev/getting-started/authentication/webauthn) support. + * + * @default false + */ + enableWebAuthn?: boolean +} + +interface DeprecatedAuthConfig { + /** + * Set debug to true to enable debug messages for authentication and database operations. + * + * - ⚠ If you added a custom {@link AuthConfig.logger}, this setting is ignored. + * + * @default false + * @deprecated Use `logLevel: "verbose"` instead. + */ + debug?: boolean + /** + * JSON Web Tokens are enabled by default if you have not specified an {@link AuthConfig.adapter}. + * JSON Web Tokens are encrypted (JWE) by default. We recommend you keep this behaviour. + * + * @deprecated + */ + jwt?: Partial +} + +/** + * These options are meant for integrators who would like to use `@auth/core` as the base for their library. + * + * If you are a developer, you likely do not need these options. + */ +export interface IntegrationAuthConfig { + /** + * Auth.js ships its own CSRF protection. You can disable this, if your framework has built-in protection. + * Make sure your framework covers both server and client-side. + */ + skipCSRFCheck?: typeof skipCSRFCheck + /** + * By default, the `@auth/core` package returns a `Response` object. + * It might be easier though to not needing to re-parse the response if you are creating + * a framework-specific package. This option will make the Auth.js core return + * the internal response object instead. + */ + raw?: typeof raw +} diff --git a/packages/core/src/jwt.ts b/packages/core/src/jwt.ts index a7420d1918..e46b03309c 100644 --- a/packages/core/src/jwt.ts +++ b/packages/core/src/jwt.ts @@ -275,8 +275,16 @@ export interface JWTOptions { * @default 30 * 24 * 60 * 60 // 30 days */ maxAge: number - /** Override this method to control the Auth.js issued JWT encoding. */ + /** + * Override this method to control the Auth.js issued JWT encoding. + * + * @deprecated Use {@link AuthConfig.session.seal} instead. + */ encode: (params: JWTEncodeParams) => Awaitable - /** Override this method to control the Auth.js issued JWT decoding. */ + /** + * Override this method to control the Auth.js issued JWT decoding. + * + * @deprecated Use {@link AuthConfig.session.unseal} instead. + */ decode: (params: JWTDecodeParams) => Awaitable } diff --git a/packages/core/src/lib/utils/logger.ts b/packages/core/src/lib/utils/logger.ts index 031f614964..f42b1158ce 100644 --- a/packages/core/src/lib/utils/logger.ts +++ b/packages/core/src/lib/utils/logger.ts @@ -60,22 +60,30 @@ const defaultLogger: LoggerInstance = { /** * Override the built-in logger with user's implementation. - * Any `undefined` level will use the default logger. + * Any `undefined` level will use the default. */ export function setLogger( - config: Pick + config: Pick ): LoggerInstance { - const newLogger: LoggerInstance = { - ...defaultLogger, - } + const newLogger: LoggerInstance = { error() {}, warn() {}, debug() {} } + + let { debug, logLevel = "silent", logger } = config + if (debug) logLevel = "verbose" + + const levels = ["error", "warn", "debug"] + const levelIndex = levels.indexOf(logLevel) - // Turn off debug logging if `debug` isn't set to `true` - if (!config.debug) newLogger.debug = () => {} + for (const level of levels) { + if (levels.indexOf(level) <= levelIndex) { + newLogger[level] = defaultLogger[level] + } + } - if (config.logger?.error) newLogger.error = config.logger.error - if (config.logger?.warn) newLogger.warn = config.logger.warn - if (config.logger?.debug) newLogger.debug = config.logger.debug + // User preference overrides default logger + if (logger?.error) newLogger.error = logger.error + if (logger?.warn) newLogger.warn = logger.warn + if (logger?.debug) newLogger.debug = logger.debug + logger ??= newLogger - config.logger ??= newLogger return newLogger } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 36d8a9c062..05b5350da3 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -222,8 +222,8 @@ export interface PagesOptions { * * @default "/signin" */ - signIn: string - signOut: string + signIn?: string + signOut?: string /** * The path to the error page. * @@ -232,10 +232,10 @@ export interface PagesOptions { * * @default "/error" */ - error: string - verifyRequest: string + error?: string + verifyRequest?: string /** If set, new users will be directed here on first sign in */ - newUser: string + newUser?: string } type ISODateString = string From 6ce090940807c6ddc9ea55a5780883415d338065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:11:02 +0200 Subject: [PATCH 02/15] =?UTF-8?q?resCookies,=20`InternalOptions`=20->?= =?UTF-8?q?=C2=A0`InternalConfig`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/index.ts | 17 +++-- .../src/lib/actions/callback/handle-login.ts | 6 +- .../core/src/lib/actions/callback/index.ts | 8 +-- .../lib/actions/callback/oauth/callback.ts | 4 +- .../src/lib/actions/callback/oauth/checks.ts | 22 +++--- .../lib/actions/callback/oauth/csrf-token.ts | 4 +- packages/core/src/lib/actions/session.ts | 4 +- .../lib/actions/signin/authorization-url.ts | 4 +- packages/core/src/lib/actions/signin/index.ts | 4 +- .../core/src/lib/actions/signin/send-token.ts | 4 +- packages/core/src/lib/actions/signout.ts | 4 +- .../core/src/lib/actions/webauthn-options.ts | 4 +- packages/core/src/lib/index.ts | 72 +++++++++++++------ packages/core/src/lib/init.ts | 59 ++++++++------- packages/core/src/lib/pages/index.ts | 6 +- packages/core/src/lib/utils/callback-url.ts | 4 +- packages/core/src/lib/utils/session.ts | 4 +- packages/core/src/lib/utils/webauthn-utils.ts | 8 +-- packages/core/src/providers/webauthn.ts | 8 +-- packages/core/src/types.ts | 4 +- 20 files changed, 142 insertions(+), 108 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c2baeecff7..5246ba3d7a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -43,11 +43,13 @@ import { ErrorPageLoop, isClientError, } from "./errors.js" -import { AuthInternal, raw, skipCSRFCheck } from "./lib/index.js" +import { AuthInternal } from "./lib/index.js" import { setEnvDefaults, createActionURL } from "./lib/utils/env.js" import renderPage from "./lib/pages/index.js" import { setLogger, type LoggerInstance } from "./lib/utils/logger.js" import { toInternalRequest, toResponse } from "./lib/utils/web.js" +import { isAuthAction } from "./lib/utils/actions.js" +import { raw, skipCSRFCheck, customFetch } from "./lib/symbols.js" import type { Adapter, AdapterSession, AdapterUser } from "./adapters.js" import type { @@ -64,11 +66,16 @@ import type { User, } from "./types.js" import type { CredentialInput, Provider } from "./providers/index.js" -import { JWT, JWTOptions } from "./jwt.js" -import { isAuthAction } from "./lib/utils/actions.js" +import type { JWT, JWTOptions } from "./jwt.js" -export { customFetch } from "./lib/symbols.js" -export { skipCSRFCheck, raw, setEnvDefaults, createActionURL, isAuthAction } +export { + setEnvDefaults, + createActionURL, + isAuthAction, + customFetch, + raw, + skipCSRFCheck, +} export async function Auth( request: Request, diff --git a/packages/core/src/lib/actions/callback/handle-login.ts b/packages/core/src/lib/actions/callback/handle-login.ts index e053c3ef63..74624dd10f 100644 --- a/packages/core/src/lib/actions/callback/handle-login.ts +++ b/packages/core/src/lib/actions/callback/handle-login.ts @@ -6,7 +6,7 @@ import type { AdapterSession, AdapterUser, } from "../../../adapters.js" -import type { Account, InternalOptions, User } from "../../../types.js" +import type { Account, InternalConfig, User } from "../../../types.js" import type { JWT } from "../../../jwt.js" import type { OAuthConfig } from "../../../providers/index.js" import type { SessionToken } from "../../utils/cookie.js" @@ -27,7 +27,7 @@ export async function handleLoginOrRegister( sessionToken: SessionToken, _profile: User | AdapterUser | { email: string }, _account: AdapterAccount | Account | null, - options: InternalOptions + options: InternalConfig ) { // Input validation if (!_account?.providerAccountId || !_account.type) @@ -251,7 +251,7 @@ export async function handleLoginOrRegister( return { session, user: userByAccount, isNewUser } } else { - const { provider: p } = options as InternalOptions<"oauth" | "oidc"> + const { provider: p } = options as InternalConfig<"oauth" | "oidc"> const { type, provider, providerAccountId, userId, ...tokenSet } = account const defaults = { providerAccountId, provider, type, userId } account = Object.assign(p.account(tokenSet) ?? {}, defaults) diff --git a/packages/core/src/lib/actions/callback/index.ts b/packages/core/src/lib/actions/callback/index.ts index d11048f7cb..ab4bb47c34 100644 --- a/packages/core/src/lib/actions/callback/index.ts +++ b/packages/core/src/lib/actions/callback/index.ts @@ -17,7 +17,7 @@ import type { AdapterSession } from "../../../adapters.js" import type { Account, Authenticator, - InternalOptions, + InternalConfig, RequestInternal, ResponseInternal, User, @@ -32,7 +32,7 @@ import { /** Handle callbacks from login services */ export async function callback( request: RequestInternal, - options: InternalOptions, + options: InternalConfig, sessionStore: SessionStore, cookies: Cookie[] ): Promise { @@ -537,8 +537,8 @@ export async function callback( } async function handleAuthorized( - params: Parameters[0], - config: InternalOptions + params: Parameters[0], + config: InternalConfig ): Promise { let authorized const { signIn, redirect } = config.callbacks diff --git a/packages/core/src/lib/actions/callback/oauth/callback.ts b/packages/core/src/lib/actions/callback/oauth/callback.ts index 1b8c93e1f9..99dc35a317 100644 --- a/packages/core/src/lib/actions/callback/oauth/callback.ts +++ b/packages/core/src/lib/actions/callback/oauth/callback.ts @@ -7,7 +7,7 @@ import { import type { Account, - InternalOptions, + InternalConfig, LoggerInstance, Profile, RequestInternal, @@ -47,7 +47,7 @@ function clientSecretBasic(clientId: string, clientSecret: string) { export async function handleOAuth( params: RequestInternal["query"], cookies: RequestInternal["cookies"], - options: InternalOptions<"oauth" | "oidc"> + options: InternalConfig<"oauth" | "oidc"> ) { const { logger, provider } = options diff --git a/packages/core/src/lib/actions/callback/oauth/checks.ts b/packages/core/src/lib/actions/callback/oauth/checks.ts index 8d8fac115b..e80d9c254f 100644 --- a/packages/core/src/lib/actions/callback/oauth/checks.ts +++ b/packages/core/src/lib/actions/callback/oauth/checks.ts @@ -6,7 +6,7 @@ import { decode, encode } from "../../../../jwt.js" import type { CookiesOptions, - InternalOptions, + InternalConfig, RequestInternal, User, } from "../../../../types.js" @@ -23,7 +23,7 @@ const COOKIE_TTL = 60 * 15 // 15 minutes async function sealCookie( name: keyof CookiesOptions, payload: string, - options: InternalOptions<"oauth" | "oidc" | WebAuthnProviderType> + options: InternalConfig<"oauth" | "oidc" | WebAuthnProviderType> ): Promise { const { cookies, logger } = options const cookie = cookies[name] @@ -50,7 +50,7 @@ async function sealCookie( async function parseCookie( name: keyof CookiesOptions, value: string | undefined, - options: InternalOptions + options: InternalConfig ): Promise { try { const { logger, cookies, jwt } = options @@ -73,7 +73,7 @@ async function parseCookie( function clearCookie( name: keyof CookiesOptions, - options: InternalOptions, + options: InternalConfig, resCookies: Cookie[] ) { const { logger, cookies } = options @@ -93,7 +93,7 @@ function useCookie( return async function ( cookies: RequestInternal["cookies"], resCookies: Cookie[], - options: InternalOptions<"oidc"> + options: InternalConfig<"oidc"> ) { const { provider, logger } = options if (!provider?.checks?.includes(check)) return @@ -111,7 +111,7 @@ function useCookie( */ export const pkce = { /** Creates a PKCE code challenge and verifier pair. The verifier in stored in the cookie. */ - async create(options: InternalOptions<"oauth">) { + async create(options: InternalConfig<"oauth">) { const code_verifier = o.generateRandomCodeVerifier() const value = await o.calculatePKCECodeChallenge(code_verifier) const cookie = await sealCookie("pkceCodeVerifier", code_verifier, options) @@ -139,7 +139,7 @@ const encodedStateSalt = "encodedState" */ export const state = { /** Creates a state cookie with an optionally encoded body. */ - async create(options: InternalOptions<"oauth">, origin?: string) { + async create(options: InternalConfig<"oauth">, origin?: string) { const { provider } = options if (!provider.checks.includes("state")) { if (origin) { @@ -172,7 +172,7 @@ export const state = { */ use: useCookie("state", "state"), /** Decodes the state. If it could not be decoded, it throws an error. */ - async decode(state: string, options: InternalOptions) { + async decode(state: string, options: InternalConfig) { try { options.logger.debug("DECODE_STATE", { state }) const payload = await decode({ @@ -189,7 +189,7 @@ export const state = { } export const nonce = { - async create(options: InternalOptions<"oidc">) { + async create(options: InternalConfig<"oidc">) { if (!options.provider.checks.includes("nonce")) return const value = o.generateRandomNonce() const cookie = await sealCookie("nonce", value, options) @@ -215,7 +215,7 @@ interface WebAuthnChallengePayload { const webauthnChallengeSalt = "encodedWebauthnChallenge" export const webauthnChallenge = { async create( - options: InternalOptions, + options: InternalConfig, challenge: string, registerData?: User ) { @@ -234,7 +234,7 @@ export const webauthnChallenge = { }, /** Returns WebAuthn challenge if present. */ async use( - options: InternalOptions, + options: InternalConfig, cookies: RequestInternal["cookies"], resCookies: Cookie[] ): Promise { diff --git a/packages/core/src/lib/actions/callback/oauth/csrf-token.ts b/packages/core/src/lib/actions/callback/oauth/csrf-token.ts index 337c1d62dd..2f2dab77c8 100644 --- a/packages/core/src/lib/actions/callback/oauth/csrf-token.ts +++ b/packages/core/src/lib/actions/callback/oauth/csrf-token.ts @@ -1,9 +1,9 @@ import { createHash, randomString } from "../../../utils/web.js" -import type { AuthAction, InternalOptions } from "../../../../types.js" +import type { AuthAction, InternalConfig } from "../../../../types.js" import { MissingCSRF } from "../../../../errors.js" interface CreateCSRFTokenParams { - options: InternalOptions + options: InternalConfig cookieValue?: string isPost: boolean bodyValue?: string diff --git a/packages/core/src/lib/actions/session.ts b/packages/core/src/lib/actions/session.ts index 7ff6f7f357..ad7fabd1dd 100644 --- a/packages/core/src/lib/actions/session.ts +++ b/packages/core/src/lib/actions/session.ts @@ -2,12 +2,12 @@ import { JWTSessionError, SessionTokenError } from "../../errors.js" import { fromDate } from "../utils/date.js" import type { Adapter } from "../../adapters.js" -import type { InternalOptions, ResponseInternal, Session } from "../../types.js" +import type { InternalConfig, ResponseInternal, Session } from "../../types.js" import type { Cookie, SessionStore } from "../utils/cookie.js" /** Return a session object filtered via `callbacks.session` */ export async function session( - options: InternalOptions, + options: InternalConfig, sessionStore: SessionStore, cookies: Cookie[], isUpdate?: boolean, diff --git a/packages/core/src/lib/actions/signin/authorization-url.ts b/packages/core/src/lib/actions/signin/authorization-url.ts index ac7d96aeee..373c8137f5 100644 --- a/packages/core/src/lib/actions/signin/authorization-url.ts +++ b/packages/core/src/lib/actions/signin/authorization-url.ts @@ -1,7 +1,7 @@ import * as checks from "../callback/oauth/checks.js" import * as o from "oauth4webapi" -import type { InternalOptions, RequestInternal } from "../../../types.js" +import type { InternalConfig, RequestInternal } from "../../../types.js" import type { Cookie } from "../../utils/cookie.js" import { customFetch } from "../../symbols.js" @@ -12,7 +12,7 @@ import { customFetch } from "../../symbols.js" */ export async function getAuthorizationUrl( query: RequestInternal["query"], - options: InternalOptions<"oauth" | "oidc"> + options: InternalConfig<"oauth" | "oidc"> ) { const { logger, provider } = options diff --git a/packages/core/src/lib/actions/signin/index.ts b/packages/core/src/lib/actions/signin/index.ts index e47c934ea0..a120bcfaef 100644 --- a/packages/core/src/lib/actions/signin/index.ts +++ b/packages/core/src/lib/actions/signin/index.ts @@ -3,7 +3,7 @@ import { sendToken } from "./send-token.js" import type { Cookie } from "../../utils/cookie.js" import type { - InternalOptions, + InternalConfig, RequestInternal, ResponseInternal, } from "../../../types.js" @@ -11,7 +11,7 @@ import type { export async function signIn( request: RequestInternal, cookies: Cookie[], - options: InternalOptions + options: InternalConfig ): Promise { const signInUrl = `${options.url.origin}${options.basePath}/signin` diff --git a/packages/core/src/lib/actions/signin/send-token.ts b/packages/core/src/lib/actions/signin/send-token.ts index 27024affaf..c6702b4e79 100644 --- a/packages/core/src/lib/actions/signin/send-token.ts +++ b/packages/core/src/lib/actions/signin/send-token.ts @@ -1,7 +1,7 @@ import { createHash, randomString, toRequest } from "../../utils/web.js" import { AccessDenied } from "../../../errors.js" -import type { InternalOptions, RequestInternal } from "../../../types.js" +import type { InternalConfig, RequestInternal } from "../../../types.js" import type { Account } from "../../../types.js" /** @@ -11,7 +11,7 @@ import type { Account } from "../../../types.js" */ export async function sendToken( request: RequestInternal, - options: InternalOptions<"email"> + options: InternalConfig<"email"> ) { const { body } = request const { provider, callbacks, adapter } = options diff --git a/packages/core/src/lib/actions/signout.ts b/packages/core/src/lib/actions/signout.ts index b09c90933b..3368c1167c 100644 --- a/packages/core/src/lib/actions/signout.ts +++ b/packages/core/src/lib/actions/signout.ts @@ -1,6 +1,6 @@ import { SignOutError } from "../../errors.js" -import type { InternalOptions, ResponseInternal } from "../../types.js" +import type { InternalConfig, ResponseInternal } from "../../types.js" import type { Cookie, SessionStore } from "../utils/cookie.js" /** @@ -13,7 +13,7 @@ import type { Cookie, SessionStore } from "../utils/cookie.js" export async function signOut( cookies: Cookie[], sessionStore: SessionStore, - options: InternalOptions + options: InternalConfig ): Promise { const { jwt, events, callbackUrl: redirect, logger, session } = options const sessionToken = sessionStore.value diff --git a/packages/core/src/lib/actions/webauthn-options.ts b/packages/core/src/lib/actions/webauthn-options.ts index f34e4cc3e4..fc08391a19 100644 --- a/packages/core/src/lib/actions/webauthn-options.ts +++ b/packages/core/src/lib/actions/webauthn-options.ts @@ -1,5 +1,5 @@ import type { - InternalOptions, + InternalConfig, RequestInternal, ResponseInternal, User, @@ -19,7 +19,7 @@ import { */ export async function webAuthnOptions( request: RequestInternal, - options: InternalOptions, + options: InternalConfig, sessionStore: SessionStore, cookies: Cookie[] // @ts-expect-error issue with returning from a switch case diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 00ee0027c1..6e1272c9f0 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -9,19 +9,17 @@ import type { RequestInternal, ResponseInternal } from "../types.js" import type { AuthConfig } from "../index.js" import { skipCSRFCheck } from "./symbols.js" -export { customFetch, raw, skipCSRFCheck } from "./symbols.js" - /** @internal */ export async function AuthInternal( request: RequestInternal, - authOptions: AuthConfig + config: AuthConfig ): Promise { const { action, providerId, error, method } = request - const csrfDisabled = authOptions.skipCSRFCheck === skipCSRFCheck + const csrfDisabled = config.skipCSRFCheck === skipCSRFCheck - const { options, cookies } = await init({ - authOptions, + const internalConfig = await init({ + config, action, providerId, url: request.url, @@ -33,24 +31,41 @@ export async function AuthInternal( }) const sessionStore = new SessionStore( - options.cookies.sessionToken, + internalConfig.cookies.sessionToken, request.cookies, - options.logger + internalConfig.logger ) if (method === "GET") { - const render = renderPage({ ...options, query: request.query, cookies }) + const render = renderPage({ + ...internalConfig, + query: request.query, + cookies: internalConfig.resCookies, + }) switch (action) { case "callback": - return await actions.callback(request, options, sessionStore, cookies) + return await actions.callback( + request, + internalConfig, + sessionStore, + internalConfig.resCookies + ) case "csrf": - return render.csrf(csrfDisabled, options, cookies) + return render.csrf( + csrfDisabled, + internalConfig, + internalConfig.resCookies + ) case "error": return render.error(error) case "providers": - return render.providers(options.providers) + return render.providers(internalConfig.providers) case "session": - return await actions.session(options, sessionStore, cookies) + return await actions.session( + internalConfig, + sessionStore, + internalConfig.resCookies + ) case "signin": return render.signin(providerId, error) case "signout": @@ -60,36 +75,49 @@ export async function AuthInternal( case "webauthn-options": return await actions.webAuthnOptions( request, - options, + internalConfig, sessionStore, - cookies + internalConfig.resCookies ) default: } } else { - const { csrfTokenVerified } = options + const { csrfTokenVerified } = internalConfig switch (action) { case "callback": - if (options.provider.type === "credentials") + if (internalConfig.provider.type === "credentials") // Verified CSRF Token required for credentials providers only validateCSRF(action, csrfTokenVerified) - return await actions.callback(request, options, sessionStore, cookies) + return await actions.callback( + request, + internalConfig, + sessionStore, + internalConfig.resCookies + ) case "session": validateCSRF(action, csrfTokenVerified) return await actions.session( - options, + internalConfig, sessionStore, - cookies, + internalConfig.resCookies, true, request.body?.data ) case "signin": validateCSRF(action, csrfTokenVerified) - return await actions.signIn(request, cookies, options) + return await actions.signIn( + request, + internalConfig.resCookies, + internalConfig + ) case "signout": validateCSRF(action, csrfTokenVerified) - return await actions.signOut(cookies, sessionStore, options) + return await actions.signOut( + internalConfig.resCookies, + sessionStore, + internalConfig + ) default: } } diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index 9e8ca122f6..c3961b4a62 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -8,14 +8,17 @@ import parseProviders from "./utils/providers.js" import { setLogger, type LoggerInstance } from "./utils/logger.js" import { merge } from "./utils/merge.js" -import type { InternalOptions, RequestInternal } from "../types.js" +import type { + InternalConfig as InternalConfig, + RequestInternal, +} from "../types.js" import type { AuthConfig } from "../index.js" interface InitParams { url: URL - authOptions: AuthConfig + config: AuthConfig providerId?: string - action: InternalOptions["action"] + action: InternalConfig["action"] /** Callback URL value extracted from the incoming request. */ callbackUrl?: string /** CSRF token value extracted from the incoming request. From body if POST, from query if GET */ @@ -26,7 +29,7 @@ interface InitParams { cookies: RequestInternal["cookies"] } -export const defaultCallbacks: InternalOptions["callbacks"] = { +export const defaultCallbacks: InternalConfig["callbacks"] = { signIn() { return true }, @@ -52,7 +55,7 @@ export const defaultCallbacks: InternalOptions["callbacks"] = { /** Initialize all internal options and cookies. */ export async function init({ - authOptions: config, + config, providerId, action, url, @@ -61,10 +64,7 @@ export async function init({ csrfToken: reqCsrfToken, csrfDisabled, isPost, -}: InitParams): Promise<{ - options: InternalOptions - cookies: cookie.Cookie[] -}> { +}: InitParams): Promise { const logger = setLogger(config) const { providers, provider } = parseProviders({ url, providerId, config }) @@ -87,7 +87,7 @@ export async function init({ // User provided options are overridden by other options, // except for the options with special handling above - const options: InternalOptions = { + const internalConfig: InternalConfig = { debug: false, pages: {}, theme: { @@ -139,62 +139,59 @@ export async function init({ experimental: { ...config.experimental, }, + resCookies: [], } - // Init cookies - - const cookies: cookie.Cookie[] = [] - if (csrfDisabled) { - options.csrfTokenVerified = true + internalConfig.csrfTokenVerified = true } else { const { csrfToken, cookie: csrfCookie, csrfTokenVerified, } = await createCSRFToken({ - options, - cookieValue: reqCookies?.[options.cookies.csrfToken.name], + options: internalConfig, + cookieValue: reqCookies?.[internalConfig.cookies.csrfToken.name], isPost, bodyValue: reqCsrfToken, }) - options.csrfToken = csrfToken - options.csrfTokenVerified = csrfTokenVerified + internalConfig.csrfToken = csrfToken + internalConfig.csrfTokenVerified = csrfTokenVerified if (csrfCookie) { - cookies.push({ - name: options.cookies.csrfToken.name, + internalConfig.resCookies.push({ + name: internalConfig.cookies.csrfToken.name, value: csrfCookie, - options: options.cookies.csrfToken.options, + options: internalConfig.cookies.csrfToken.options, }) } } const { callbackUrl, callbackUrlCookie } = await createCallbackUrl({ - options, - cookieValue: reqCookies?.[options.cookies.callbackUrl.name], + options: internalConfig, + cookieValue: reqCookies?.[internalConfig.cookies.callbackUrl.name], paramValue: reqCallbackUrl, }) - options.callbackUrl = callbackUrl + internalConfig.callbackUrl = callbackUrl if (callbackUrlCookie) { - cookies.push({ - name: options.cookies.callbackUrl.name, + internalConfig.resCookies.push({ + name: internalConfig.cookies.callbackUrl.name, value: callbackUrlCookie, - options: options.cookies.callbackUrl.options, + options: internalConfig.cookies.callbackUrl.options, }) } - return { options, cookies } + return internalConfig } type Method = (...args: any[]) => Promise /** Wraps an object of methods and adds error handling. */ function eventsErrorHandler( - methods: Partial, + methods: Partial, logger: LoggerInstance -): Partial { +): Partial { return Object.keys(methods).reduce((acc, name) => { acc[name] = async (...args: any[]) => { try { diff --git a/packages/core/src/lib/pages/index.ts b/packages/core/src/lib/pages/index.ts index 0e83e203fb..12256bc55e 100644 --- a/packages/core/src/lib/pages/index.ts +++ b/packages/core/src/lib/pages/index.ts @@ -7,7 +7,7 @@ import VerifyRequestPage from "./verify-request.js" import { UnknownAction } from "../../errors.js" import type { - InternalOptions, + InternalConfig, RequestInternal, ResponseInternal, InternalProvider, @@ -40,7 +40,7 @@ type RenderPageParams = { cookies?: Cookie[] } & Partial< Pick< - InternalOptions, + InternalConfig, "url" | "callbackUrl" | "csrfToken" | "providers" | "theme" | "pages" > > @@ -53,7 +53,7 @@ export default function renderPage(params: RenderPageParams) { const { url, theme, query, cookies, pages, providers } = params return { - csrf(skip: boolean, options: InternalOptions, cookies: Cookie[]) { + csrf(skip: boolean, options: InternalConfig, cookies: Cookie[]) { if (!skip) { return { headers: { "Content-Type": "application/json" }, diff --git a/packages/core/src/lib/utils/callback-url.ts b/packages/core/src/lib/utils/callback-url.ts index 081335228a..772955ff35 100644 --- a/packages/core/src/lib/utils/callback-url.ts +++ b/packages/core/src/lib/utils/callback-url.ts @@ -1,7 +1,7 @@ -import type { InternalOptions } from "../../types.js" +import type { InternalConfig } from "../../types.js" interface CreateCallbackUrlParams { - options: InternalOptions + options: InternalConfig /** Try reading value from request body (POST) then from query param (GET) */ paramValue?: string cookieValue?: string diff --git a/packages/core/src/lib/utils/session.ts b/packages/core/src/lib/utils/session.ts index 11dc371933..62d8282679 100644 --- a/packages/core/src/lib/utils/session.ts +++ b/packages/core/src/lib/utils/session.ts @@ -1,11 +1,11 @@ -import type { InternalOptions, User } from "../../types.js" +import type { InternalConfig, User } from "../../types.js" import type { SessionStore } from "./cookie.js" /** * Returns the currently logged in user, if any. */ export async function getLoggedInUser( - options: InternalOptions, + options: InternalConfig, sessionStore: SessionStore ): Promise { const { diff --git a/packages/core/src/lib/utils/webauthn-utils.ts b/packages/core/src/lib/utils/webauthn-utils.ts index c0d8119bf0..fecd47cc07 100644 --- a/packages/core/src/lib/utils/webauthn-utils.ts +++ b/packages/core/src/lib/utils/webauthn-utils.ts @@ -3,7 +3,7 @@ import type { Account, Authenticator, Awaited, - InternalOptions, + InternalConfig, RequestInternal, ResponseInternal, User, @@ -39,7 +39,7 @@ export type WebAuthnRegister = "register" export type WebAuthnAuthenticate = "authenticate" export type WebAuthnAction = WebAuthnRegister | WebAuthnAuthenticate -type InternalOptionsWebAuthn = InternalOptions & { +type InternalOptionsWebAuthn = InternalConfig & { adapter: Required } export type WebAuthnOptionsResponseBody = @@ -318,7 +318,7 @@ export async function verifyAuthenticate( } export async function verifyRegister( - options: InternalOptions, + options: InternalConfig, request: RequestInternal, resCookies: Cookie[] ): Promise<{ account: Account; user: User; authenticator: Authenticator }> { @@ -480,7 +480,7 @@ async function getRegistrationOptions( } export function assertInternalOptionsWebAuthn( - options: InternalOptions + options: InternalConfig ): InternalOptionsWebAuthn { const { provider, adapter } = options diff --git a/packages/core/src/providers/webauthn.ts b/packages/core/src/providers/webauthn.ts index 2cbc56bd38..3f1ed3073c 100644 --- a/packages/core/src/providers/webauthn.ts +++ b/packages/core/src/providers/webauthn.ts @@ -15,7 +15,7 @@ import type { } from "@simplewebauthn/server" import type { - InternalOptions, + InternalConfig, RequestInternal, SemverString, User, @@ -45,7 +45,7 @@ type RelayingPartyArray = { } export type GetUserInfo = ( - options: InternalOptions, + options: InternalConfig, request: RequestInternal ) => Promise< | { user: User; exists: true } @@ -92,7 +92,7 @@ export interface WebAuthnConfig extends CommonProviderOptions { * Function that returns the relaying party for the current request. */ getRelayingParty: ( - options: InternalOptions, + options: InternalConfig, request: RequestInternal ) => RelayingParty /** @@ -266,7 +266,7 @@ const getUserInfo: GetUserInfo = async (options, request) => { */ function getRelayingParty( /** The options object containing the provider and URL information. */ - options: InternalOptions + options: InternalConfig ): RelayingParty { const { provider, url } = options const { relayingParty } = provider diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 05b5350da3..8ab9b0f1e6 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -394,7 +394,7 @@ export interface Authenticator { } /** @internal */ -export interface InternalOptions { +export interface InternalConfig { providers: InternalProvider[] url: URL action: AuthAction @@ -424,4 +424,6 @@ export interface InternalOptions { isOnRedirectProxy: boolean experimental: NonNullable basePath: string + /** These set of cookies should be serialized and sent to the client as `Set-Cookie` headers */ + resCookies: Cookie[] } From 4ed0c7ccb1b0f25409334cdff8f79b2bc7bf7da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:15:58 +0200 Subject: [PATCH 03/15] move session storage to InternalConfig --- packages/core/src/lib/index.ts | 19 ++++++------------- packages/core/src/lib/init.ts | 17 +++++++++++------ packages/core/src/types.ts | 3 ++- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 6e1272c9f0..920f28ad8f 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -1,5 +1,4 @@ import { UnknownAction } from "../errors.js" -import { SessionStore } from "./utils/cookie.js" import { init } from "./init.js" import renderPage from "./pages/index.js" import * as actions from "./actions/index.js" @@ -30,12 +29,6 @@ export async function AuthInternal( csrfDisabled, }) - const sessionStore = new SessionStore( - internalConfig.cookies.sessionToken, - request.cookies, - internalConfig.logger - ) - if (method === "GET") { const render = renderPage({ ...internalConfig, @@ -47,7 +40,7 @@ export async function AuthInternal( return await actions.callback( request, internalConfig, - sessionStore, + internalConfig.sessionStore, internalConfig.resCookies ) case "csrf": @@ -63,7 +56,7 @@ export async function AuthInternal( case "session": return await actions.session( internalConfig, - sessionStore, + internalConfig.sessionStore, internalConfig.resCookies ) case "signin": @@ -76,7 +69,7 @@ export async function AuthInternal( return await actions.webAuthnOptions( request, internalConfig, - sessionStore, + internalConfig.sessionStore, internalConfig.resCookies ) default: @@ -91,14 +84,14 @@ export async function AuthInternal( return await actions.callback( request, internalConfig, - sessionStore, + internalConfig.sessionStore, internalConfig.resCookies ) case "session": validateCSRF(action, csrfTokenVerified) return await actions.session( internalConfig, - sessionStore, + internalConfig.sessionStore, internalConfig.resCookies, true, request.body?.data @@ -115,7 +108,7 @@ export async function AuthInternal( validateCSRF(action, csrfTokenVerified) return await actions.signOut( internalConfig.resCookies, - sessionStore, + internalConfig.sessionStore, internalConfig ) default: diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index c3961b4a62..3100482d02 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -85,6 +85,11 @@ export async function init({ } } + const cookies = merge( + cookie.defaultCookies(config.useSecureCookies ?? url.protocol === "https:"), + config.cookies + ) + // User provided options are overridden by other options, // except for the options with special handling above const internalConfig: InternalConfig = { @@ -104,12 +109,7 @@ export async function init({ action, // @ts-expect-errors provider, - cookies: merge( - cookie.defaultCookies( - config.useSecureCookies ?? url.protocol === "https:" - ), - config.cookies - ), + cookies, providers, // Session options session: { @@ -140,6 +140,11 @@ export async function init({ ...config.experimental, }, resCookies: [], + sessionStore: new cookie.SessionStore( + cookies.sessionToken, + reqCookies, + logger + ), } if (csrfDisabled) { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8ab9b0f1e6..b4fc950d99 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -56,7 +56,7 @@ import type { TokenEndpointResponse } from "oauth4webapi" import type { Adapter } from "./adapters.js" import { AuthConfig } from "./index.js" import type { JWTOptions } from "./jwt.js" -import type { Cookie } from "./lib/utils/cookie.js" +import type { Cookie, SessionStore } from "./lib/utils/cookie.js" import type { LoggerInstance } from "./lib/utils/logger.js" import type { CredentialsConfig, @@ -426,4 +426,5 @@ export interface InternalConfig { basePath: string /** These set of cookies should be serialized and sent to the client as `Set-Cookie` headers */ resCookies: Cookie[] + sessionStore: SessionStore } From 2d4abe51843f7ee1b132e0a504124ef7018c75c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:20:17 +0200 Subject: [PATCH 04/15] refactor renderPage --- packages/core/src/lib/index.ts | 6 +----- packages/core/src/lib/pages/index.ts | 30 +++++++++++----------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 920f28ad8f..395a2e12b1 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -30,11 +30,7 @@ export async function AuthInternal( }) if (method === "GET") { - const render = renderPage({ - ...internalConfig, - query: request.query, - cookies: internalConfig.resCookies, - }) + const render = renderPage(request, internalConfig) switch (action) { case "callback": return await actions.callback( diff --git a/packages/core/src/lib/pages/index.ts b/packages/core/src/lib/pages/index.ts index 12256bc55e..f7ad450f69 100644 --- a/packages/core/src/lib/pages/index.ts +++ b/packages/core/src/lib/pages/index.ts @@ -35,22 +35,16 @@ function send({ } } -type RenderPageParams = { - query?: RequestInternal["query"] - cookies?: Cookie[] -} & Partial< - Pick< - InternalConfig, - "url" | "callbackUrl" | "csrfToken" | "providers" | "theme" | "pages" - > -> - /** * Unless the user defines their [own pages](https://authjs.dev/reference/core#pages), * we render a set of default ones, using Preact SSR. */ -export default function renderPage(params: RenderPageParams) { - const { url, theme, query, cookies, pages, providers } = params +export default function renderPage( + request: RequestInternal, + config: InternalConfig +) { + const { query } = request + const { url, theme, resCookies: cookies, pages, providers } = config return { csrf(skip: boolean, options: InternalConfig, cookies: Cookie[]) { @@ -86,7 +80,7 @@ export default function renderPage(params: RenderPageParams) { if (pages?.signIn) { let signinUrl = `${pages.signIn}${ pages.signIn.includes("?") ? "&" : "?" - }${new URLSearchParams({ callbackUrl: params.callbackUrl ?? "/" })}` + }${new URLSearchParams({ callbackUrl: config.callbackUrl ?? "/" })}` if (error) signinUrl = `${signinUrl}&${new URLSearchParams({ error })}` return { redirect: signinUrl, cookies } } @@ -111,9 +105,9 @@ export default function renderPage(params: RenderPageParams) { cookies, theme, html: SigninPage({ - csrfToken: params.csrfToken, + csrfToken: config.csrfToken, // We only want to render providers - providers: params.providers?.filter( + providers: config.providers?.filter( (provider) => // Always render oauth and email type providers ["email", "oauth", "oidc"].includes(provider.type) || @@ -124,8 +118,8 @@ export default function renderPage(params: RenderPageParams) { // Don't render other provider types false ), - callbackUrl: params.callbackUrl, - theme: params.theme, + callbackUrl: config.callbackUrl, + theme: config.theme, error, ...query, }), @@ -138,7 +132,7 @@ export default function renderPage(params: RenderPageParams) { return send({ cookies, theme, - html: SignoutPage({ csrfToken: params.csrfToken, url, theme }), + html: SignoutPage({ csrfToken: config.csrfToken, url, theme }), title: "Sign Out", }) }, From c168b170d3ebc83229b4432df84d2e56f7cde88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:27:08 +0200 Subject: [PATCH 05/15] simplify action render methods --- .../core/src/lib/actions/callback/index.ts | 69 ++++++++----------- packages/core/src/lib/actions/session.ts | 5 +- packages/core/src/lib/actions/signin/index.ts | 15 ++-- packages/core/src/lib/actions/signout.ts | 19 +++-- .../core/src/lib/actions/webauthn-options.ts | 12 ++-- packages/core/src/lib/index.ts | 56 +++------------ packages/core/src/lib/pages/index.ts | 13 ++-- 7 files changed, 70 insertions(+), 119 deletions(-) diff --git a/packages/core/src/lib/actions/callback/index.ts b/packages/core/src/lib/actions/callback/index.ts index ab4bb47c34..47f636fba9 100644 --- a/packages/core/src/lib/actions/callback/index.ts +++ b/packages/core/src/lib/actions/callback/index.ts @@ -22,7 +22,6 @@ import type { ResponseInternal, User, } from "../../../types.js" -import type { Cookie, SessionStore } from "../../utils/cookie.js" import { assertInternalOptionsWebAuthn, verifyAuthenticate, @@ -32,11 +31,9 @@ import { /** Handle callbacks from login services */ export async function callback( request: RequestInternal, - options: InternalConfig, - sessionStore: SessionStore, - cookies: Cookie[] + config: InternalConfig ): Promise { - if (!options.provider) + if (!config.provider) throw new InvalidProvider("Callback route called without provider") const { query, body, method, headers } = request const { @@ -50,7 +47,9 @@ export async function callback( callbacks, session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, logger, - } = options + resCookies: cookies, + sessionStore, + } = config const useJwtSession = sessionStrategy === "jwt" @@ -66,13 +65,13 @@ export async function callback( // If we have a state and we are on a redirect proxy, we try to parse it // and see if it contains a valid origin to redirect to. If it does, we // redirect the user to that origin with the original state. - if (options.isOnRedirectProxy && params?.state) { + if (config.isOnRedirectProxy && params?.state) { // NOTE: We rely on the state being encrypted using a shared secret // between the proxy and the original server. - const parsedState = await state.decode(params.state, options) + const parsedState = await state.decode(params.state, config) const shouldRedirect = parsedState?.origin && - new URL(parsedState.origin).origin !== options.url.origin + new URL(parsedState.origin).origin !== config.url.origin if (shouldRedirect) { const proxyRedirect = `${parsedState.origin}?${new URLSearchParams(params)}` logger.debug("Proxy redirecting to", proxyRedirect) @@ -83,7 +82,7 @@ export async function callback( const authorizationResult = await handleOAuth( params, request.cookies, - options + config ) if (authorizationResult.cookies.length) { @@ -126,7 +125,7 @@ export async function callback( account, profile: OAuthProfile, }, - options + config ) if (redirect) return { redirect, cookies } @@ -134,7 +133,7 @@ export async function callback( sessionStore.value, userFromProvider, account, - options + config ) if (useJwtSession) { @@ -157,7 +156,7 @@ export async function callback( if (token === null) { cookies.push(...sessionStore.clean()) } else { - const salt = options.cookies.sessionToken.name + const salt = config.cookies.sessionToken.name // Encode token const newToken = await jwt.encode({ ...jwt, token, salt }) @@ -173,10 +172,10 @@ export async function callback( } else { // Save Session Token in cookie cookies.push({ - name: options.cookies.sessionToken.name, + name: config.cookies.sessionToken.name, value: (session as AdapterSession).sessionToken, options: { - ...options.cookies.sessionToken.options, + ...config.cookies.sessionToken.options, expires: (session as AdapterSession).expires, }, }) @@ -215,7 +214,7 @@ export async function callback( throw e } - const secret = provider.secret ?? options.secret + const secret = provider.secret ?? config.secret // @ts-expect-error -- Verified in `assertConfig`. const invite = await adapter.useVerificationToken({ // @ts-expect-error User-land adapters might decide to omit the identifier during lookup @@ -247,7 +246,7 @@ export async function callback( provider: provider.id, } - const redirect = await handleAuthorized({ user, account }, options) + const redirect = await handleAuthorized({ user, account }, config) if (redirect) return { redirect, cookies } // Sign user in @@ -255,12 +254,7 @@ export async function callback( user: loggedInUser, session, isNewUser, - } = await handleLoginOrRegister( - sessionStore.value, - user, - account, - options - ) + } = await handleLoginOrRegister(sessionStore.value, user, account, config) if (useJwtSession) { const defaultToken = { @@ -281,7 +275,7 @@ export async function callback( if (token === null) { cookies.push(...sessionStore.clean()) } else { - const salt = options.cookies.sessionToken.name + const salt = config.cookies.sessionToken.name // Encode token const newToken = await jwt.encode({ ...jwt, token, salt }) @@ -297,10 +291,10 @@ export async function callback( } else { // Save Session Token in cookie cookies.push({ - name: options.cookies.sessionToken.name, + name: config.cookies.sessionToken.name, value: (session as AdapterSession).sessionToken, options: { - ...options.cookies.sessionToken.options, + ...config.cookies.sessionToken.options, expires: (session as AdapterSession).expires, }, }) @@ -347,7 +341,7 @@ export async function callback( const redirect = await handleAuthorized( { user, account, credentials }, - options + config ) if (redirect) return { redirect, cookies } @@ -370,7 +364,7 @@ export async function callback( if (token === null) { cookies.push(...sessionStore.clean()) } else { - const salt = options.cookies.sessionToken.name + const salt = config.cookies.sessionToken.name // Encode token const newToken = await jwt.encode({ ...jwt, token, salt }) @@ -399,7 +393,7 @@ export async function callback( } // Return an error if the adapter is missing or if the provider // is not a webauthn provider. - const localOptions = assertInternalOptionsWebAuthn(options) + const localOptions = assertInternalOptionsWebAuthn(config) // Verify request to get user, account and authenticator let user: User @@ -419,7 +413,7 @@ export async function callback( break } case "register": { - const verified = await verifyRegister(options, request, cookies) + const verified = await verifyRegister(config, request, cookies) user = verified.user account = verified.account @@ -430,7 +424,7 @@ export async function callback( } // Check if user is allowed to sign in - await handleAuthorized({ user, account }, options) + await handleAuthorized({ user, account }, config) // Sign user in, creating them and their account if needed const { @@ -438,12 +432,7 @@ export async function callback( isNewUser, session, account: currentAccount, - } = await handleLoginOrRegister( - sessionStore.value, - user, - account, - options - ) + } = await handleLoginOrRegister(sessionStore.value, user, account, config) if (!currentAccount) { // This is mostly for type checking. It should never actually happen. @@ -478,7 +467,7 @@ export async function callback( if (token === null) { cookies.push(...sessionStore.clean()) } else { - const salt = options.cookies.sessionToken.name + const salt = config.cookies.sessionToken.name // Encode token const newToken = await jwt.encode({ ...jwt, token, salt }) @@ -494,10 +483,10 @@ export async function callback( } else { // Save Session Token in cookie cookies.push({ - name: options.cookies.sessionToken.name, + name: config.cookies.sessionToken.name, value: (session as AdapterSession).sessionToken, options: { - ...options.cookies.sessionToken.options, + ...config.cookies.sessionToken.options, expires: (session as AdapterSession).expires, }, }) diff --git a/packages/core/src/lib/actions/session.ts b/packages/core/src/lib/actions/session.ts index ad7fabd1dd..be39089334 100644 --- a/packages/core/src/lib/actions/session.ts +++ b/packages/core/src/lib/actions/session.ts @@ -3,13 +3,10 @@ import { fromDate } from "../utils/date.js" import type { Adapter } from "../../adapters.js" import type { InternalConfig, ResponseInternal, Session } from "../../types.js" -import type { Cookie, SessionStore } from "../utils/cookie.js" /** Return a session object filtered via `callbacks.session` */ export async function session( options: InternalConfig, - sessionStore: SessionStore, - cookies: Cookie[], isUpdate?: boolean, newSession?: any ): Promise> { @@ -20,6 +17,8 @@ export async function session( callbacks, logger, session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, + resCookies: cookies, + sessionStore, } = options const response: ResponseInternal = { diff --git a/packages/core/src/lib/actions/signin/index.ts b/packages/core/src/lib/actions/signin/index.ts index a120bcfaef..e03ce4de58 100644 --- a/packages/core/src/lib/actions/signin/index.ts +++ b/packages/core/src/lib/actions/signin/index.ts @@ -1,7 +1,6 @@ import { getAuthorizationUrl } from "./authorization-url.js" import { sendToken } from "./send-token.js" -import type { Cookie } from "../../utils/cookie.js" import type { InternalConfig, RequestInternal, @@ -10,25 +9,25 @@ import type { export async function signIn( request: RequestInternal, - cookies: Cookie[], - options: InternalConfig + config: InternalConfig ): Promise { - const signInUrl = `${options.url.origin}${options.basePath}/signin` + const { resCookies: cookies } = config + const signInUrl = `${config.url.origin}${config.basePath}/signin` - if (!options.provider) return { redirect: signInUrl, cookies } + if (!config.provider) return { redirect: signInUrl, cookies } - switch (options.provider.type) { + switch (config.provider.type) { case "oauth": case "oidc": { const { redirect, cookies: authCookies } = await getAuthorizationUrl( request.query, - options + config ) if (authCookies) cookies.push(...authCookies) return { redirect, cookies } } case "email": { - const response = await sendToken(request, options) + const response = await sendToken(request, config) return { ...response, cookies } } default: diff --git a/packages/core/src/lib/actions/signout.ts b/packages/core/src/lib/actions/signout.ts index 3368c1167c..35c6160c0d 100644 --- a/packages/core/src/lib/actions/signout.ts +++ b/packages/core/src/lib/actions/signout.ts @@ -1,7 +1,6 @@ import { SignOutError } from "../../errors.js" import type { InternalConfig, ResponseInternal } from "../../types.js" -import type { Cookie, SessionStore } from "../utils/cookie.js" /** * Destroys the session. @@ -11,21 +10,27 @@ import type { Cookie, SessionStore } from "../utils/cookie.js" * {@link AuthConfig["events"].signOut} is emitted. */ export async function signOut( - cookies: Cookie[], - sessionStore: SessionStore, - options: InternalConfig + config: InternalConfig ): Promise { - const { jwt, events, callbackUrl: redirect, logger, session } = options + const { + jwt, + events, + callbackUrl: redirect, + logger, + session, + resCookies: cookies, + sessionStore, + } = config const sessionToken = sessionStore.value if (!sessionToken) return { redirect, cookies } try { if (session.strategy === "jwt") { - const salt = options.cookies.sessionToken.name + const salt = config.cookies.sessionToken.name const token = await jwt.decode({ ...jwt, token: sessionToken, salt }) await events.signOut?.({ token }) } else { - const session = await options.adapter?.deleteSession(sessionToken) + const session = await config.adapter?.deleteSession(sessionToken) await events.signOut?.({ session }) } } catch (e) { diff --git a/packages/core/src/lib/actions/webauthn-options.ts b/packages/core/src/lib/actions/webauthn-options.ts index fc08391a19..7a25272ab9 100644 --- a/packages/core/src/lib/actions/webauthn-options.ts +++ b/packages/core/src/lib/actions/webauthn-options.ts @@ -4,7 +4,6 @@ import type { ResponseInternal, User, } from "../../types.js" -import type { Cookie, SessionStore } from "../utils/cookie.js" import { getLoggedInUser } from "../utils/session.js" import { assertInternalOptionsWebAuthn, @@ -19,18 +18,17 @@ import { */ export async function webAuthnOptions( request: RequestInternal, - options: InternalConfig, - sessionStore: SessionStore, - cookies: Cookie[] + config: InternalConfig // @ts-expect-error issue with returning from a switch case ): Promise { // Return an error if the adapter is missing or if the provider // is not a webauthn provider. - const narrowOptions = assertInternalOptionsWebAuthn(options) + const narrowOptions = assertInternalOptionsWebAuthn(config) const { provider } = narrowOptions // Extract the action from the query parameters const { action } = (request.query ?? {}) as Record + const { resCookies: cookies, sessionStore } = config // Action must be either "register", "authenticate", or undefined if ( @@ -49,7 +47,7 @@ export async function webAuthnOptions( } // Get the user info from the session - const sessionUser = await getLoggedInUser(options, sessionStore) + const sessionUser = await getLoggedInUser(config, sessionStore) // Extract user info from request // If session user exists, we don't need to call getUserInfo @@ -58,7 +56,7 @@ export async function webAuthnOptions( user: sessionUser, exists: true, } - : await provider.getUserInfo(options, request) + : await provider.getUserInfo(config, request) const userInfo = getUserInfoResponse?.user diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 395a2e12b1..eff169642f 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -33,28 +33,15 @@ export async function AuthInternal( const render = renderPage(request, internalConfig) switch (action) { case "callback": - return await actions.callback( - request, - internalConfig, - internalConfig.sessionStore, - internalConfig.resCookies - ) + return await actions.callback(request, internalConfig) case "csrf": - return render.csrf( - csrfDisabled, - internalConfig, - internalConfig.resCookies - ) + return render.csrf(csrfDisabled) case "error": return render.error(error) case "providers": - return render.providers(internalConfig.providers) + return render.providers() case "session": - return await actions.session( - internalConfig, - internalConfig.sessionStore, - internalConfig.resCookies - ) + return await actions.session(internalConfig) case "signin": return render.signin(providerId, error) case "signout": @@ -62,12 +49,7 @@ export async function AuthInternal( case "verify-request": return render.verifyRequest() case "webauthn-options": - return await actions.webAuthnOptions( - request, - internalConfig, - internalConfig.sessionStore, - internalConfig.resCookies - ) + return await actions.webAuthnOptions(request, internalConfig) default: } } else { @@ -77,36 +59,16 @@ export async function AuthInternal( if (internalConfig.provider.type === "credentials") // Verified CSRF Token required for credentials providers only validateCSRF(action, csrfTokenVerified) - return await actions.callback( - request, - internalConfig, - internalConfig.sessionStore, - internalConfig.resCookies - ) + return await actions.callback(request, internalConfig) case "session": validateCSRF(action, csrfTokenVerified) - return await actions.session( - internalConfig, - internalConfig.sessionStore, - internalConfig.resCookies, - true, - request.body?.data - ) + return await actions.session(internalConfig, true, request.body?.data) case "signin": validateCSRF(action, csrfTokenVerified) - return await actions.signIn( - request, - internalConfig.resCookies, - internalConfig - ) - + return await actions.signIn(request, internalConfig) case "signout": validateCSRF(action, csrfTokenVerified) - return await actions.signOut( - internalConfig.resCookies, - internalConfig.sessionStore, - internalConfig - ) + return await actions.signOut(internalConfig) default: } } diff --git a/packages/core/src/lib/pages/index.ts b/packages/core/src/lib/pages/index.ts index f7ad450f69..f57ccd17b0 100644 --- a/packages/core/src/lib/pages/index.ts +++ b/packages/core/src/lib/pages/index.ts @@ -13,7 +13,6 @@ import type { InternalProvider, PublicProvider, } from "../../types.js" -import type { Cookie } from "../utils/cookie.js" function send({ html, @@ -47,23 +46,23 @@ export default function renderPage( const { url, theme, resCookies: cookies, pages, providers } = config return { - csrf(skip: boolean, options: InternalConfig, cookies: Cookie[]) { + csrf(skip: boolean) { if (!skip) { return { headers: { "Content-Type": "application/json" }, - body: { csrfToken: options.csrfToken }, + body: { csrfToken: config.csrfToken }, cookies, } } - options.logger.warn("csrf-disabled") + config.logger.warn("csrf-disabled") cookies.push({ - name: options.cookies.csrfToken.name, + name: config.cookies.csrfToken.name, value: "", - options: { ...options.cookies.csrfToken.options, maxAge: 0 }, + options: { ...config.cookies.csrfToken.options, maxAge: 0 }, }) return { status: 404, cookies } }, - providers(providers: InternalProvider[]) { + providers() { return { headers: { "Content-Type": "application/json" }, body: providers.reduce>( From a7f7110aa4c6265ce4518c4ec3313eb1d1526900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:32:11 +0200 Subject: [PATCH 06/15] options -> config --- .../src/lib/actions/callback/handle-login.ts | 20 +++--- .../core/src/lib/actions/callback/index.ts | 2 +- .../lib/actions/callback/oauth/callback.ts | 12 ++-- .../src/lib/actions/callback/oauth/checks.ts | 64 +++++++++---------- .../lib/actions/callback/oauth/csrf-token.ts | 8 +-- packages/core/src/lib/actions/session.ts | 12 ++-- .../lib/actions/signin/authorization-url.ts | 20 +++--- .../core/src/lib/actions/signin/send-token.ts | 12 ++-- .../core/src/lib/actions/webauthn-options.ts | 4 +- packages/core/src/lib/init.ts | 4 +- packages/core/src/lib/utils/callback-url.ts | 6 +- packages/core/src/lib/utils/session.ts | 9 ++- packages/core/src/lib/utils/webauthn-utils.ts | 17 +++-- packages/core/src/providers/webauthn.ts | 8 +-- 14 files changed, 98 insertions(+), 100 deletions(-) diff --git a/packages/core/src/lib/actions/callback/handle-login.ts b/packages/core/src/lib/actions/callback/handle-login.ts index 74624dd10f..65c446b40a 100644 --- a/packages/core/src/lib/actions/callback/handle-login.ts +++ b/packages/core/src/lib/actions/callback/handle-login.ts @@ -27,7 +27,7 @@ export async function handleLoginOrRegister( sessionToken: SessionToken, _profile: User | AdapterUser | { email: string }, _account: AdapterAccount | Account | null, - options: InternalConfig + config: InternalConfig ) { // Input validation if (!_account?.providerAccountId || !_account.type) @@ -40,7 +40,7 @@ export async function handleLoginOrRegister( jwt, events, session: { strategy: sessionStrategy, generateSessionToken }, - } = options + } = config // If no adapter is configured then we don't have a database and cannot // persist data; in this mode we just return a dummy session object. @@ -72,7 +72,7 @@ export async function handleLoginOrRegister( if (sessionToken) { if (useJwtSession) { try { - const salt = options.cookies.sessionToken.name + const salt = config.cookies.sessionToken.name session = await jwt.decode({ ...jwt, token: sessionToken, salt }) if (session && "sub" in session && session.sub) { user = await getUser(session.sub) @@ -121,7 +121,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: user.id, - expires: fromDate(options.session.maxAge), + expires: fromDate(config.session.maxAge), }) return { session, user, isNewUser } @@ -153,7 +153,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: userByAccount.id, - expires: fromDate(options.session.maxAge), + expires: fromDate(config.session.maxAge), }) const currentAccount: AdapterAccount = { @@ -212,7 +212,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: user.id, - expires: fromDate(options.session.maxAge), + expires: fromDate(config.session.maxAge), }) const currentAccount: AdapterAccount = { ...account, userId: user.id } @@ -246,12 +246,12 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: userByAccount.id, - expires: fromDate(options.session.maxAge), + expires: fromDate(config.session.maxAge), }) return { session, user: userByAccount, isNewUser } } else { - const { provider: p } = options as InternalConfig<"oauth" | "oidc"> + const { provider: p } = config as InternalConfig<"oauth" | "oidc"> const { type, provider, providerAccountId, userId, ...tokenSet } = account const defaults = { providerAccountId, provider, type, userId } account = Object.assign(p.account(tokenSet) ?? {}, defaults) @@ -287,7 +287,7 @@ export async function handleLoginOrRegister( ? await getUserByEmail(profile.email) : null if (userByEmail) { - const provider = options.provider as OAuthConfig + const provider = config.provider as OAuthConfig 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. @@ -326,7 +326,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: user.id, - expires: fromDate(options.session.maxAge), + expires: fromDate(config.session.maxAge), }) return { session, user, isNewUser } diff --git a/packages/core/src/lib/actions/callback/index.ts b/packages/core/src/lib/actions/callback/index.ts index 47f636fba9..9b9149e11a 100644 --- a/packages/core/src/lib/actions/callback/index.ts +++ b/packages/core/src/lib/actions/callback/index.ts @@ -413,7 +413,7 @@ export async function callback( break } case "register": { - const verified = await verifyRegister(config, request, cookies) + const verified = await verifyRegister(request, config) user = verified.user account = verified.account diff --git a/packages/core/src/lib/actions/callback/oauth/callback.ts b/packages/core/src/lib/actions/callback/oauth/callback.ts index 99dc35a317..9791ff0e3d 100644 --- a/packages/core/src/lib/actions/callback/oauth/callback.ts +++ b/packages/core/src/lib/actions/callback/oauth/callback.ts @@ -47,9 +47,9 @@ function clientSecretBasic(clientId: string, clientSecret: string) { export async function handleOAuth( params: RequestInternal["query"], cookies: RequestInternal["cookies"], - options: InternalConfig<"oauth" | "oidc"> + config: InternalConfig<"oauth" | "oidc"> ) { - const { logger, provider } = options + const { logger, provider } = config let as: o.AuthorizationServer @@ -127,7 +127,7 @@ export async function handleOAuth( const resCookies: Cookie[] = [] - const state = await checks.state.use(cookies, resCookies, options) + const state = await checks.state.use(cookies, resCookies, config) let codeGrantParams: URLSearchParams try { @@ -149,10 +149,10 @@ export async function handleOAuth( throw err } - const codeVerifier = await checks.pkce.use(cookies, resCookies, options) + const codeVerifier = await checks.pkce.use(cookies, resCookies, config) let redirect_uri = provider.callbackUrl - if (!options.isOnRedirectProxy && provider.redirectProxyUrl) { + if (!config.isOnRedirectProxy && provider.redirectProxyUrl) { redirect_uri = provider.redirectProxyUrl } @@ -217,7 +217,7 @@ export async function handleOAuth( client, codeGrantResponse, { - expectedNonce: await checks.nonce.use(cookies, resCookies, options), + expectedNonce: await checks.nonce.use(cookies, resCookies, config), requireIdToken, } ) diff --git a/packages/core/src/lib/actions/callback/oauth/checks.ts b/packages/core/src/lib/actions/callback/oauth/checks.ts index e80d9c254f..0f176d1568 100644 --- a/packages/core/src/lib/actions/callback/oauth/checks.ts +++ b/packages/core/src/lib/actions/callback/oauth/checks.ts @@ -23,9 +23,9 @@ const COOKIE_TTL = 60 * 15 // 15 minutes async function sealCookie( name: keyof CookiesOptions, payload: string, - options: InternalConfig<"oauth" | "oidc" | WebAuthnProviderType> + config: InternalConfig<"oauth" | "oidc" | WebAuthnProviderType> ): Promise { - const { cookies, logger } = options + const { cookies, logger } = config const cookie = cookies[name] const expires = new Date() expires.setTime(expires.getTime() + COOKIE_TTL * 1000) @@ -38,7 +38,7 @@ async function sealCookie( }) const encoded = await encode({ - ...options.jwt, + ...config.jwt, maxAge: COOKIE_TTL, token: { value: payload } satisfies CookiePayload, salt: cookie.name, @@ -50,10 +50,10 @@ async function sealCookie( async function parseCookie( name: keyof CookiesOptions, value: string | undefined, - options: InternalConfig + config: InternalConfig ): Promise { try { - const { logger, cookies, jwt } = options + const { logger, cookies, jwt } = config logger.debug(`PARSE_${name.toUpperCase()}`, { cookie: value }) if (!value) throw new InvalidCheck(`${name} cookie was missing`) @@ -73,10 +73,10 @@ async function parseCookie( function clearCookie( name: keyof CookiesOptions, - options: InternalConfig, + config: InternalConfig, resCookies: Cookie[] ) { - const { logger, cookies } = options + const { logger, cookies } = config const cookie = cookies[name] logger.debug(`CLEAR_${name.toUpperCase()}`, { cookie }) resCookies.push({ @@ -93,14 +93,14 @@ function useCookie( return async function ( cookies: RequestInternal["cookies"], resCookies: Cookie[], - options: InternalConfig<"oidc"> + config: InternalConfig<"oidc"> ) { - const { provider, logger } = options + const { provider, logger } = config if (!provider?.checks?.includes(check)) return - const cookieValue = cookies?.[options.cookies[name].name] + const cookieValue = cookies?.[config.cookies[name].name] logger.debug(`USE_${name.toUpperCase()}`, { value: cookieValue }) - const parsed = await parseCookie(name, cookieValue, options) - clearCookie(name, options, resCookies) + const parsed = await parseCookie(name, cookieValue, config) + clearCookie(name, config, resCookies) return parsed } } @@ -111,10 +111,10 @@ function useCookie( */ export const pkce = { /** Creates a PKCE code challenge and verifier pair. The verifier in stored in the cookie. */ - async create(options: InternalConfig<"oauth">) { + async create(config: InternalConfig<"oauth">) { const code_verifier = o.generateRandomCodeVerifier() const value = await o.calculatePKCECodeChallenge(code_verifier) - const cookie = await sealCookie("pkceCodeVerifier", code_verifier, options) + const cookie = await sealCookie("pkceCodeVerifier", code_verifier, config) return { cookie, value } }, /** @@ -139,8 +139,8 @@ const encodedStateSalt = "encodedState" */ export const state = { /** Creates a state cookie with an optionally encoded body. */ - async create(options: InternalConfig<"oauth">, origin?: string) { - const { provider } = options + async create(config: InternalConfig<"oauth">, origin?: string) { + const { provider } = config if (!provider.checks.includes("state")) { if (origin) { throw new InvalidCheck( @@ -156,12 +156,12 @@ export const state = { random: o.generateRandomState(), } satisfies EncodedState const value = await encode({ - secret: options.jwt.secret, + secret: config.jwt.secret, token: payload, salt: encodedStateSalt, maxAge: STATE_MAX_AGE, }) - const cookie = await sealCookie("state", value, options) + const cookie = await sealCookie("state", value, config) return { cookie, value } }, @@ -172,11 +172,11 @@ export const state = { */ use: useCookie("state", "state"), /** Decodes the state. If it could not be decoded, it throws an error. */ - async decode(state: string, options: InternalConfig) { + async decode(state: string, config: InternalConfig) { try { - options.logger.debug("DECODE_STATE", { state }) + config.logger.debug("DECODE_STATE", { state }) const payload = await decode({ - secret: options.jwt.secret, + secret: config.jwt.secret, token: state, salt: encodedStateSalt, }) @@ -189,10 +189,10 @@ export const state = { } export const nonce = { - async create(options: InternalConfig<"oidc">) { - if (!options.provider.checks.includes("nonce")) return + async create(config: InternalConfig<"oidc">) { + if (!config.provider.checks.includes("nonce")) return const value = o.generateRandomNonce() - const cookie = await sealCookie("nonce", value, options) + const cookie = await sealCookie("nonce", value, config) return { cookie, value } }, /** @@ -215,7 +215,7 @@ interface WebAuthnChallengePayload { const webauthnChallengeSalt = "encodedWebauthnChallenge" export const webauthnChallenge = { async create( - options: InternalConfig, + config: InternalConfig, challenge: string, registerData?: User ) { @@ -223,33 +223,33 @@ export const webauthnChallenge = { cookie: await sealCookie( "webauthnChallenge", await encode({ - secret: options.jwt.secret, + secret: config.jwt.secret, token: { challenge, registerData } satisfies WebAuthnChallengePayload, salt: webauthnChallengeSalt, maxAge: WEBAUTHN_CHALLENGE_MAX_AGE, }), - options + config ), } }, /** Returns WebAuthn challenge if present. */ async use( - options: InternalConfig, + config: InternalConfig, cookies: RequestInternal["cookies"], resCookies: Cookie[] ): Promise { - const cookieValue = cookies?.[options.cookies.webauthnChallenge.name] + const cookieValue = cookies?.[config.cookies.webauthnChallenge.name] - const parsed = await parseCookie("webauthnChallenge", cookieValue, options) + const parsed = await parseCookie("webauthnChallenge", cookieValue, config) const payload = await decode({ - secret: options.jwt.secret, + secret: config.jwt.secret, token: parsed, salt: webauthnChallengeSalt, }) // Clear the WebAuthn challenge cookie after use - clearCookie("webauthnChallenge", options, resCookies) + clearCookie("webauthnChallenge", config, resCookies) if (!payload) throw new InvalidCheck("WebAuthn challenge was missing") diff --git a/packages/core/src/lib/actions/callback/oauth/csrf-token.ts b/packages/core/src/lib/actions/callback/oauth/csrf-token.ts index 2f2dab77c8..84444fd91c 100644 --- a/packages/core/src/lib/actions/callback/oauth/csrf-token.ts +++ b/packages/core/src/lib/actions/callback/oauth/csrf-token.ts @@ -3,7 +3,7 @@ import { createHash, randomString } from "../../../utils/web.js" import type { AuthAction, InternalConfig } from "../../../../types.js" import { MissingCSRF } from "../../../../errors.js" interface CreateCSRFTokenParams { - options: InternalConfig + config: InternalConfig cookieValue?: string isPost: boolean bodyValue?: string @@ -24,7 +24,7 @@ interface CreateCSRFTokenParams { * https://owasp.org/www-chapter-london/assets/slides/David_Johansson-Double_Defeat_of_Double-Submit_Cookie.pdf */ export async function createCSRFToken({ - options, + config, cookieValue, isPost, bodyValue, @@ -33,7 +33,7 @@ export async function createCSRFToken({ const [csrfToken, csrfTokenHash] = cookieValue.split("|") const expectedCsrfTokenHash = await createHash( - `${csrfToken}${options.secret}` + `${csrfToken}${config.secret}` ) if (csrfTokenHash === expectedCsrfTokenHash) { @@ -48,7 +48,7 @@ export async function createCSRFToken({ // New CSRF token const csrfToken = randomString(32) - const csrfTokenHash = await createHash(`${csrfToken}${options.secret}`) + const csrfTokenHash = await createHash(`${csrfToken}${config.secret}`) const cookie = `${csrfToken}|${csrfTokenHash}` return { cookie, csrfToken } diff --git a/packages/core/src/lib/actions/session.ts b/packages/core/src/lib/actions/session.ts index be39089334..b74cb9b5fe 100644 --- a/packages/core/src/lib/actions/session.ts +++ b/packages/core/src/lib/actions/session.ts @@ -6,7 +6,7 @@ import type { InternalConfig, ResponseInternal, Session } from "../../types.js" /** Return a session object filtered via `callbacks.session` */ export async function session( - options: InternalConfig, + config: InternalConfig, isUpdate?: boolean, newSession?: any ): Promise> { @@ -19,7 +19,7 @@ export async function session( session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, resCookies: cookies, sessionStore, - } = options + } = config const response: ResponseInternal = { body: null, @@ -33,7 +33,7 @@ export async function session( if (sessionStrategy === "jwt") { try { - const salt = options.cookies.sessionToken.name + const salt = config.cookies.sessionToken.name const payload = await jwt.decode({ ...jwt, token: sessionToken, salt }) if (!payload) throw new Error("Invalid JWT") @@ -101,7 +101,7 @@ export async function session( if (userAndSession) { const { user, session } = userAndSession - const sessionUpdateAge = options.session.updateAge + const sessionUpdateAge = config.session.updateAge // Calculate last updated date to throttle write updates to database // Formula: ({expiry date} - sessionMaxAge) + sessionUpdateAge // e.g. ({expiry date} - 30 days) + 1 hour @@ -136,10 +136,10 @@ export async function session( // Set cookie again to update expiry response.cookies?.push({ - name: options.cookies.sessionToken.name, + name: config.cookies.sessionToken.name, value: sessionToken, options: { - ...options.cookies.sessionToken.options, + ...config.cookies.sessionToken.options, expires: newExpires, }, }) diff --git a/packages/core/src/lib/actions/signin/authorization-url.ts b/packages/core/src/lib/actions/signin/authorization-url.ts index 373c8137f5..a254097995 100644 --- a/packages/core/src/lib/actions/signin/authorization-url.ts +++ b/packages/core/src/lib/actions/signin/authorization-url.ts @@ -12,9 +12,9 @@ import { customFetch } from "../../symbols.js" */ export async function getAuthorizationUrl( query: RequestInternal["query"], - options: InternalConfig<"oauth" | "oidc"> + config: InternalConfig<"oauth" | "oidc"> ) { - const { logger, provider } = options + const { logger, provider } = config let url = provider.authorization?.url let as: o.AuthorizationServer | undefined @@ -45,7 +45,7 @@ export async function getAuthorizationUrl( let redirect_uri: string = provider.callbackUrl let data: string | undefined - if (!options.isOnRedirectProxy && provider.redirectProxyUrl) { + if (!config.isOnRedirectProxy && provider.redirectProxyUrl) { redirect_uri = provider.redirectProxyUrl data = provider.callbackUrl logger.debug("using redirect proxy", { redirect_uri, data }) @@ -73,13 +73,13 @@ export async function getAuthorizationUrl( provider.authorization?.url.searchParams.get("response_mode") === "form_post" ) { - options.cookies.state.options.sameSite = "none" - options.cookies.state.options.secure = true - options.cookies.nonce.options.sameSite = "none" - options.cookies.nonce.options.secure = true + config.cookies.state.options.sameSite = "none" + config.cookies.state.options.secure = true + config.cookies.nonce.options.sameSite = "none" + config.cookies.nonce.options.secure = true } - const state = await checks.state.create(options, data) + const state = await checks.state.create(config, data) if (state) { authParams.set("state", state.value) cookies.push(state.cookie) @@ -91,14 +91,14 @@ export async function getAuthorizationUrl( // a random `nonce` must be used for CSRF protection. if (provider.type === "oidc") provider.checks = ["nonce"] } else { - const { value, cookie } = await checks.pkce.create(options) + const { value, cookie } = await checks.pkce.create(config) authParams.set("code_challenge", value) authParams.set("code_challenge_method", "S256") cookies.push(cookie) } } - const nonce = await checks.nonce.create(options) + const nonce = await checks.nonce.create(config) if (nonce) { authParams.set("nonce", nonce.value) cookies.push(nonce.cookie) diff --git a/packages/core/src/lib/actions/signin/send-token.ts b/packages/core/src/lib/actions/signin/send-token.ts index c6702b4e79..4055996f02 100644 --- a/packages/core/src/lib/actions/signin/send-token.ts +++ b/packages/core/src/lib/actions/signin/send-token.ts @@ -11,10 +11,10 @@ import type { Account } from "../../../types.js" */ export async function sendToken( request: RequestInternal, - options: InternalConfig<"email"> + config: InternalConfig<"email"> ) { const { body } = request - const { provider, callbacks, adapter } = options + const { provider, callbacks, adapter } = config const normalizer = provider.normalizeIdentifier ?? defaultNormalizer const email = normalizer(body?.email) @@ -43,12 +43,12 @@ export async function sendToken( return { redirect: await callbacks.redirect({ url: authorized, - baseUrl: options.url.origin, + baseUrl: config.url.origin, }), } } - const { callbackUrl, theme } = options + const { callbackUrl, theme } = config const token = (await provider.generateVerificationToken?.()) ?? randomString(32) @@ -57,9 +57,9 @@ export async function sendToken( Date.now() + (provider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000 ) - const secret = provider.secret ?? options.secret + const secret = provider.secret ?? config.secret - const baseUrl = new URL(options.basePath, options.url.origin) + const baseUrl = new URL(config.basePath, config.url.origin) const sendRequest = provider.sendVerificationRequest({ identifier: email, diff --git a/packages/core/src/lib/actions/webauthn-options.ts b/packages/core/src/lib/actions/webauthn-options.ts index 7a25272ab9..07695cacf2 100644 --- a/packages/core/src/lib/actions/webauthn-options.ts +++ b/packages/core/src/lib/actions/webauthn-options.ts @@ -28,7 +28,7 @@ export async function webAuthnOptions( // Extract the action from the query parameters const { action } = (request.query ?? {}) as Record - const { resCookies: cookies, sessionStore } = config + const { resCookies: cookies } = config // Action must be either "register", "authenticate", or undefined if ( @@ -47,7 +47,7 @@ export async function webAuthnOptions( } // Get the user info from the session - const sessionUser = await getLoggedInUser(config, sessionStore) + const sessionUser = await getLoggedInUser(config) // Extract user info from request // If session user exists, we don't need to call getUserInfo diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index 3100482d02..01fb7cdad2 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -155,7 +155,7 @@ export async function init({ cookie: csrfCookie, csrfTokenVerified, } = await createCSRFToken({ - options: internalConfig, + config: internalConfig, cookieValue: reqCookies?.[internalConfig.cookies.csrfToken.name], isPost, bodyValue: reqCsrfToken, @@ -174,7 +174,7 @@ export async function init({ } const { callbackUrl, callbackUrlCookie } = await createCallbackUrl({ - options: internalConfig, + config: internalConfig, cookieValue: reqCookies?.[internalConfig.cookies.callbackUrl.name], paramValue: reqCallbackUrl, }) diff --git a/packages/core/src/lib/utils/callback-url.ts b/packages/core/src/lib/utils/callback-url.ts index 772955ff35..e9ab79b4db 100644 --- a/packages/core/src/lib/utils/callback-url.ts +++ b/packages/core/src/lib/utils/callback-url.ts @@ -1,7 +1,7 @@ import type { InternalConfig } from "../../types.js" interface CreateCallbackUrlParams { - options: InternalConfig + config: InternalConfig /** Try reading value from request body (POST) then from query param (GET) */ paramValue?: string cookieValue?: string @@ -12,11 +12,11 @@ interface CreateCallbackUrlParams { * and add it to `req.options.callbackUrl`. */ export async function createCallbackUrl({ - options, + config, paramValue, cookieValue, }: CreateCallbackUrlParams) { - const { url, callbacks } = options + const { url, callbacks } = config let callbackUrl = url.origin diff --git a/packages/core/src/lib/utils/session.ts b/packages/core/src/lib/utils/session.ts index 62d8282679..a7392fbf98 100644 --- a/packages/core/src/lib/utils/session.ts +++ b/packages/core/src/lib/utils/session.ts @@ -1,25 +1,24 @@ import type { InternalConfig, User } from "../../types.js" -import type { SessionStore } from "./cookie.js" /** * Returns the currently logged in user, if any. */ export async function getLoggedInUser( - options: InternalConfig, - sessionStore: SessionStore + config: InternalConfig ): Promise { const { adapter, jwt, session: { strategy: sessionStrategy }, - } = options + sessionStore, + } = config const sessionToken = sessionStore.value if (!sessionToken) return null // Try to decode JWT if (sessionStrategy === "jwt") { - const salt = options.cookies.sessionToken.name + const salt = config.cookies.sessionToken.name const payload = await jwt.decode({ ...jwt, token: sessionToken, salt }) if (payload && payload.sub) { diff --git a/packages/core/src/lib/utils/webauthn-utils.ts b/packages/core/src/lib/utils/webauthn-utils.ts index fecd47cc07..2b457a7149 100644 --- a/packages/core/src/lib/utils/webauthn-utils.ts +++ b/packages/core/src/lib/utils/webauthn-utils.ts @@ -318,11 +318,10 @@ export async function verifyAuthenticate( } export async function verifyRegister( - options: InternalConfig, request: RequestInternal, - resCookies: Cookie[] + config: InternalConfig ): Promise<{ account: Account; user: User; authenticator: Authenticator }> { - const { provider } = options + const { provider, resCookies } = config // Get WebAuthn response from request body const data = @@ -340,7 +339,7 @@ export async function verifyRegister( // Get challenge from request cookies const { challenge: expectedChallenge, registerData: user } = - await webauthnChallenge.use(options, request.cookies, resCookies) + await webauthnChallenge.use(config, request.cookies, resCookies) if (!user) { throw new AuthError( "Missing user registration data in WebAuthn challenge cookie" @@ -350,7 +349,7 @@ export async function verifyRegister( // Verify the response let verification: VerifiedRegistrationResponse try { - const relayingParty = provider.getRelayingParty(options, request) + const relayingParty = provider.getRelayingParty(config, request) verification = await provider.simpleWebAuthn.verifyRegistrationResponse({ ...provider.verifyRegistrationOptions, expectedChallenge, @@ -372,7 +371,7 @@ export async function verifyRegister( // Build a new account const account = { providerAccountId: toBase64(verification.registrationInfo.credentialID), - provider: options.provider.id, + provider: config.provider.id, type: provider.type, } @@ -480,9 +479,9 @@ async function getRegistrationOptions( } export function assertInternalOptionsWebAuthn( - options: InternalConfig + config: InternalConfig ): InternalOptionsWebAuthn { - const { provider, adapter } = options + const { provider, adapter } = config // Adapter is required for WebAuthn if (!adapter) @@ -492,7 +491,7 @@ export function assertInternalOptionsWebAuthn( throw new InvalidProvider("Provider must be WebAuthn") } // Narrow the options type for typed usage later - return { ...options, provider, adapter } + return { ...config, provider, adapter } } function fromAdapterAuthenticator( diff --git a/packages/core/src/providers/webauthn.ts b/packages/core/src/providers/webauthn.ts index 3f1ed3073c..45f8fbb0a2 100644 --- a/packages/core/src/providers/webauthn.ts +++ b/packages/core/src/providers/webauthn.ts @@ -45,7 +45,7 @@ type RelayingPartyArray = { } export type GetUserInfo = ( - options: InternalConfig, + config: InternalConfig, request: RequestInternal ) => Promise< | { user: User; exists: true } @@ -92,7 +92,7 @@ export interface WebAuthnConfig extends CommonProviderOptions { * Function that returns the relaying party for the current request. */ getRelayingParty: ( - options: InternalConfig, + config: InternalConfig, request: RequestInternal ) => RelayingParty /** @@ -266,9 +266,9 @@ const getUserInfo: GetUserInfo = async (options, request) => { */ function getRelayingParty( /** The options object containing the provider and URL information. */ - options: InternalConfig + config: InternalConfig ): RelayingParty { - const { provider, url } = options + const { provider, url } = config const { relayingParty } = provider const id = Array.isArray(relayingParty?.id) From 3a25cf65f5ffd04ac0c409d9481f92201dbe9725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:40:19 +0200 Subject: [PATCH 07/15] more fixes --- packages/core/src/lib/index.ts | 30 ++++++++++++++-------------- packages/core/src/lib/pages/index.ts | 21 +++++++++---------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index eff169642f..72d3320a59 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -11,14 +11,14 @@ import { skipCSRFCheck } from "./symbols.js" /** @internal */ export async function AuthInternal( request: RequestInternal, - config: AuthConfig + userConfig: AuthConfig ): Promise { const { action, providerId, error, method } = request - const csrfDisabled = config.skipCSRFCheck === skipCSRFCheck + const csrfDisabled = userConfig.skipCSRFCheck === skipCSRFCheck - const internalConfig = await init({ - config, + const config = await init({ + config: userConfig, action, providerId, url: request.url, @@ -30,10 +30,10 @@ export async function AuthInternal( }) if (method === "GET") { - const render = renderPage(request, internalConfig) + const render = renderPage(config) switch (action) { case "callback": - return await actions.callback(request, internalConfig) + return await actions.callback(request, config) case "csrf": return render.csrf(csrfDisabled) case "error": @@ -41,34 +41,34 @@ export async function AuthInternal( case "providers": return render.providers() case "session": - return await actions.session(internalConfig) + return await actions.session(config) case "signin": - return render.signin(providerId, error) + return render.signin(request) case "signout": return render.signout() case "verify-request": return render.verifyRequest() case "webauthn-options": - return await actions.webAuthnOptions(request, internalConfig) + return await actions.webAuthnOptions(request, config) default: } } else { - const { csrfTokenVerified } = internalConfig + const { csrfTokenVerified } = config switch (action) { case "callback": - if (internalConfig.provider.type === "credentials") + if (config.provider.type === "credentials") // Verified CSRF Token required for credentials providers only validateCSRF(action, csrfTokenVerified) - return await actions.callback(request, internalConfig) + return await actions.callback(request, config) case "session": validateCSRF(action, csrfTokenVerified) - return await actions.session(internalConfig, true, request.body?.data) + return await actions.session(config, true, request.body?.data) case "signin": validateCSRF(action, csrfTokenVerified) - return await actions.signIn(request, internalConfig) + return await actions.signIn(request, config) case "signout": validateCSRF(action, csrfTokenVerified) - return await actions.signOut(internalConfig) + return await actions.signOut(config) default: } } diff --git a/packages/core/src/lib/pages/index.ts b/packages/core/src/lib/pages/index.ts index f57ccd17b0..f93b30bcf6 100644 --- a/packages/core/src/lib/pages/index.ts +++ b/packages/core/src/lib/pages/index.ts @@ -38,11 +38,7 @@ function send({ * Unless the user defines their [own pages](https://authjs.dev/reference/core#pages), * we render a set of default ones, using Preact SSR. */ -export default function renderPage( - request: RequestInternal, - config: InternalConfig -) { - const { query } = request +export default function renderPage(config: Partial) { const { url, theme, resCookies: cookies, pages, providers } = config return { @@ -54,18 +50,18 @@ export default function renderPage( cookies, } } - config.logger.warn("csrf-disabled") - cookies.push({ - name: config.cookies.csrfToken.name, + config.logger?.warn("csrf-disabled") + cookies?.push({ + name: config.cookies!.csrfToken.name, value: "", - options: { ...config.cookies.csrfToken.options, maxAge: 0 }, + options: { ...config.cookies?.csrfToken.options, maxAge: 0 }, }) return { status: 404, cookies } }, providers() { return { headers: { "Content-Type": "application/json" }, - body: providers.reduce>( + body: providers?.reduce>( (acc, { id, name, type, signinUrl, callbackUrl }) => { acc[id] = { id, name, type, signinUrl, callbackUrl } return acc @@ -74,7 +70,8 @@ export default function renderPage( ), } }, - signin(providerId?: string, error?: any) { + signin(request: RequestInternal) { + const { error, query, providerId } = request if (providerId) throw new UnknownAction("Unsupported action") if (pages?.signIn) { let signinUrl = `${pages.signIn}${ @@ -119,7 +116,7 @@ export default function renderPage( ), callbackUrl: config.callbackUrl, theme: config.theme, - error, + error: error as any, ...query, }), title: "Sign In", From 6577129e6104b008ac458ddfee7a8573b3168024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:48:56 +0200 Subject: [PATCH 08/15] simplify init --- packages/core/src/lib/index.ts | 36 ++++++---------- packages/core/src/lib/init.ts | 75 +++++++++++++++------------------- 2 files changed, 47 insertions(+), 64 deletions(-) diff --git a/packages/core/src/lib/index.ts b/packages/core/src/lib/index.ts index 72d3320a59..a589259578 100644 --- a/packages/core/src/lib/index.ts +++ b/packages/core/src/lib/index.ts @@ -13,27 +13,17 @@ export async function AuthInternal( request: RequestInternal, userConfig: AuthConfig ): Promise { - const { action, providerId, error, method } = request + const { action, error, method } = request - const csrfDisabled = userConfig.skipCSRFCheck === skipCSRFCheck + const internalConfig = await init(request, userConfig) - const config = await init({ - config: userConfig, - action, - providerId, - url: request.url, - callbackUrl: request.body?.callbackUrl ?? request.query?.callbackUrl, - csrfToken: request.body?.csrfToken, - cookies: request.cookies, - isPost: method === "POST", - csrfDisabled, - }) + const csrfDisabled = userConfig.skipCSRFCheck === skipCSRFCheck if (method === "GET") { - const render = renderPage(config) + const render = renderPage(internalConfig) switch (action) { case "callback": - return await actions.callback(request, config) + return await actions.callback(request, internalConfig) case "csrf": return render.csrf(csrfDisabled) case "error": @@ -41,7 +31,7 @@ export async function AuthInternal( case "providers": return render.providers() case "session": - return await actions.session(config) + return await actions.session(internalConfig) case "signin": return render.signin(request) case "signout": @@ -49,26 +39,26 @@ export async function AuthInternal( case "verify-request": return render.verifyRequest() case "webauthn-options": - return await actions.webAuthnOptions(request, config) + return await actions.webAuthnOptions(request, internalConfig) default: } } else { - const { csrfTokenVerified } = config + const { csrfTokenVerified } = internalConfig switch (action) { case "callback": - if (config.provider.type === "credentials") + if (internalConfig.provider.type === "credentials") // Verified CSRF Token required for credentials providers only validateCSRF(action, csrfTokenVerified) - return await actions.callback(request, config) + return await actions.callback(request, internalConfig) case "session": validateCSRF(action, csrfTokenVerified) - return await actions.session(config, true, request.body?.data) + return await actions.session(internalConfig, true, request.body?.data) case "signin": validateCSRF(action, csrfTokenVerified) - return await actions.signIn(request, config) + return await actions.signIn(request, internalConfig) case "signout": validateCSRF(action, csrfTokenVerified) - return await actions.signOut(config) + return await actions.signOut(internalConfig) default: } } diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index 01fb7cdad2..f4391e852c 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -7,6 +7,7 @@ import { AdapterError, EventError } from "../errors.js" import parseProviders from "./utils/providers.js" import { setLogger, type LoggerInstance } from "./utils/logger.js" import { merge } from "./utils/merge.js" +import { skipCSRFCheck } from "./symbols.js" import type { InternalConfig as InternalConfig, @@ -14,21 +15,6 @@ import type { } from "../types.js" import type { AuthConfig } from "../index.js" -interface InitParams { - url: URL - config: AuthConfig - providerId?: string - action: InternalConfig["action"] - /** Callback URL value extracted from the incoming request. */ - callbackUrl?: string - /** CSRF token value extracted from the incoming request. From body if POST, from query if GET */ - csrfToken?: string - /** Is the incoming request a POST request? */ - csrfDisabled: boolean - isPost: boolean - cookies: RequestInternal["cookies"] -} - export const defaultCallbacks: InternalConfig["callbacks"] = { signIn() { return true @@ -53,20 +39,25 @@ export const defaultCallbacks: InternalConfig["callbacks"] = { }, } -/** Initialize all internal options and cookies. */ -export async function init({ - config, - providerId, - action, - url, - cookies: reqCookies, - callbackUrl: reqCallbackUrl, - csrfToken: reqCsrfToken, - csrfDisabled, - isPost, -}: InitParams): Promise { - const logger = setLogger(config) - const { providers, provider } = parseProviders({ url, providerId, config }) +/** Initialize all internal options. */ +export async function init( + request: RequestInternal, + userConfig: AuthConfig +): Promise { + const logger = setLogger(userConfig) + + const { providerId, action, url, cookies: reqCookies } = request + const isPost = request.method === "POST" + const csrfDisabled = userConfig.skipCSRFCheck === skipCSRFCheck + + const reqCallbackUrl = request.body?.callbackUrl ?? request.query?.callbackUrl + const reqCsrfToken = request.body?.csrfToken + + const { providers, provider } = parseProviders({ + url, + providerId, + config: userConfig, + }) const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default @@ -86,8 +77,10 @@ export async function init({ } const cookies = merge( - cookie.defaultCookies(config.useSecureCookies ?? url.protocol === "https:"), - config.cookies + cookie.defaultCookies( + userConfig.useSecureCookies ?? url.protocol === "https:" + ), + userConfig.cookies ) // User provided options are overridden by other options, @@ -102,7 +95,7 @@ export async function init({ buttonText: "", }, // Custom options override defaults - ...config, + ...userConfig, // These computed settings can have values in userOptions but we override them // and are request-specific. url, @@ -114,30 +107,30 @@ export async function init({ // Session options session: { // If no adapter specified, force use of JSON Web Tokens (stateless) - strategy: config.adapter ? "database" : "jwt", + strategy: userConfig.adapter ? "database" : "jwt", maxAge, updateAge: 24 * 60 * 60, generateSessionToken: () => crypto.randomUUID(), - ...config.session, + ...userConfig.session, }, // JWT options jwt: { - secret: config.secret!, // Asserted in assert.ts - maxAge: config.session?.maxAge ?? maxAge, // default to same as `session.maxAge` + secret: userConfig.secret!, // Asserted in assert.ts + maxAge: userConfig.session?.maxAge ?? maxAge, // default to same as `session.maxAge` encode: jwt.encode, decode: jwt.decode, - ...config.jwt, + ...userConfig.jwt, }, // Event messages - events: eventsErrorHandler(config.events ?? {}, logger), - adapter: adapterErrorHandler(config.adapter, logger), + events: eventsErrorHandler(userConfig.events ?? {}, logger), + adapter: adapterErrorHandler(userConfig.adapter, logger), // Callback functions - callbacks: { ...defaultCallbacks, ...config.callbacks }, + callbacks: { ...defaultCallbacks, ...userConfig.callbacks }, logger, callbackUrl: url.origin, isOnRedirectProxy, experimental: { - ...config.experimental, + ...userConfig.experimental, }, resCookies: [], sessionStore: new cookie.SessionStore( From 09015917b740597cf97d80159f4b16d82e106394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:49:47 +0200 Subject: [PATCH 09/15] simplify --- packages/core/src/lib/init.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index f4391e852c..ff0254f0ea 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -9,10 +9,7 @@ import { setLogger, type LoggerInstance } from "./utils/logger.js" import { merge } from "./utils/merge.js" import { skipCSRFCheck } from "./symbols.js" -import type { - InternalConfig as InternalConfig, - RequestInternal, -} from "../types.js" +import type { InternalConfig, RequestInternal } from "../types.js" import type { AuthConfig } from "../index.js" export const defaultCallbacks: InternalConfig["callbacks"] = { From 651bace08cbafc88db91771599b1ed2d646fda9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:53:02 +0200 Subject: [PATCH 10/15] rename --- packages/core/src/lib/init.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index ff0254f0ea..16ee28ad42 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -82,7 +82,7 @@ export async function init( // User provided options are overridden by other options, // except for the options with special handling above - const internalConfig: InternalConfig = { + const config: InternalConfig = { debug: false, pages: {}, theme: { @@ -138,46 +138,46 @@ export async function init( } if (csrfDisabled) { - internalConfig.csrfTokenVerified = true + config.csrfTokenVerified = true } else { const { csrfToken, cookie: csrfCookie, csrfTokenVerified, } = await createCSRFToken({ - config: internalConfig, - cookieValue: reqCookies?.[internalConfig.cookies.csrfToken.name], + config: config, + cookieValue: reqCookies?.[config.cookies.csrfToken.name], isPost, bodyValue: reqCsrfToken, }) - internalConfig.csrfToken = csrfToken - internalConfig.csrfTokenVerified = csrfTokenVerified + config.csrfToken = csrfToken + config.csrfTokenVerified = csrfTokenVerified if (csrfCookie) { - internalConfig.resCookies.push({ - name: internalConfig.cookies.csrfToken.name, + config.resCookies.push({ + name: config.cookies.csrfToken.name, value: csrfCookie, - options: internalConfig.cookies.csrfToken.options, + options: config.cookies.csrfToken.options, }) } } const { callbackUrl, callbackUrlCookie } = await createCallbackUrl({ - config: internalConfig, - cookieValue: reqCookies?.[internalConfig.cookies.callbackUrl.name], + config: config, + cookieValue: reqCookies?.[config.cookies.callbackUrl.name], paramValue: reqCallbackUrl, }) - internalConfig.callbackUrl = callbackUrl + config.callbackUrl = callbackUrl if (callbackUrlCookie) { - internalConfig.resCookies.push({ - name: internalConfig.cookies.callbackUrl.name, + config.resCookies.push({ + name: config.cookies.callbackUrl.name, value: callbackUrlCookie, - options: internalConfig.cookies.callbackUrl.options, + options: config.cookies.callbackUrl.options, }) } - return internalConfig + return config } type Method = (...args: any[]) => Promise From 21f32bd805993ca02ebc6eb3be86c75bd941a8c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:54:52 +0200 Subject: [PATCH 11/15] use isOAuthProvider --- packages/core/src/lib/init.ts | 7 ++----- packages/core/src/lib/utils/providers.ts | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index 16ee28ad42..a1dc95ee61 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -4,7 +4,7 @@ import * as cookie from "./utils/cookie.js" import { createCSRFToken } from "./actions/callback/oauth/csrf-token.js" import { AdapterError, EventError } from "../errors.js" -import parseProviders from "./utils/providers.js" +import parseProviders, { isOAuthProvider } from "./utils/providers.js" import { setLogger, type LoggerInstance } from "./utils/logger.js" import { merge } from "./utils/merge.js" import { skipCSRFCheck } from "./symbols.js" @@ -59,10 +59,7 @@ export async function init( const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default let isOnRedirectProxy = false - if ( - (provider?.type === "oauth" || provider?.type === "oidc") && - provider.redirectProxyUrl - ) { + if (isOAuthProvider(provider) && provider.redirectProxyUrl) { try { isOnRedirectProxy = new URL(provider.redirectProxyUrl).origin === url.origin diff --git a/packages/core/src/lib/utils/providers.ts b/packages/core/src/lib/utils/providers.ts index 8e8a4e66d0..925a6b9adc 100644 --- a/packages/core/src/lib/utils/providers.ts +++ b/packages/core/src/lib/utils/providers.ts @@ -192,7 +192,7 @@ export function isOAuth2Provider( /** Either OAuth 2 or OIDC */ export function isOAuthProvider( - provider: InternalProvider + provider?: InternalProvider ): provider is InternalProvider<"oauth" | "oidc"> { - return provider.type === "oauth" || provider.type === "oidc" + return provider?.type === "oauth" || provider?.type === "oidc" } From a939373120071cf014a51ef6fe90676062dc0ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Sun, 20 Oct 2024 16:57:12 +0200 Subject: [PATCH 12/15] simplify --- packages/core/src/lib/init.ts | 1 - packages/core/src/types.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index a1dc95ee61..c2caf7e621 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -80,7 +80,6 @@ export async function init( // User provided options are overridden by other options, // except for the options with special handling above const config: InternalConfig = { - debug: false, pages: {}, theme: { colorScheme: "auto", diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b4fc950d99..c9b35c7121 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -407,7 +407,6 @@ export interface InternalConfig { csrfTokenVerified?: boolean secret: string | string[] theme: Theme - debug: boolean logger: LoggerInstance session: NonNullable> pages: Partial From f075e17fd9df68f8a8e7c8ad47252179071ea1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Mon, 21 Oct 2024 00:59:14 +0200 Subject: [PATCH 13/15] simplify session seal/unseal --- packages/core/src/index.ts | 4 +- .../src/lib/actions/callback/handle-login.ts | 23 ++++----- .../core/src/lib/actions/callback/index.ts | 39 +++++++-------- .../src/lib/actions/callback/oauth/checks.ts | 33 ++++++------- packages/core/src/lib/actions/session.ts | 11 ++--- packages/core/src/lib/actions/signout.ts | 10 ++-- packages/core/src/lib/init.ts | 47 +++++++++++-------- packages/core/src/lib/utils/session.ts | 47 +++++++------------ packages/core/src/types.ts | 14 ++++-- 9 files changed, 109 insertions(+), 119 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5246ba3d7a..8ba0022933 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -311,7 +311,7 @@ export interface AuthConfig * By default, the cookie is sealed using an encrypted JWT. It uses the _A256CBC-HS512_ algorithm ({@link https://www.rfc-editor.org/rfc/rfc7518.html#section-5.2.5 JWE}). * {@link AuthConfig.session.secret} is used to derive a suitable encryption key. */ - seal?: () => Awaitable + seal?: JWTOptions["encode"] /** * Unseals the session payload from the cookie, to read the data on the server. * @@ -320,7 +320,7 @@ export interface AuthConfig * * If you passed an array of secrets, we will iterate over them from first-to-last, trying to unseal the data. */ - unseal?: () => Awaitable + unseal?: JWTOptions["decode"] } /** * Specify URLs to be used if you want to create custom sign in, sign out and error pages. diff --git a/packages/core/src/lib/actions/callback/handle-login.ts b/packages/core/src/lib/actions/callback/handle-login.ts index 65c446b40a..1a6fbf8584 100644 --- a/packages/core/src/lib/actions/callback/handle-login.ts +++ b/packages/core/src/lib/actions/callback/handle-login.ts @@ -35,12 +35,7 @@ export async function handleLoginOrRegister( if (!["email", "oauth", "oidc", "webauthn"].includes(_account.type)) throw new Error("Provider not supported") - const { - adapter, - jwt, - events, - session: { strategy: sessionStrategy, generateSessionToken }, - } = config + const { adapter, events, session: sessionConfig } = config // If no adapter is configured then we don't have a database and cannot // persist data; in this mode we just return a dummy session object. @@ -67,13 +62,13 @@ export async function handleLoginOrRegister( let user: AdapterUser | null = null let isNewUser = false - const useJwtSession = sessionStrategy === "jwt" + const useJwtSession = !sessionConfig.isDatabase + const { generateSessionToken, maxAge: sessionMaxAge } = sessionConfig if (sessionToken) { if (useJwtSession) { try { - const salt = config.cookies.sessionToken.name - session = await jwt.decode({ ...jwt, token: sessionToken, salt }) + session = await sessionConfig.unseal(sessionToken) if (session && "sub" in session && session.sub) { user = await getUser(session.sub) } @@ -121,7 +116,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: user.id, - expires: fromDate(config.session.maxAge), + expires: fromDate(sessionMaxAge), }) return { session, user, isNewUser } @@ -153,7 +148,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: userByAccount.id, - expires: fromDate(config.session.maxAge), + expires: fromDate(sessionMaxAge), }) const currentAccount: AdapterAccount = { @@ -212,7 +207,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: user.id, - expires: fromDate(config.session.maxAge), + expires: fromDate(sessionMaxAge), }) const currentAccount: AdapterAccount = { ...account, userId: user.id } @@ -246,7 +241,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: userByAccount.id, - expires: fromDate(config.session.maxAge), + expires: fromDate(sessionMaxAge), }) return { session, user: userByAccount, isNewUser } @@ -326,7 +321,7 @@ export async function handleLoginOrRegister( : await createSession({ sessionToken: generateSessionToken(), userId: user.id, - expires: fromDate(config.session.maxAge), + expires: fromDate(sessionMaxAge), }) return { session, user, isNewUser } diff --git a/packages/core/src/lib/actions/callback/index.ts b/packages/core/src/lib/actions/callback/index.ts index 9b9149e11a..769545f4b5 100644 --- a/packages/core/src/lib/actions/callback/index.ts +++ b/packages/core/src/lib/actions/callback/index.ts @@ -42,16 +42,16 @@ export async function callback( url, callbackUrl, pages, - jwt, events, callbacks, - session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, + session: sessionConfig, logger, resCookies: cookies, sessionStore, } = config - const useJwtSession = sessionStrategy === "jwt" + const useJwtSession = !sessionConfig.isDatabase + const sessionMaxAge = sessionConfig.maxAge try { if (provider.type === "oauth" || provider.type === "oidc") { @@ -156,9 +156,7 @@ export async function callback( if (token === null) { cookies.push(...sessionStore.clean()) } else { - const salt = config.cookies.sessionToken.name - // Encode token - const newToken = await jwt.encode({ ...jwt, token, salt }) + const newToken = await sessionConfig.seal(token) // Set cookie expiry date const cookieExpires = new Date() @@ -263,7 +261,7 @@ export async function callback( picture: loggedInUser.image, sub: loggedInUser.id?.toString(), } - const token = await callbacks.jwt({ + const payload = await callbacks.jwt({ token: defaultToken, user: loggedInUser, account, @@ -271,13 +269,11 @@ export async function callback( trigger: isNewUser ? "signUp" : "signIn", }) - // Clear cookies if token is null - if (token === null) { + // Clear cookies if payload is null + if (payload === null) { cookies.push(...sessionStore.clean()) } else { - const salt = config.cookies.sessionToken.name - // Encode token - const newToken = await jwt.encode({ ...jwt, token, salt }) + const newToken = await sessionConfig.seal(payload) // Set cookie expiry date const cookieExpires = new Date() @@ -352,7 +348,7 @@ export async function callback( sub: user.id, } - const token = await callbacks.jwt({ + const payload = await callbacks.jwt({ token: defaultToken, user, account, @@ -360,13 +356,12 @@ export async function callback( trigger: "signIn", }) - // Clear cookies if token is null - if (token === null) { + // Clear cookies if payload is null + if (payload === null) { cookies.push(...sessionStore.clean()) } else { - const salt = config.cookies.sessionToken.name // Encode token - const newToken = await jwt.encode({ ...jwt, token, salt }) + const newToken = await sessionConfig.seal(payload) // Set cookie expiry date const cookieExpires = new Date() @@ -455,7 +450,7 @@ export async function callback( picture: loggedInUser.image, sub: loggedInUser.id?.toString(), } - const token = await callbacks.jwt({ + const payload = await callbacks.jwt({ token: defaultToken, user: loggedInUser, account: currentAccount, @@ -463,13 +458,11 @@ export async function callback( trigger: isNewUser ? "signUp" : "signIn", }) - // Clear cookies if token is null - if (token === null) { + // Clear cookies if payload is null + if (payload === null) { cookies.push(...sessionStore.clean()) } else { - const salt = config.cookies.sessionToken.name - // Encode token - const newToken = await jwt.encode({ ...jwt, token, salt }) + const newToken = await sessionConfig.seal(payload) // Set cookie expiry date const cookieExpires = new Date() diff --git a/packages/core/src/lib/actions/callback/oauth/checks.ts b/packages/core/src/lib/actions/callback/oauth/checks.ts index 0f176d1568..83aa93c56f 100644 --- a/packages/core/src/lib/actions/callback/oauth/checks.ts +++ b/packages/core/src/lib/actions/callback/oauth/checks.ts @@ -2,7 +2,7 @@ import * as o from "oauth4webapi" import { InvalidCheck } from "../../../../errors.js" // NOTE: We use the default JWT methods here because they encrypt/decrypt the payload, not just sign it. -import { decode, encode } from "../../../../jwt.js" +import { decode as unseal, encode as seal } from "../../../../jwt.js" import type { CookiesOptions, @@ -37,14 +37,15 @@ async function sealCookie( expires, }) - const encoded = await encode({ - ...config.jwt, + const sealed = await seal({ + secret: config.secret, maxAge: COOKIE_TTL, token: { value: payload } satisfies CookiePayload, salt: cookie.name, }) + const cookieOptions = { ...cookie.options, expires } - return { name: cookie.name, value: encoded, options: cookieOptions } + return { name: cookie.name, value: sealed, options: cookieOptions } } async function parseCookie( @@ -53,16 +54,16 @@ async function parseCookie( config: InternalConfig ): Promise { try { - const { logger, cookies, jwt } = config + const { logger, cookies } = config logger.debug(`PARSE_${name.toUpperCase()}`, { cookie: value }) if (!value) throw new InvalidCheck(`${name} cookie was missing`) - const parsed = await decode({ - ...jwt, + const unsealed = await unseal({ + secret: config.secret, token: value, salt: cookies[name].name, }) - if (parsed?.value) return parsed.value + if (unsealed?.value) return unsealed.value throw new Error("Invalid cookie") } catch (error) { throw new InvalidCheck(`${name} value could not be parsed`, { @@ -155,8 +156,8 @@ export const state = { origin, random: o.generateRandomState(), } satisfies EncodedState - const value = await encode({ - secret: config.jwt.secret, + const value = await seal({ + secret: config.secret, token: payload, salt: encodedStateSalt, maxAge: STATE_MAX_AGE, @@ -175,8 +176,8 @@ export const state = { async decode(state: string, config: InternalConfig) { try { config.logger.debug("DECODE_STATE", { state }) - const payload = await decode({ - secret: config.jwt.secret, + const payload = await unseal({ + secret: config.secret, token: state, salt: encodedStateSalt, }) @@ -222,8 +223,8 @@ export const webauthnChallenge = { return { cookie: await sealCookie( "webauthnChallenge", - await encode({ - secret: config.jwt.secret, + await seal({ + secret: config.secret, token: { challenge, registerData } satisfies WebAuthnChallengePayload, salt: webauthnChallengeSalt, maxAge: WEBAUTHN_CHALLENGE_MAX_AGE, @@ -242,8 +243,8 @@ export const webauthnChallenge = { const parsed = await parseCookie("webauthnChallenge", cookieValue, config) - const payload = await decode({ - secret: config.jwt.secret, + const payload = await unseal({ + secret: config.secret, token: parsed, salt: webauthnChallengeSalt, }) diff --git a/packages/core/src/lib/actions/session.ts b/packages/core/src/lib/actions/session.ts index b74cb9b5fe..93df95da1c 100644 --- a/packages/core/src/lib/actions/session.ts +++ b/packages/core/src/lib/actions/session.ts @@ -12,15 +12,15 @@ export async function session( ): Promise> { const { adapter, - jwt, events, callbacks, logger, - session: { strategy: sessionStrategy, maxAge: sessionMaxAge }, + session: sessionConfig, resCookies: cookies, sessionStore, } = config + const sessionMaxAge = sessionConfig.maxAge const response: ResponseInternal = { body: null, headers: { "Content-Type": "application/json" }, @@ -31,10 +31,9 @@ export async function session( if (!sessionToken) return response - if (sessionStrategy === "jwt") { + if (!sessionConfig.isDatabase) { try { - const salt = config.cookies.sessionToken.name - const payload = await jwt.decode({ ...jwt, token: sessionToken, salt }) + const payload = await sessionConfig.unseal(sessionToken) if (!payload) throw new Error("Invalid JWT") @@ -61,7 +60,7 @@ export async function session( response.body = newSession // Refresh JWT expiry by re-signing it, with an updated expiry date - const newToken = await jwt.encode({ ...jwt, token, salt }) + const newToken = await sessionConfig.seal(token) // Set cookie, to also update expiry date on cookie const sessionCookies = sessionStore.chunk(newToken, { diff --git a/packages/core/src/lib/actions/signout.ts b/packages/core/src/lib/actions/signout.ts index 35c6160c0d..c787a0ac98 100644 --- a/packages/core/src/lib/actions/signout.ts +++ b/packages/core/src/lib/actions/signout.ts @@ -13,7 +13,6 @@ export async function signOut( config: InternalConfig ): Promise { const { - jwt, events, callbackUrl: redirect, logger, @@ -25,13 +24,12 @@ export async function signOut( if (!sessionToken) return { redirect, cookies } try { - if (session.strategy === "jwt") { - const salt = config.cookies.sessionToken.name - const token = await jwt.decode({ ...jwt, token: sessionToken, salt }) - await events.signOut?.({ token }) - } else { + if (session.isDatabase) { const session = await config.adapter?.deleteSession(sessionToken) await events.signOut?.({ session }) + } else { + const token = await session.unseal(sessionToken) + await events.signOut?.({ token }) } } catch (e) { logger.error(new SignOutError(e as Error)) diff --git a/packages/core/src/lib/init.ts b/packages/core/src/lib/init.ts index c2caf7e621..fcf91542bb 100644 --- a/packages/core/src/lib/init.ts +++ b/packages/core/src/lib/init.ts @@ -56,8 +56,6 @@ export async function init( config: userConfig, }) - const maxAge = 30 * 24 * 60 * 60 // Sessions expire after 30 days of being idle by default - let isOnRedirectProxy = false if (isOAuthProvider(provider) && provider.redirectProxyUrl) { try { @@ -77,6 +75,11 @@ export async function init( userConfig.cookies ) + const sessionSecret = userConfig.jwt?.secret ?? userConfig.secret! // Asserted in assert.ts + const sessionSalt = cookies.sessionToken.name + const unseal = userConfig.jwt?.decode ?? jwt.decode + const seal = userConfig.jwt?.encode ?? jwt.encode + // User provided options are overridden by other options, // except for the options with special handling above const config: InternalConfig = { @@ -99,20 +102,28 @@ export async function init( providers, // Session options session: { - // If no adapter specified, force use of JSON Web Tokens (stateless) - strategy: userConfig.adapter ? "database" : "jwt", - maxAge, - updateAge: 24 * 60 * 60, - generateSessionToken: () => crypto.randomUUID(), - ...userConfig.session, - }, - // JWT options - jwt: { - secret: userConfig.secret!, // Asserted in assert.ts - maxAge: userConfig.session?.maxAge ?? maxAge, // default to same as `session.maxAge` - encode: jwt.encode, - decode: jwt.decode, - ...userConfig.jwt, + isDatabase: !userConfig.session?.strategy + ? !!userConfig.adapter + : userConfig.session.strategy === "database", + generateSessionToken() { + return crypto.randomUUID() + }, + updateAge: 24 * 60 * 60, // Sessions are updated if they are within 24 hours of expiry by default + maxAge: userConfig.jwt?.maxAge ?? 30 * 24 * 60 * 60, // Sessions expire after 30 days of being idle by default + unseal(value) { + return unseal({ + secret: sessionSecret, + token: value, + salt: sessionSalt, + }) + }, + seal(payload) { + return seal({ + secret: sessionSecret, + token: payload, + salt: sessionSalt, + }) + }, }, // Event messages events: eventsErrorHandler(userConfig.events ?? {}, logger), @@ -122,9 +133,7 @@ export async function init( logger, callbackUrl: url.origin, isOnRedirectProxy, - experimental: { - ...userConfig.experimental, - }, + experimental: { ...userConfig.experimental }, resCookies: [], sessionStore: new cookie.SessionStore( cookies.sessionToken, diff --git a/packages/core/src/lib/utils/session.ts b/packages/core/src/lib/utils/session.ts index a7392fbf98..3aa3a19913 100644 --- a/packages/core/src/lib/utils/session.ts +++ b/packages/core/src/lib/utils/session.ts @@ -1,40 +1,27 @@ import type { InternalConfig, User } from "../../types.js" -/** - * Returns the currently logged in user, if any. - */ +/** Returns the currently logged in user, if any. */ export async function getLoggedInUser( config: InternalConfig ): Promise { - const { - adapter, - jwt, - session: { strategy: sessionStrategy }, - sessionStore, - } = config + const { adapter, session, sessionStore } = config - const sessionToken = sessionStore.value - if (!sessionToken) return null + const sessionCookie = sessionStore.value + if (!sessionCookie) return null - // Try to decode JWT - if (sessionStrategy === "jwt") { - const salt = config.cookies.sessionToken.name - const payload = await jwt.decode({ ...jwt, token: sessionToken, salt }) - - if (payload && payload.sub) { - return { - id: payload.sub, - name: payload.name, - email: payload.email, - image: payload.picture, - } - } - } else { - const userAndSession = await adapter?.getSessionAndUser(sessionToken) - if (userAndSession) { - return userAndSession.user - } + if (session.isDatabase) { + const userAndSession = await adapter?.getSessionAndUser(sessionCookie) + if (userAndSession) return userAndSession.user } - return null + const payload = await session.unseal(sessionCookie) + + if (!payload?.sub) return null + + return { + id: payload.sub, + name: payload.name, + email: payload.email, + image: payload.picture, + } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index c9b35c7121..fef9f1151d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -55,7 +55,6 @@ import type { CookieSerializeOptions } from "cookie" import type { TokenEndpointResponse } from "oauth4webapi" import type { Adapter } from "./adapters.js" import { AuthConfig } from "./index.js" -import type { JWTOptions } from "./jwt.js" import type { Cookie, SessionStore } from "./lib/utils/cookie.js" import type { LoggerInstance } from "./lib/utils/logger.js" import type { @@ -69,6 +68,7 @@ import type { WebAuthnConfig, WebAuthnProviderType, } from "./providers/webauthn.js" +import { JWT } from "./jwt.js" export type { WebAuthnOptionsResponseBody } from "./lib/utils/webauthn-utils.js" export type { AuthConfig } from "./index.js" @@ -408,9 +408,17 @@ export interface InternalConfig { secret: string | string[] theme: Theme logger: LoggerInstance - session: NonNullable> + session: { + isDatabase: boolean + unseal(value: string): Awaitable + seal(payload: JWT): Awaitable + generateSessionToken: NonNullable< + Required + >["generateSessionToken"] + maxAge: NonNullable>["maxAge"] + updateAge: NonNullable>["updateAge"] + } pages: Partial - jwt: JWTOptions events: NonNullable adapter: Required | undefined callbacks: NonNullable> From 37bdc730a69ccc2498fcb4127e9fcce6de7deffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Mon, 21 Oct 2024 01:03:07 +0200 Subject: [PATCH 14/15] mark absolute `maxAge` support as TODO --- packages/core/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8ba0022933..860a9ea952 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -287,7 +287,7 @@ export interface AuthConfig * * @default 2592000 // 30 days */ - maxAge?: number | Date + maxAge?: number // TODO: | Date /** * How often the session should be updated in seconds. If set to `0`, the session is updated every time. * From cb4042288e3b22e01328d41d8af958ba69823c0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Orb=C3=A1n?= Date: Wed, 1 Jan 2025 21:00:10 +0100 Subject: [PATCH 15/15] chore: lint --- docs/pages/getting-started/adapters/drizzle.mdx | 12 ++++++------ .../getting-started/session-management/login.mdx | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/pages/getting-started/adapters/drizzle.mdx b/docs/pages/getting-started/adapters/drizzle.mdx index 95a440a543..7707eb06a8 100644 --- a/docs/pages/getting-started/adapters/drizzle.mdx +++ b/docs/pages/getting-started/adapters/drizzle.mdx @@ -95,8 +95,8 @@ export const accounts = pgTable( columns: [account.provider, account.providerAccountId], }), }, - ], -); + ] +) export const sessions = pgTable("session", { sessionToken: text("sessionToken").primaryKey(), @@ -119,8 +119,8 @@ export const verificationTokens = pgTable( columns: [verificationToken.identifier, verificationToken.token], }), }, - ], -); + ] +) export const authenticators = pgTable( "authenticator", @@ -142,8 +142,8 @@ export const authenticators = pgTable( columns: [authenticator.userId, authenticator.credentialID], }), }, - ], -); + ] +) ``` diff --git a/docs/pages/getting-started/session-management/login.mdx b/docs/pages/getting-started/session-management/login.mdx index 4d0f7b5fe6..66f75c9901 100644 --- a/docs/pages/getting-started/session-management/login.mdx +++ b/docs/pages/getting-started/session-management/login.mdx @@ -129,7 +129,6 @@ Just like in other frameworks, you can also pass a provider to the `signIn` func - The Express package runs server-side and therefore it doesn't make sense to create a "SignIn button component". However, to signin or signout with Express, send a request to the appropriate [REST API Endpoints](/reference/core/types#authaction) from your client (i.e. `/auth/signin`, `/auth/signout`, etc.). To sign in users with Express, you can create a route that handles the sign-in logic. Here is an example: