From 30c100e3fdaa00be4d627f84351e3421d9829243 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath <adamjmcgrath@gmail.com> Date: Wed, 1 Nov 2023 16:06:45 +0000 Subject: [PATCH 1/7] Allow lazy config in auth0-session --- src/auth0-session/client/abstract-client.ts | 7 +- src/auth0-session/client/edge-client.ts | 90 +++++++++---------- src/auth0-session/client/node-client.ts | 8 +- src/auth0-session/config.ts | 2 + src/auth0-session/handlers/callback.ts | 6 +- src/auth0-session/handlers/login.ts | 6 +- src/auth0-session/handlers/logout.ts | 6 +- src/auth0-session/session/abstract-session.ts | 22 +++-- src/auth0-session/session/stateful-session.ts | 35 +++++--- .../session/stateless-session.ts | 50 +++++++---- src/auth0-session/transient-store.ts | 21 +++-- .../auth0-session/client/edge-client.test.ts | 3 +- .../auth0-session/client/node-client.test.ts | 15 ++++ tests/auth0-session/handlers/callback.test.ts | 8 ++ tests/auth0-session/handlers/login.test.ts | 8 ++ tests/auth0-session/handlers/logout.test.ts | 8 ++ .../session/stateless-session.test.ts | 10 +++ tests/auth0-session/transient-store.test.ts | 9 ++ 18 files changed, 206 insertions(+), 108 deletions(-) diff --git a/src/auth0-session/client/abstract-client.ts b/src/auth0-session/client/abstract-client.ts index d51771e73..25f0c9360 100644 --- a/src/auth0-session/client/abstract-client.ts +++ b/src/auth0-session/client/abstract-client.ts @@ -1,4 +1,4 @@ -import { Config } from '../config'; +import { Config, GetConfig } from '../config'; import { Auth0Request } from '../http'; export type Telemetry = { @@ -85,7 +85,10 @@ export interface AuthorizationParameters { } export abstract class AbstractClient { - constructor(protected config: Config, protected telemetry: Telemetry) {} + protected getConfig: () => Config | Promise<Config>; + constructor(getConfig: GetConfig, protected telemetry: Telemetry) { + this.getConfig = typeof getConfig === 'function' ? getConfig : () => getConfig; + } abstract authorizationUrl(parameters: Record<string, unknown>): Promise<string>; abstract callbackParams(req: Auth0Request, expectedState: string): Promise<URLSearchParams>; abstract callback( diff --git a/src/auth0-session/client/edge-client.ts b/src/auth0-session/client/edge-client.ts index 0529d1c69..1480f7bca 100644 --- a/src/auth0-session/client/edge-client.ts +++ b/src/auth0-session/client/edge-client.ts @@ -6,13 +6,11 @@ import { OpenIDCallbackChecks, TokenEndpointResponse, AbstractClient, - EndSessionParameters, - Telemetry + EndSessionParameters } from './abstract-client'; import { ApplicationError, DiscoveryError, IdentityProviderError, UserInfoError } from '../utils/errors'; import { AccessTokenError, AccessTokenErrorCode } from '../../utils/errors'; import urlJoin from 'url-join'; -import { Config } from '../config'; const encodeBase64 = (input: string) => { const unencoded = new TextEncoder().encode(input); @@ -28,36 +26,29 @@ const encodeBase64 = (input: string) => { export class EdgeClient extends AbstractClient { private client?: oauth.Client; private as?: oauth.AuthorizationServer; - private httpOptions: () => oauth.HttpRequestOptions; - constructor(protected config: Config, protected telemetry: Telemetry) { - super(config, telemetry); - if (config.authorizationParams.response_type !== 'code') { - throw new Error('This SDK only supports `response_type=code` when used in an Edge runtime.'); + private async httpOptions(): Promise<oauth.HttpRequestOptions> { + const headers = new Headers(); + const config = await this.getConfig(); + if (config.enableTelemetry) { + const { name, version } = this.telemetry; + headers.set('User-Agent', `${name}/${version}`); + headers.set( + 'Auth0-Client', + encodeBase64( + JSON.stringify({ + name, + version, + env: { + edge: true + } + }) + ) + ); } - - this.httpOptions = () => { - const headers = new Headers(); - if (config.enableTelemetry) { - const { name, version } = telemetry; - headers.set('User-Agent', `${name}/${version}`); - headers.set( - 'Auth0-Client', - encodeBase64( - JSON.stringify({ - name, - version, - env: { - edge: true - } - }) - ) - ); - } - return { - signal: AbortSignal.timeout(this.config.httpTimeout), - headers - }; + return { + signal: AbortSignal.timeout(config.httpTimeout), + headers }; } @@ -65,22 +56,26 @@ export class EdgeClient extends AbstractClient { if (this.as) { return [this.as, this.client as oauth.Client]; } + const config = await this.getConfig(); + if (config.authorizationParams.response_type !== 'code') { + throw new Error('This SDK only supports `response_type=code` when used in an Edge runtime.'); + } - const issuer = new URL(this.config.issuerBaseURL); + const issuer = new URL(config.issuerBaseURL); try { this.as = await oauth - .discoveryRequest(issuer, this.httpOptions()) + .discoveryRequest(issuer, await this.httpOptions()) .then((response) => oauth.processDiscoveryResponse(issuer, response)); } catch (e) { - throw new DiscoveryError(e, this.config.issuerBaseURL); + throw new DiscoveryError(e, config.issuerBaseURL); } this.client = { - client_id: this.config.clientID, - ...(!this.config.clientAssertionSigningKey && { client_secret: this.config.clientSecret }), - token_endpoint_auth_method: this.config.clientAuthMethod, - id_token_signed_response_alg: this.config.idTokenSigningAlg, - [oauth.clockTolerance]: this.config.clockTolerance + client_id: config.clientID, + ...(!config.clientAssertionSigningKey && { client_secret: config.clientSecret }), + token_endpoint_auth_method: config.clientAuthMethod, + id_token_signed_response_alg: config.idTokenSigningAlg, + [oauth.clockTolerance]: config.clockTolerance }; return [this.as, this.client]; @@ -88,8 +83,9 @@ export class EdgeClient extends AbstractClient { async authorizationUrl(parameters: Record<string, unknown>): Promise<string> { const [as] = await this.getClient(); + const config = await this.getConfig(); const authorizationUrl = new URL(as.authorization_endpoint as string); - authorizationUrl.searchParams.set('client_id', this.config.clientID); + authorizationUrl.searchParams.set('client_id', config.clientID); Object.entries(parameters).forEach(([key, value]) => { if (value === null || value === undefined) { return; @@ -126,8 +122,7 @@ export class EdgeClient extends AbstractClient { extras: CallbackExtras ): Promise<TokenEndpointResponse> { const [as, client] = await this.getClient(); - - const { clientAssertionSigningKey, clientAssertionSigningAlg } = this.config; + const { clientAssertionSigningKey, clientAssertionSigningAlg } = await this.getConfig(); let clientPrivateKey = clientAssertionSigningKey as CryptoKey | undefined; /* c8 ignore next 3 */ @@ -167,15 +162,16 @@ export class EdgeClient extends AbstractClient { async endSessionUrl(parameters: EndSessionParameters): Promise<string> { const [as] = await this.getClient(); const issuerUrl = new URL(as.issuer); + const config = await this.getConfig(); if ( - this.config.idpLogout && - (this.config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && this.config.auth0Logout !== false)) + config.idpLogout && + (config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && config.auth0Logout !== false)) ) { const { id_token_hint, post_logout_redirect_uri, ...extraParams } = parameters; const auth0LogoutUrl: URL = new URL(urlJoin(as.issuer, '/v2/logout')); post_logout_redirect_uri && auth0LogoutUrl.searchParams.set('returnTo', post_logout_redirect_uri); - auth0LogoutUrl.searchParams.set('client_id', this.config.clientID); + auth0LogoutUrl.searchParams.set('client_id', config.clientID); Object.entries(extraParams).forEach(([key, value]: [string, string]) => { if (value === null || value === undefined) { return; @@ -195,13 +191,13 @@ export class EdgeClient extends AbstractClient { oidcLogoutUrl.searchParams.set(key, value); }); - oidcLogoutUrl.searchParams.set('client_id', this.config.clientID); + oidcLogoutUrl.searchParams.set('client_id', config.clientID); return oidcLogoutUrl.toString(); } async userinfo(accessToken: string): Promise<Record<string, unknown>> { const [as, client] = await this.getClient(); - const response = await oauth.userInfoRequest(as, client, accessToken, this.httpOptions()); + const response = await oauth.userInfoRequest(as, client, accessToken, await this.httpOptions()); try { return await oauth.processUserInfoResponse(as, client, oauth.skipSubjectCheck, response); diff --git a/src/auth0-session/client/node-client.ts b/src/auth0-session/client/node-client.ts index dad56ac13..683f1f306 100644 --- a/src/auth0-session/client/node-client.ts +++ b/src/auth0-session/client/node-client.ts @@ -38,9 +38,10 @@ export class NodeClient extends AbstractClient { return this.client; } const { - config, + getConfig, telemetry: { name, version } } = this; + const config = await getConfig(); const defaultHttpOptions: CustomHttpOptionsProvider = (_url, options) => ({ ...options, @@ -132,10 +133,7 @@ export class NodeClient extends AbstractClient { const issuerUrl = new URL(issuer.metadata.issuer); if (config.idpLogout) { - if ( - this.config.idpLogout && - (this.config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && this.config.auth0Logout !== false)) - ) { + if (config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && config.auth0Logout !== false)) { Object.defineProperty(this.client, 'endSessionUrl', { value(params: EndSessionParameters) { const { id_token_hint, post_logout_redirect_uri, ...extraParams } = params; diff --git a/src/auth0-session/config.ts b/src/auth0-session/config.ts index 59f06d23a..191f4aa20 100644 --- a/src/auth0-session/config.ts +++ b/src/auth0-session/config.ts @@ -364,3 +364,5 @@ export interface LogoutOptions { */ logoutParams?: { [key: string]: any }; } + +export type GetConfig = Config | (() => Config | Promise<Config>); diff --git a/src/auth0-session/handlers/callback.ts b/src/auth0-session/handlers/callback.ts index 6a094593c..47b8d889d 100644 --- a/src/auth0-session/handlers/callback.ts +++ b/src/auth0-session/handlers/callback.ts @@ -1,5 +1,5 @@ import urlJoin from 'url-join'; -import { AuthorizationParameters, Config } from '../config'; +import { AuthorizationParameters, GetConfig, Config } from '../config'; import TransientStore from '../transient-store'; import { decodeState } from '../utils/encoding'; import { SessionCache } from '../session-cache'; @@ -25,12 +25,14 @@ export type CallbackOptions = { export type HandleCallback = (req: Auth0Request, res: Auth0Response, options?: CallbackOptions) => Promise<void>; export default function callbackHandlerFactory( - config: Config, + getConfig: GetConfig, client: AbstractClient, sessionCache: SessionCache, transientCookieHandler: TransientStore ): HandleCallback { + const getConfigFn = typeof getConfig === 'function' ? getConfig : () => getConfig; return async (req, res, options) => { + const config = await getConfigFn(); const redirectUri = options?.redirectUri || getRedirectUri(config); let tokenResponse; diff --git a/src/auth0-session/handlers/login.ts b/src/auth0-session/handlers/login.ts index bda12462f..7bf3a98d0 100644 --- a/src/auth0-session/handlers/login.ts +++ b/src/auth0-session/handlers/login.ts @@ -1,5 +1,5 @@ import urlJoin from 'url-join'; -import { Config, LoginOptions } from '../config'; +import { Config, GetConfig, LoginOptions } from '../config'; import TransientStore from '../transient-store'; import { encodeState } from '../utils/encoding'; import createDebug from '../utils/debug'; @@ -23,11 +23,13 @@ export type AuthVerification = { }; export default function loginHandlerFactory( - config: Config, + getConfig: GetConfig, client: AbstractClient, transientHandler: TransientStore ): HandleLogin { + const getConfigFn = typeof getConfig === 'function' ? getConfig : () => getConfig; return async (req, res, options = {}) => { + const config = await getConfigFn(); const returnTo = options.returnTo || config.baseURL; const opts = { diff --git a/src/auth0-session/handlers/logout.ts b/src/auth0-session/handlers/logout.ts index b511b8317..01cc01217 100644 --- a/src/auth0-session/handlers/logout.ts +++ b/src/auth0-session/handlers/logout.ts @@ -1,6 +1,6 @@ import urlJoin from 'url-join'; import createDebug from '../utils/debug'; -import { Config, LogoutOptions } from '../config'; +import { GetConfig, LogoutOptions } from '../config'; import { SessionCache } from '../session-cache'; import { Auth0Request, Auth0Response } from '../http'; import { AbstractClient } from '../client/abstract-client'; @@ -10,11 +10,13 @@ const debug = createDebug('logout'); export type HandleLogout = (req: Auth0Request, res: Auth0Response, options?: LogoutOptions) => Promise<void>; export default function logoutHandlerFactory( - config: Config, + getConfig: GetConfig, client: AbstractClient, sessionCache: SessionCache ): HandleLogout { + const getConfigFn = typeof getConfig === 'function' ? getConfig : () => getConfig; return async (req, res, options = {}) => { + const config = await getConfigFn(); let returnURL = options.returnTo || config.routes.postLogoutRedirect; debug('logout() with return url: %s', returnURL); diff --git a/src/auth0-session/session/abstract-session.ts b/src/auth0-session/session/abstract-session.ts index 76f158ace..17bd9a8cc 100644 --- a/src/auth0-session/session/abstract-session.ts +++ b/src/auth0-session/session/abstract-session.ts @@ -1,6 +1,6 @@ import createDebug from '../utils/debug'; import { CookieSerializeOptions } from 'cookie'; -import { Config } from '../config'; +import { Config, GetConfig } from '../config'; import { Auth0RequestCookies, Auth0ResponseCookies } from '../http'; const debug = createDebug('session'); @@ -36,7 +36,11 @@ const assert = (bool: boolean, msg: string) => { }; export abstract class AbstractSession<Session> { - constructor(protected config: Config) {} + protected getConfig: () => Config | Promise<Config>; + + constructor(getConfig: GetConfig) { + this.getConfig = typeof getConfig === 'function' ? getConfig : () => getConfig; + } abstract getSession(req: Auth0RequestCookies): Promise<SessionPayload<Session> | undefined | null>; @@ -58,7 +62,8 @@ export abstract class AbstractSession<Session> { ): Promise<void>; public async read(req: Auth0RequestCookies): Promise<[Session?, number?]> { - const { rollingDuration, absoluteDuration } = this.config.session; + const config = await this.getConfig(); + const { rollingDuration, absoluteDuration } = config.session; try { const existingSessionValue = await this.getSession(req); @@ -95,9 +100,10 @@ export abstract class AbstractSession<Session> { session: Session | null | undefined, createdAt?: number ): Promise<void> { + const config = await this.getConfig(); const { cookie: { transient, ...cookieConfig } - } = this.config.session; + } = config.session; if (!session) { await this.deleteSession(req, res, cookieConfig); @@ -107,7 +113,7 @@ export abstract class AbstractSession<Session> { const isNewSession = typeof createdAt === 'undefined'; const uat = epoch(); const iat = typeof createdAt === 'number' ? createdAt : uat; - const exp = this.calculateExp(iat, uat); + const exp = this.calculateExp(iat, uat, config); const cookieOptions: CookieSerializeOptions = { ...cookieConfig @@ -119,9 +125,9 @@ export abstract class AbstractSession<Session> { await this.setSession(req, res, session, uat, iat, exp, cookieOptions, isNewSession); } - private calculateExp(iat: number, uat: number): number { - const { absoluteDuration } = this.config.session; - const { rolling, rollingDuration } = this.config.session; + private calculateExp(iat: number, uat: number, config: Config): number { + const { absoluteDuration } = config.session; + const { rolling, rollingDuration } = config.session; if (typeof absoluteDuration !== 'number') { return uat + (rollingDuration as number); diff --git a/src/auth0-session/session/stateful-session.ts b/src/auth0-session/session/stateful-session.ts index c83cf84d3..e3b64561a 100644 --- a/src/auth0-session/session/stateful-session.ts +++ b/src/auth0-session/session/stateful-session.ts @@ -1,6 +1,5 @@ import { CookieSerializeOptions } from 'cookie'; import createDebug from '../utils/debug'; -import { Config } from '../config'; import { AbstractSession, SessionPayload } from './abstract-session'; import { generateCookieValue, getCookieValue } from '../utils/signed-cookies'; import { signing } from '../utils/hkdf'; @@ -29,16 +28,20 @@ export class StatefulSession< Session extends { [key: string]: any } = { [key: string]: any } > extends AbstractSession<Session> { private keys?: Uint8Array[]; - private store: SessionStore<Session>; + private store?: SessionStore<Session>; - constructor(protected config: Config) { - super(config); - this.store = config.session.store as SessionStore<Session>; + private async getStore(): Promise<SessionStore<Session>> { + if (!this.store) { + const config = await this.getConfig(); + this.store = config.session.store as SessionStore<Session>; + } + return this.store; } private async getKeys(): Promise<Uint8Array[]> { if (!this.keys) { - const secret = this.config.secret; + const config = await this.getConfig(); + const secret = config.secret; const secrets = Array.isArray(secret) ? secret : [secret]; this.keys = await Promise.all(secrets.map(signing)); } @@ -46,14 +49,16 @@ export class StatefulSession< } async getSession(req: Auth0RequestCookies): Promise<SessionPayload<Session> | undefined | null> { - const { name: sessionName } = this.config.session; + const config = await this.getConfig(); + const { name: sessionName } = config.session; const cookies = req.getCookies(); const keys = await this.getKeys(); const sessionId = await getCookieValue(sessionName, cookies[sessionName], keys); if (sessionId) { + const store = await this.getStore(); debug('reading session from %s store', sessionId); - return this.store.get(sessionId); + return store.get(sessionId); } return; } @@ -68,7 +73,9 @@ export class StatefulSession< cookieOptions: CookieSerializeOptions, isNewSession: boolean ): Promise<void> { - const { name: sessionName, genId } = this.config.session; + const config = await this.getConfig(); + const store = await this.getStore(); + const { name: sessionName, genId } = config.session; const cookies = req.getCookies(); const keys = await this.getKeys(); let sessionId = await getCookieValue(sessionName, cookies[sessionName], keys); @@ -77,7 +84,7 @@ export class StatefulSession< // from the store and regenerate the session id to prevent session fixation issue. if (sessionId && isNewSession) { debug('regenerating session id %o to prevent session fixation', sessionId); - await this.store.delete(sessionId); + await store.delete(sessionId); sessionId = undefined; } @@ -88,7 +95,7 @@ export class StatefulSession< debug('set session %o', sessionId); const cookieValue = await generateCookieValue(sessionName, sessionId, keys[0]); res.setCookie(sessionName, cookieValue, cookieOptions); - await this.store.set(sessionId, { + await store.set(sessionId, { header: { iat, uat, exp }, data: session }); @@ -99,15 +106,17 @@ export class StatefulSession< res: Auth0ResponseCookies, cookieOptions: CookieSerializeOptions ): Promise<void> { - const { name: sessionName } = this.config.session; + const config = await this.getConfig(); + const { name: sessionName } = config.session; const cookies = req.getCookies(); const keys = await this.getKeys(); const sessionId = await getCookieValue(sessionName, cookies[sessionName], keys); if (sessionId) { + const store = await this.getStore(); debug('deleting session %o', sessionId); res.clearCookie(sessionName, cookieOptions); - await this.store.delete(sessionId); + await store.delete(sessionId); } } } diff --git a/src/auth0-session/session/stateless-session.ts b/src/auth0-session/session/stateless-session.ts index 34ee07bcc..31f2f2d1e 100644 --- a/src/auth0-session/session/stateless-session.ts +++ b/src/auth0-session/session/stateless-session.ts @@ -18,28 +18,36 @@ export class StatelessSession< Session extends { [key: string]: any } = { [key: string]: any } > extends AbstractSession<Session> { private keys?: Uint8Array[]; - private chunkSize: number; + private chunkSize?: number; constructor(protected config: Config) { super(config); - const { - cookie: { transient, ...cookieConfig }, - name: sessionName - } = this.config.session; - const cookieOptions: CookieSerializeOptions = { - ...cookieConfig - }; - if (!transient) { - cookieOptions.expires = new Date(); - } + } - const emptyCookie = serialize(`${sessionName}.0`, '', cookieOptions); - this.chunkSize = MAX_COOKIE_SIZE - emptyCookie.length; + private async getChunkSize(): Promise<number> { + if (this.chunkSize === undefined) { + const config = await this.getConfig(); + const { + cookie: { transient, ...cookieConfig }, + name: sessionName + } = config.session; + const cookieOptions: CookieSerializeOptions = { + ...cookieConfig + }; + if (!transient) { + cookieOptions.expires = new Date(); + } + + const emptyCookie = serialize(`${sessionName}.0`, '', cookieOptions); + this.chunkSize = MAX_COOKIE_SIZE - emptyCookie.length; + } + return this.chunkSize; } private async getKeys(): Promise<Uint8Array[]> { if (!this.keys) { - const secret = this.config.secret; + const config = await this.getConfig(); + const secret = config.secret; const secrets = Array.isArray(secret) ? secret : [secret]; this.keys = await Promise.all(secrets.map(encryption)); } @@ -65,7 +73,8 @@ export class StatelessSession< } async getSession(req: Auth0RequestCookies): Promise<SessionPayload<Session> | undefined | null> { - const { name: sessionName } = this.config.session; + const config = await this.getConfig(); + const { name: sessionName } = config.session; const cookies = req.getCookies(); let existingSessionValue: string | undefined; if (sessionName in cookies) { @@ -112,13 +121,15 @@ export class StatelessSession< exp: number, cookieOptions: CookieSerializeOptions ): Promise<void> { - const { name: sessionName } = this.config.session; + const config = await this.getConfig(); + const { name: sessionName } = config.session; const cookies = req.getCookies(); debug('found session, creating signed session cookie(s) with name %o(.i)', sessionName); const value = await this.encrypt(session, { iat, uat, exp }); - const chunkCount = Math.ceil(value.length / this.chunkSize); + const chunkSize = await this.getChunkSize(); + const chunkCount = Math.ceil(value.length / chunkSize); const existingCookies = new Set( Object.keys(cookies).filter((cookie) => cookie.match(`^${sessionName}(?:\\.\\d)?$`)) @@ -127,7 +138,7 @@ export class StatelessSession< if (chunkCount > 1) { debug('cookie size greater than %d, chunking', this.chunkSize); for (let i = 0; i < chunkCount; i++) { - const chunkValue = value.slice(i * this.chunkSize, (i + 1) * this.chunkSize); + const chunkValue = value.slice(i * chunkSize, (i + 1) * chunkSize); const chunkCookieName = `${sessionName}.${i}`; res.setCookie(chunkCookieName, chunkValue, cookieOptions); existingCookies.delete(chunkCookieName); @@ -147,7 +158,8 @@ export class StatelessSession< res: Auth0ResponseCookies, cookieOptions: CookieSerializeOptions ): Promise<void> { - const { name: sessionName } = this.config.session; + const config = await this.getConfig(); + const { name: sessionName } = config.session; const cookies = req.getCookies(); for (const cookieName of Object.keys(cookies)) { diff --git a/src/auth0-session/transient-store.ts b/src/auth0-session/transient-store.ts index ee89457b2..ca7fcbdd0 100644 --- a/src/auth0-session/transient-store.ts +++ b/src/auth0-session/transient-store.ts @@ -1,6 +1,6 @@ import { generateCookieValue, getCookieValue } from './utils/signed-cookies'; import { signing } from './utils/hkdf'; -import { Config } from './config'; +import { Config, GetConfig } from './config'; import { Auth0Request, Auth0Response } from './http'; export interface StoreOptions { @@ -11,11 +11,16 @@ export interface StoreOptions { export default class TransientStore { private keys?: Uint8Array[]; - constructor(private config: Config) {} + protected getConfig: () => Config | Promise<Config>; + + constructor(getConfig: GetConfig) { + this.getConfig = typeof getConfig === 'function' ? getConfig : () => getConfig; + } private async getKeys(): Promise<Uint8Array[]> { if (!this.keys) { - const secret = this.config.secret; + const config = await this.getConfig(); + const secret = config.secret; const secrets = Array.isArray(secret) ? secret : [secret]; this.keys = await Promise.all(secrets.map(signing)); } @@ -41,7 +46,8 @@ export default class TransientStore { { sameSite = 'none', value }: StoreOptions ): Promise<string> { const isSameSiteNone = sameSite === 'none'; - const { domain, path, secure } = this.config.transactionCookie; + const config = await this.getConfig(); + const { domain, path, secure } = config.transactionCookie; const basicAttr = { httpOnly: true, secure, @@ -60,7 +66,7 @@ export default class TransientStore { }); } - if (isSameSiteNone && this.config.legacySameSiteCookie) { + if (isSameSiteNone && config.legacySameSiteCookie) { const cookieValue = await generateCookieValue(`_${key}`, value, signingKey); // Set the fallback cookie with no SameSite or Secure attributes. res.setCookie(`_${key}`, cookieValue, basicAttr); @@ -81,13 +87,14 @@ export default class TransientStore { async read(key: string, req: Auth0Request, res: Auth0Response): Promise<string | undefined> { const cookies = req.getCookies(); const cookie = cookies[key]; - const cookieConfig = this.config.transactionCookie; + const config = await this.getConfig(); + const cookieConfig = config.transactionCookie; const verifyingKeys = await this.getKeys(); let value = await getCookieValue(key, cookie, verifyingKeys); res.clearCookie(key, cookieConfig); - if (this.config.legacySameSiteCookie) { + if (config.legacySameSiteCookie) { const fallbackKey = `_${key}`; if (!value) { const fallbackCookie = cookies[fallbackKey]; diff --git a/tests/auth0-session/client/edge-client.test.ts b/tests/auth0-session/client/edge-client.test.ts index 48b7805c9..a4c9afb48 100644 --- a/tests/auth0-session/client/edge-client.test.ts +++ b/tests/auth0-session/client/edge-client.test.ts @@ -293,7 +293,8 @@ describe('edge client', function () { }); it('should only support code flow', async () => { - await expect(getClient({ authorizationParams: { response_type: 'id_token' } })).rejects.toThrow( + const client = await getClient({ authorizationParams: { response_type: 'id_token' } }); + await expect(client.authorizationUrl({})).rejects.toThrow( 'This SDK only supports `response_type=code` when used in an Edge runtime.' ); }); diff --git a/tests/auth0-session/client/node-client.test.ts b/tests/auth0-session/client/node-client.test.ts index 1aa983d51..c68fae9d2 100644 --- a/tests/auth0-session/client/node-client.test.ts +++ b/tests/auth0-session/client/node-client.test.ts @@ -69,6 +69,21 @@ describe('node client', function () { expect(headerProps).not.toContain('auth0-client'); }); + it('should accept lazy config', async function () { + expect( + () => + new NodeClient( + () => { + throw new Error(); + }, + { + name: 'nextjs-auth0', + version + } + ) + ).not.toThrow(); + }); + it('should not strip new headers', async function () { const client = await getClient(); const response = await client.userinfo('__test_token__'); diff --git a/tests/auth0-session/handlers/callback.test.ts b/tests/auth0-session/handlers/callback.test.ts index 332eaeebf..5623c79fd 100644 --- a/tests/auth0-session/handlers/callback.test.ts +++ b/tests/auth0-session/handlers/callback.test.ts @@ -10,6 +10,7 @@ import { IncomingMessage, ServerResponse } from 'http'; import { readFileSync } from 'fs'; import { join } from 'path'; import * as qs from 'querystring'; +import callbackHandlerFactory from '../../../src/auth0-session/handlers/callback'; const privateKey = readFileSync(join(__dirname, '..', 'fixtures', 'private-key.pem'), 'utf-8'); @@ -20,6 +21,13 @@ const authVerificationCookie = (cookies: Record<string, string>) => ({ auth_veri describe('callback', () => { afterEach(teardown); + it('should accept lazy config', () => { + const getConfig = () => { + throw new Error(); + }; + expect(() => (callbackHandlerFactory as any)(getConfig)).not.toThrow(); + }); + it('should error when the body is empty', async () => { const baseURL = await setup(defaultConfig); diff --git a/tests/auth0-session/handlers/login.test.ts b/tests/auth0-session/handlers/login.test.ts index 6b01a47d8..a9f16ff87 100644 --- a/tests/auth0-session/handlers/login.test.ts +++ b/tests/auth0-session/handlers/login.test.ts @@ -4,12 +4,20 @@ import { setup, teardown } from '../fixtures/server'; import { defaultConfig, fromCookieJar, get, getCookie } from '../fixtures/helpers'; import { decodeState, encodeState } from '../../../src/auth0-session/utils/encoding'; import { LoginOptions } from '../../../src/auth0-session'; +import loginHandlerFactory from '../../../src/auth0-session/handlers/login'; const authVerificationCookie = (cookie: string) => JSON.parse(decodeURIComponent(cookie)); describe('login', () => { afterEach(teardown); + it('should accept lazy config', () => { + const getConfig = () => { + throw new Error(); + }; + expect(() => (loginHandlerFactory as any)(getConfig)).not.toThrow(); + }); + it('should redirect to the authorize url for /login', async () => { const baseURL = await setup(defaultConfig); const cookieJar = new CookieJar(); diff --git a/tests/auth0-session/handlers/logout.test.ts b/tests/auth0-session/handlers/logout.test.ts index 2afb75209..83517b9ad 100644 --- a/tests/auth0-session/handlers/logout.test.ts +++ b/tests/auth0-session/handlers/logout.test.ts @@ -6,6 +6,7 @@ import { toSignedCookieJar, defaultConfig, get, post, fromCookieJar } from '../f import { makeIdToken } from '../fixtures/cert'; import { encodeState } from '../../../src/auth0-session/utils/encoding'; import wellKnown from '../fixtures/well-known.json'; +import logoutHandlerFactory from '../../../src/auth0-session/handlers/logout'; const login = async (baseURL: string): Promise<CookieJar> => { const nonce = '__test_nonce__'; @@ -24,6 +25,13 @@ const login = async (baseURL: string): Promise<CookieJar> => { describe('logout route', () => { afterEach(teardown); + it('should accept lazy config', () => { + const getConfig = () => { + throw new Error(); + }; + expect(() => (logoutHandlerFactory as any)(getConfig)).not.toThrow(); + }); + it('should perform a local logout', async () => { const baseURL = await setup({ ...defaultConfig, idpLogout: false }); const cookieJar = await login(baseURL); diff --git a/tests/auth0-session/session/stateless-session.test.ts b/tests/auth0-session/session/stateless-session.test.ts index 7eb01c336..b00d5d95f 100644 --- a/tests/auth0-session/session/stateless-session.test.ts +++ b/tests/auth0-session/session/stateless-session.test.ts @@ -9,6 +9,7 @@ import { setup, teardown } from '../fixtures/server'; import { defaultConfig, fromCookieJar, get, toCookieJar } from '../fixtures/helpers'; import { encryption } from '../../../src/auth0-session/utils/hkdf'; import { makeIdToken } from '../fixtures/cert'; +import { StatelessSession } from '../../../src/auth0-session'; const hr = 60 * 60 * 1000; const day = 24 * hr; @@ -38,6 +39,15 @@ const encrypted = async (claims: Partial<IdTokenClaims> = { sub: '__test_sub__' describe('StatelessSession', () => { afterEach(teardown); + it('should accept lazy config', () => { + expect( + () => + new StatelessSession((() => { + throw new Error(); + }) as any) + ).not.toThrow(); + }); + it('should not create a session when there are no cookies', async () => { const baseURL = await setup(defaultConfig); await expect(get(baseURL, '/session')).rejects.toThrowError('Unauthorized'); diff --git a/tests/auth0-session/transient-store.test.ts b/tests/auth0-session/transient-store.test.ts index 426371aeb..898ae0346 100644 --- a/tests/auth0-session/transient-store.test.ts +++ b/tests/auth0-session/transient-store.test.ts @@ -30,6 +30,15 @@ const setup = async ( describe('TransientStore', () => { afterEach(teardown); + it('should accept lazy config', () => { + expect( + () => + new TransientStore(() => { + throw new Error(); + }) + ).not.toThrow(); + }); + it('should use the passed-in key to set the cookies', async () => { const baseURL: string = await setup(defaultConfig, async (req: NodeRequest, res: NodeResponse) => transientStore.save('test_key', req, res, { value: 'foo' }) From d79ff8f59accdf0535d616ae80dc57de343317e9 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath <adamjmcgrath@gmail.com> Date: Thu, 2 Nov 2023 14:18:05 +0000 Subject: [PATCH 2/7] Allow lazy config in Next.js layer --- package.json | 1 + src/auth0-session/client/abstract-client.ts | 8 +- src/auth0-session/client/edge-client.ts | 172 +++---- src/auth0-session/client/node-client.ts | 271 ++++++----- src/auth0-session/config.ts | 3 +- src/auth0-session/handlers/callback.ts | 9 +- src/auth0-session/handlers/login.ts | 7 +- src/auth0-session/handlers/logout.ts | 7 +- src/auth0-session/index.ts | 12 +- src/auth0-session/session-cache.ts | 2 +- src/auth0-session/session/abstract-session.ts | 6 +- src/auth0-session/session/stateful-session.ts | 25 +- .../session/stateless-session.ts | 26 +- src/auth0-session/transient-store.ts | 19 +- src/config.ts | 428 ++---------------- src/edge.ts | 25 +- src/handlers/callback.ts | 33 +- src/handlers/login.ts | 45 +- src/handlers/logout.ts | 1 - src/handlers/profile.ts | 25 +- src/helpers/testing.ts | 7 +- src/helpers/with-middleware-auth-required.ts | 11 +- src/helpers/with-page-auth-required.ts | 23 +- src/index.ts | 28 +- src/init.ts | 55 +-- src/session/cache.ts | 52 ++- src/session/get-access-token.ts | 16 +- src/session/session.ts | 6 +- .../auth0-session/client/edge-client.test.ts | 29 +- .../auth0-session/client/node-client.test.ts | 38 +- tests/auth0-session/fixtures/server.ts | 16 +- tests/config.test.ts | 62 +-- tests/fixtures/app-router-helpers.ts | 2 +- tests/helpers/testing.test.ts | 17 +- tests/index.test.ts | 19 +- tests/session/cache.test.ts | 6 +- tests/session/session.test.ts | 63 ++- 37 files changed, 612 insertions(+), 963 deletions(-) diff --git a/package.json b/package.json index ec0c11510..de45cebaf 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "!<rootDir>/src/edge.ts", "!<rootDir>/src/index.ts", "!<rootDir>/src/shared.ts", + "!<rootDir>/src/version.ts", "!<rootDir>/src/auth0-session/config.ts", "!<rootDir>/src/auth0-session/index.ts", "!<rootDir>/src/auth0-session/session-cache.ts" diff --git a/src/auth0-session/client/abstract-client.ts b/src/auth0-session/client/abstract-client.ts index 25f0c9360..48f92edfd 100644 --- a/src/auth0-session/client/abstract-client.ts +++ b/src/auth0-session/client/abstract-client.ts @@ -1,5 +1,5 @@ -import { Config, GetConfig } from '../config'; import { Auth0Request } from '../http'; +import { Config } from '../config'; export type Telemetry = { name: string; @@ -85,10 +85,6 @@ export interface AuthorizationParameters { } export abstract class AbstractClient { - protected getConfig: () => Config | Promise<Config>; - constructor(getConfig: GetConfig, protected telemetry: Telemetry) { - this.getConfig = typeof getConfig === 'function' ? getConfig : () => getConfig; - } abstract authorizationUrl(parameters: Record<string, unknown>): Promise<string>; abstract callbackParams(req: Auth0Request, expectedState: string): Promise<URLSearchParams>; abstract callback( @@ -107,3 +103,5 @@ export abstract class AbstractClient { abstract generateRandomNonce(): string; abstract calculateCodeChallenge(codeVerifier: string): Promise<string> | string; } + +export type GetClient = (config: Config) => Promise<AbstractClient>; diff --git a/src/auth0-session/client/edge-client.ts b/src/auth0-session/client/edge-client.ts index 1480f7bca..4d80ccb48 100644 --- a/src/auth0-session/client/edge-client.ts +++ b/src/auth0-session/client/edge-client.ts @@ -6,11 +6,13 @@ import { OpenIDCallbackChecks, TokenEndpointResponse, AbstractClient, - EndSessionParameters + EndSessionParameters, + Telemetry } from './abstract-client'; import { ApplicationError, DiscoveryError, IdentityProviderError, UserInfoError } from '../utils/errors'; import { AccessTokenError, AccessTokenErrorCode } from '../../utils/errors'; import urlJoin from 'url-join'; +import { Config } from '../config'; const encodeBase64 = (input: string) => { const unencoded = new TextEncoder().encode(input); @@ -24,68 +26,18 @@ const encodeBase64 = (input: string) => { }; export class EdgeClient extends AbstractClient { - private client?: oauth.Client; - private as?: oauth.AuthorizationServer; - - private async httpOptions(): Promise<oauth.HttpRequestOptions> { - const headers = new Headers(); - const config = await this.getConfig(); - if (config.enableTelemetry) { - const { name, version } = this.telemetry; - headers.set('User-Agent', `${name}/${version}`); - headers.set( - 'Auth0-Client', - encodeBase64( - JSON.stringify({ - name, - version, - env: { - edge: true - } - }) - ) - ); - } - return { - signal: AbortSignal.timeout(config.httpTimeout), - headers - }; - } - - private async getClient(): Promise<[oauth.AuthorizationServer, oauth.Client]> { - if (this.as) { - return [this.as, this.client as oauth.Client]; - } - const config = await this.getConfig(); - if (config.authorizationParams.response_type !== 'code') { - throw new Error('This SDK only supports `response_type=code` when used in an Edge runtime.'); - } - - const issuer = new URL(config.issuerBaseURL); - try { - this.as = await oauth - .discoveryRequest(issuer, await this.httpOptions()) - .then((response) => oauth.processDiscoveryResponse(issuer, response)); - } catch (e) { - throw new DiscoveryError(e, config.issuerBaseURL); - } - - this.client = { - client_id: config.clientID, - ...(!config.clientAssertionSigningKey && { client_secret: config.clientSecret }), - token_endpoint_auth_method: config.clientAuthMethod, - id_token_signed_response_alg: config.idTokenSigningAlg, - [oauth.clockTolerance]: config.clockTolerance - }; - - return [this.as, this.client]; + constructor( + private client: oauth.Client, + private as: oauth.AuthorizationServer, + private config: Config, + private httpOptions: oauth.HttpRequestOptions + ) { + super(); } async authorizationUrl(parameters: Record<string, unknown>): Promise<string> { - const [as] = await this.getClient(); - const config = await this.getConfig(); - const authorizationUrl = new URL(as.authorization_endpoint as string); - authorizationUrl.searchParams.set('client_id', config.clientID); + const authorizationUrl = new URL(this.as.authorization_endpoint as string); + authorizationUrl.searchParams.set('client_id', this.config.clientID); Object.entries(parameters).forEach(([key, value]) => { if (value === null || value === undefined) { return; @@ -96,12 +48,11 @@ export class EdgeClient extends AbstractClient { } async callbackParams(req: Auth0Request, expectedState: string) { - const [as, client] = await this.getClient(); const url = req.getMethod().toUpperCase() === 'GET' ? new URL(req.getUrl()) : new URLSearchParams(await req.getBody()); let result: ReturnType<typeof oauth.validateAuthResponse>; try { - result = oauth.validateAuthResponse(as, client, url, expectedState); + result = oauth.validateAuthResponse(this.as, this.client, url, expectedState); } catch (e) { throw new ApplicationError(e); } @@ -121,8 +72,7 @@ export class EdgeClient extends AbstractClient { checks: OpenIDCallbackChecks, extras: CallbackExtras ): Promise<TokenEndpointResponse> { - const [as, client] = await this.getClient(); - const { clientAssertionSigningKey, clientAssertionSigningAlg } = await this.getConfig(); + const { clientAssertionSigningKey, clientAssertionSigningAlg } = this.config; let clientPrivateKey = clientAssertionSigningKey as CryptoKey | undefined; /* c8 ignore next 3 */ @@ -130,21 +80,21 @@ export class EdgeClient extends AbstractClient { clientPrivateKey = await jose.importPKCS8<CryptoKey>(clientPrivateKey, clientAssertionSigningAlg || 'RS256'); } const response = await oauth.authorizationCodeGrantRequest( - as, - client, + this.as, + this.client, parameters, redirectUri, checks.code_verifier as string, { additionalParameters: extras.exchangeBody, ...(clientPrivateKey && { clientPrivateKey }), - ...this.httpOptions() + ...this.httpOptions } ); const result = await oauth.processAuthorizationCodeOpenIDResponse( - as, - client, + this.as, + this.client, response, checks.nonce, checks.max_age @@ -160,18 +110,16 @@ export class EdgeClient extends AbstractClient { } async endSessionUrl(parameters: EndSessionParameters): Promise<string> { - const [as] = await this.getClient(); - const issuerUrl = new URL(as.issuer); - const config = await this.getConfig(); + const issuerUrl = new URL(this.as.issuer); if ( - config.idpLogout && - (config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && config.auth0Logout !== false)) + this.config.idpLogout && + (this.config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && this.config.auth0Logout !== false)) ) { const { id_token_hint, post_logout_redirect_uri, ...extraParams } = parameters; - const auth0LogoutUrl: URL = new URL(urlJoin(as.issuer, '/v2/logout')); + const auth0LogoutUrl: URL = new URL(urlJoin(this.as.issuer, '/v2/logout')); post_logout_redirect_uri && auth0LogoutUrl.searchParams.set('returnTo', post_logout_redirect_uri); - auth0LogoutUrl.searchParams.set('client_id', config.clientID); + auth0LogoutUrl.searchParams.set('client_id', this.config.clientID); Object.entries(extraParams).forEach(([key, value]: [string, string]) => { if (value === null || value === undefined) { return; @@ -180,10 +128,10 @@ export class EdgeClient extends AbstractClient { }); return auth0LogoutUrl.toString(); } - if (!as.end_session_endpoint) { + if (!this.as.end_session_endpoint) { throw new Error('RP Initiated Logout is not supported on your Authorization Server.'); } - const oidcLogoutUrl = new URL(as.end_session_endpoint); + const oidcLogoutUrl = new URL(this.as.end_session_endpoint); Object.entries(parameters).forEach(([key, value]: [string, string]) => { if (value === null || value === undefined) { return; @@ -191,28 +139,26 @@ export class EdgeClient extends AbstractClient { oidcLogoutUrl.searchParams.set(key, value); }); - oidcLogoutUrl.searchParams.set('client_id', config.clientID); + oidcLogoutUrl.searchParams.set('client_id', this.config.clientID); return oidcLogoutUrl.toString(); } async userinfo(accessToken: string): Promise<Record<string, unknown>> { - const [as, client] = await this.getClient(); - const response = await oauth.userInfoRequest(as, client, accessToken, await this.httpOptions()); + const response = await oauth.userInfoRequest(this.as, this.client, accessToken, this.httpOptions); try { - return await oauth.processUserInfoResponse(as, client, oauth.skipSubjectCheck, response); + return await oauth.processUserInfoResponse(this.as, this.client, oauth.skipSubjectCheck, response); } catch (e) { throw new UserInfoError(e.message); } } async refresh(refreshToken: string, extras: { exchangeBody: Record<string, any> }): Promise<TokenEndpointResponse> { - const [as, client] = await this.getClient(); - const res = await oauth.refreshTokenGrantRequest(as, client, refreshToken, { + const res = await oauth.refreshTokenGrantRequest(this.as, this.client, refreshToken, { additionalParameters: extras.exchangeBody, - ...this.httpOptions() + ...this.httpOptions }); - const result = await oauth.processRefreshTokenResponse(as, client, res); + const result = await oauth.processRefreshTokenResponse(this.as, this.client, res); if (oauth.isOAuth2Error(result)) { throw new AccessTokenError( AccessTokenErrorCode.FAILED_REFRESH_GRANT, @@ -239,3 +185,57 @@ export class EdgeClient extends AbstractClient { return oauth.calculatePKCECodeChallenge(codeVerifier); } } + +export const clientGetter = (telemetry: Telemetry): ((config: Config) => Promise<EdgeClient>) => { + let client: EdgeClient; + return async (config) => { + if (!client) { + const headers = new Headers(); + if (config.enableTelemetry) { + const { name, version } = telemetry; + headers.set('User-Agent', `${name}/${version}`); + headers.set( + 'Auth0-Client', + encodeBase64( + JSON.stringify({ + name, + version, + env: { + edge: true + } + }) + ) + ); + } + const httpOptions: oauth.HttpRequestOptions = { + signal: AbortSignal.timeout(config.httpTimeout), + headers + }; + + if (config.authorizationParams.response_type !== 'code') { + throw new Error('This SDK only supports `response_type=code` when used in an Edge runtime.'); + } + + const issuer = new URL(config.issuerBaseURL); + let as: oauth.AuthorizationServer; + try { + as = await oauth + .discoveryRequest(issuer, httpOptions) + .then((response) => oauth.processDiscoveryResponse(issuer, response)); + } catch (e) { + throw new DiscoveryError(e, config.issuerBaseURL); + } + + const oauthClient: oauth.Client = { + client_id: config.clientID, + ...(!config.clientAssertionSigningKey && { client_secret: config.clientSecret }), + token_endpoint_auth_method: config.clientAuthMethod, + id_token_signed_response_alg: config.idTokenSigningAlg, + [oauth.clockTolerance]: config.clockTolerance + }; + + client = new EdgeClient(oauthClient, as, config, httpOptions); + } + return client; + }; +}; diff --git a/src/auth0-session/client/node-client.ts b/src/auth0-session/client/node-client.ts index 683f1f306..da56a4b52 100644 --- a/src/auth0-session/client/node-client.ts +++ b/src/auth0-session/client/node-client.ts @@ -4,7 +4,8 @@ import { CallbackParamsType, OpenIDCallbackChecks, TokenEndpointResponse, - AbstractClient + AbstractClient, + Telemetry } from './abstract-client'; import { Client, @@ -23,6 +24,7 @@ import urlJoin from 'url-join'; import createDebug from '../utils/debug'; import { IncomingMessage } from 'http'; import { AccessTokenError, AccessTokenErrorCode } from '../../utils/errors'; +import { Config } from '../config'; const debug = createDebug('client'); @@ -31,140 +33,16 @@ function sortSpaceDelimitedString(str: string): string { } export class NodeClient extends AbstractClient { - private client?: Client; - - private async getClient(): Promise<Client> { - if (this.client) { - return this.client; - } - const { - getConfig, - telemetry: { name, version } - } = this; - const config = await getConfig(); - - const defaultHttpOptions: CustomHttpOptionsProvider = (_url, options) => ({ - ...options, - headers: { - ...options.headers, - 'User-Agent': `${name}/${version}`, - ...(config.enableTelemetry - ? { - 'Auth0-Client': Buffer.from( - JSON.stringify({ - name, - version, - env: { - node: process.version - } - }) - ).toString('base64') - } - : undefined) - }, - timeout: config.httpTimeout, - agent: config.httpAgent - }); - const applyHttpOptionsCustom = (entity: Issuer<Client> | typeof Issuer | Client) => { - entity[custom.http_options] = defaultHttpOptions; - }; - - applyHttpOptionsCustom(Issuer); - let issuer: Issuer<Client>; - try { - issuer = await Issuer.discover(config.issuerBaseURL); - } catch (e) { - throw new DiscoveryError(e, config.issuerBaseURL); - } - applyHttpOptionsCustom(issuer); - - const issuerTokenAlgs = Array.isArray(issuer.id_token_signing_alg_values_supported) - ? issuer.id_token_signing_alg_values_supported - : []; - if (!issuerTokenAlgs.includes(config.idTokenSigningAlg)) { - debug( - 'ID token algorithm %o is not supported by the issuer. Supported ID token algorithms are: %o.', - config.idTokenSigningAlg, - issuerTokenAlgs - ); - } - - const configRespType = sortSpaceDelimitedString(config.authorizationParams.response_type); - const issuerRespTypes = Array.isArray(issuer.response_types_supported) ? issuer.response_types_supported : []; - issuerRespTypes.map(sortSpaceDelimitedString); - if (!issuerRespTypes.includes(configRespType)) { - debug( - 'Response type %o is not supported by the issuer. Supported response types are: %o.', - configRespType, - issuerRespTypes - ); - } - - const configRespMode = config.authorizationParams.response_mode; - const issuerRespModes = Array.isArray(issuer.response_modes_supported) ? issuer.response_modes_supported : []; - if (configRespMode && !issuerRespModes.includes(configRespMode)) { - debug( - 'Response mode %o is not supported by the issuer. Supported response modes are %o.', - configRespMode, - issuerRespModes - ); - } - - let jwks; - if (config.clientAssertionSigningKey) { - const privateKey = createPrivateKey({ key: config.clientAssertionSigningKey as string }); - const jwk = await exportJWK(privateKey); - jwks = { keys: [jwk] }; - } - - this.client = new issuer.Client( - { - client_id: config.clientID, - client_secret: config.clientSecret, - id_token_signed_response_alg: config.idTokenSigningAlg, - token_endpoint_auth_method: config.clientAuthMethod as ClientAuthMethod, - token_endpoint_auth_signing_alg: config.clientAssertionSigningAlg - }, - jwks - ); - applyHttpOptionsCustom(this.client); - - this.client[custom.clock_tolerance] = config.clockTolerance; - const issuerUrl = new URL(issuer.metadata.issuer); - - if (config.idpLogout) { - if (config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && config.auth0Logout !== false)) { - Object.defineProperty(this.client, 'endSessionUrl', { - value(params: EndSessionParameters) { - const { id_token_hint, post_logout_redirect_uri, ...extraParams } = params; - const parsedUrl = new URL(urlJoin(issuer.metadata.issuer, '/v2/logout')); - parsedUrl.searchParams.set('client_id', config.clientID); - post_logout_redirect_uri && parsedUrl.searchParams.set('returnTo', post_logout_redirect_uri); - Object.entries(extraParams).forEach(([key, value]) => { - if (value === null || value === undefined) { - return; - } - parsedUrl.searchParams.set(key, value as string); - }); - return parsedUrl.toString(); - } - }); - } else if (!issuer.end_session_endpoint) { - debug('the issuer does not support RP-Initiated Logout'); - } - } - - return this.client; + constructor(private client: Client) { + super(); } async authorizationUrl(parameters: Record<string, unknown>): Promise<string> { - const client = await this.getClient(); - return client.authorizationUrl(parameters); + return this.client.authorizationUrl(parameters); } async callbackParams(req: Auth0Request) { - const client = await this.getClient(); - const obj: CallbackParamsType = client.callbackParams({ + const obj: CallbackParamsType = this.client.callbackParams({ method: req.getMethod(), url: req.getUrl(), body: await req.getBody() @@ -179,9 +57,8 @@ export class NodeClient extends AbstractClient { extras: CallbackExtras ): Promise<TokenEndpointResponse> { const params = Object.fromEntries(parameters.entries()); - const client = await this.getClient(); try { - return await client.callback(redirectUri, params, checks, extras); + return await this.client.callback(redirectUri, params, checks, extras); } catch (err) { if (err instanceof errors.OPError) { throw new IdentityProviderError(err); @@ -195,23 +72,20 @@ export class NodeClient extends AbstractClient { } async endSessionUrl(parameters: EndSessionParameters): Promise<string> { - const client = await this.getClient(); - return client.endSessionUrl(parameters); + return this.client.endSessionUrl(parameters); } async userinfo(accessToken: string): Promise<Record<string, unknown>> { - const client = await this.getClient(); try { - return await client.userinfo(accessToken); + return await this.client.userinfo(accessToken); } catch (e) { throw new UserInfoError(e.message); } } async refresh(refreshToken: string, extras: { exchangeBody: Record<string, any> }): Promise<TokenEndpointResponse> { - const client = await this.getClient(); try { - return await client.refresh(refreshToken, extras); + return await this.client.refresh(refreshToken, extras); } catch (e) { throw new AccessTokenError( AccessTokenErrorCode.FAILED_REFRESH_GRANT, @@ -233,3 +107,126 @@ export class NodeClient extends AbstractClient { return generators.codeChallenge(codeVerifier); } } + +export const clientGetter = (telemetry: Telemetry): ((config: Config) => Promise<NodeClient>) => { + let client: NodeClient; + return async (config) => { + if (!client) { + const { name, version } = telemetry; + + const defaultHttpOptions: CustomHttpOptionsProvider = (_url, options) => ({ + ...options, + headers: { + ...options.headers, + 'User-Agent': `${name}/${version}`, + ...(config.enableTelemetry + ? { + 'Auth0-Client': Buffer.from( + JSON.stringify({ + name, + version, + env: { + node: process.version + } + }) + ).toString('base64') + } + : undefined) + }, + timeout: config.httpTimeout, + agent: config.httpAgent + }); + const applyHttpOptionsCustom = (entity: Issuer<Client> | typeof Issuer | Client) => { + entity[custom.http_options] = defaultHttpOptions; + }; + + applyHttpOptionsCustom(Issuer); + let issuer: Issuer<Client>; + try { + issuer = await Issuer.discover(config.issuerBaseURL); + } catch (e) { + throw new DiscoveryError(e, config.issuerBaseURL); + } + applyHttpOptionsCustom(issuer); + + const issuerTokenAlgs = Array.isArray(issuer.id_token_signing_alg_values_supported) + ? issuer.id_token_signing_alg_values_supported + : []; + if (!issuerTokenAlgs.includes(config.idTokenSigningAlg)) { + debug( + 'ID token algorithm %o is not supported by the issuer. Supported ID token algorithms are: %o.', + config.idTokenSigningAlg, + issuerTokenAlgs + ); + } + + const configRespType = sortSpaceDelimitedString(config.authorizationParams.response_type); + const issuerRespTypes = Array.isArray(issuer.response_types_supported) ? issuer.response_types_supported : []; + issuerRespTypes.map(sortSpaceDelimitedString); + if (!issuerRespTypes.includes(configRespType)) { + debug( + 'Response type %o is not supported by the issuer. Supported response types are: %o.', + configRespType, + issuerRespTypes + ); + } + + const configRespMode = config.authorizationParams.response_mode; + const issuerRespModes = Array.isArray(issuer.response_modes_supported) ? issuer.response_modes_supported : []; + if (configRespMode && !issuerRespModes.includes(configRespMode)) { + debug( + 'Response mode %o is not supported by the issuer. Supported response modes are %o.', + configRespMode, + issuerRespModes + ); + } + + let jwks; + if (config.clientAssertionSigningKey) { + const privateKey = createPrivateKey({ key: config.clientAssertionSigningKey as string }); + const jwk = await exportJWK(privateKey); + jwks = { keys: [jwk] }; + } + + const oidcClient = new issuer.Client( + { + client_id: config.clientID, + client_secret: config.clientSecret, + id_token_signed_response_alg: config.idTokenSigningAlg, + token_endpoint_auth_method: config.clientAuthMethod as ClientAuthMethod, + token_endpoint_auth_signing_alg: config.clientAssertionSigningAlg + }, + jwks + ); + applyHttpOptionsCustom(oidcClient); + + oidcClient[custom.clock_tolerance] = config.clockTolerance; + const issuerUrl = new URL(issuer.metadata.issuer); + + if (config.idpLogout) { + if (config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && config.auth0Logout !== false)) { + Object.defineProperty(oidcClient, 'endSessionUrl', { + value(params: EndSessionParameters) { + const { id_token_hint, post_logout_redirect_uri, ...extraParams } = params; + const parsedUrl = new URL(urlJoin(issuer.metadata.issuer, '/v2/logout')); + parsedUrl.searchParams.set('client_id', config.clientID); + post_logout_redirect_uri && parsedUrl.searchParams.set('returnTo', post_logout_redirect_uri); + Object.entries(extraParams).forEach(([key, value]) => { + if (value === null || value === undefined) { + return; + } + parsedUrl.searchParams.set(key, value as string); + }); + return parsedUrl.toString(); + } + }); + } else if (!issuer.end_session_endpoint) { + debug('the issuer does not support RP-Initiated Logout'); + } + } + + client = new NodeClient(oidcClient); + } + return client; + }; +}; diff --git a/src/auth0-session/config.ts b/src/auth0-session/config.ts index 191f4aa20..91c2b2644 100644 --- a/src/auth0-session/config.ts +++ b/src/auth0-session/config.ts @@ -4,6 +4,7 @@ import type { } from './client/abstract-client'; import type { Agent } from 'https'; import { SessionStore } from './session/stateful-session'; +import { Auth0RequestCookies } from './http'; /** * Configuration properties. @@ -365,4 +366,4 @@ export interface LogoutOptions { logoutParams?: { [key: string]: any }; } -export type GetConfig = Config | (() => Config | Promise<Config>); +export type GetConfig = Config | ((req: Auth0RequestCookies) => Config | Promise<Config>); diff --git a/src/auth0-session/handlers/callback.ts b/src/auth0-session/handlers/callback.ts index 47b8d889d..69be17d3d 100644 --- a/src/auth0-session/handlers/callback.ts +++ b/src/auth0-session/handlers/callback.ts @@ -5,7 +5,7 @@ import { decodeState } from '../utils/encoding'; import { SessionCache } from '../session-cache'; import { MalformedStateCookieError, MissingStateCookieError, MissingStateParamError } from '../utils/errors'; import { Auth0Request, Auth0Response } from '../http'; -import { AbstractClient } from '../client/abstract-client'; +import { GetClient } from '../client/abstract-client'; import type { AuthVerification } from './login'; function getRedirectUri(config: Config): string { @@ -26,13 +26,14 @@ export type HandleCallback = (req: Auth0Request, res: Auth0Response, options?: C export default function callbackHandlerFactory( getConfig: GetConfig, - client: AbstractClient, + getClient: GetClient, sessionCache: SessionCache, transientCookieHandler: TransientStore ): HandleCallback { const getConfigFn = typeof getConfig === 'function' ? getConfig : () => getConfig; return async (req, res, options) => { - const config = await getConfigFn(); + const config = await getConfigFn(req); + const client = await getClient(config); const redirectUri = options?.redirectUri || getRedirectUri(config); let tokenResponse; @@ -93,7 +94,7 @@ export default function callbackHandlerFactory( } const openidState: { returnTo?: string } = decodeState(expectedState as string)!; - let session = sessionCache.fromTokenEndpointResponse(tokenResponse); + let session = await sessionCache.fromTokenEndpointResponse(req, res, tokenResponse); if (options?.afterCallback) { session = await options.afterCallback(session, openidState); diff --git a/src/auth0-session/handlers/login.ts b/src/auth0-session/handlers/login.ts index 7bf3a98d0..06f24ba2d 100644 --- a/src/auth0-session/handlers/login.ts +++ b/src/auth0-session/handlers/login.ts @@ -4,7 +4,7 @@ import TransientStore from '../transient-store'; import { encodeState } from '../utils/encoding'; import createDebug from '../utils/debug'; import { Auth0Request, Auth0Response } from '../http'; -import { AbstractClient } from '../client/abstract-client'; +import { GetClient } from '../client/abstract-client'; const debug = createDebug('handlers'); @@ -24,12 +24,13 @@ export type AuthVerification = { export default function loginHandlerFactory( getConfig: GetConfig, - client: AbstractClient, + getClient: GetClient, transientHandler: TransientStore ): HandleLogin { const getConfigFn = typeof getConfig === 'function' ? getConfig : () => getConfig; return async (req, res, options = {}) => { - const config = await getConfigFn(); + const config = await getConfigFn(req); + const client = await getClient(config); const returnTo = options.returnTo || config.baseURL; const opts = { diff --git a/src/auth0-session/handlers/logout.ts b/src/auth0-session/handlers/logout.ts index 01cc01217..984301378 100644 --- a/src/auth0-session/handlers/logout.ts +++ b/src/auth0-session/handlers/logout.ts @@ -3,7 +3,7 @@ import createDebug from '../utils/debug'; import { GetConfig, LogoutOptions } from '../config'; import { SessionCache } from '../session-cache'; import { Auth0Request, Auth0Response } from '../http'; -import { AbstractClient } from '../client/abstract-client'; +import { GetClient } from '../client/abstract-client'; const debug = createDebug('logout'); @@ -11,12 +11,13 @@ export type HandleLogout = (req: Auth0Request, res: Auth0Response, options?: Log export default function logoutHandlerFactory( getConfig: GetConfig, - client: AbstractClient, + getClient: GetClient, sessionCache: SessionCache ): HandleLogout { const getConfigFn = typeof getConfig === 'function' ? getConfig : () => getConfig; return async (req, res, options = {}) => { - const config = await getConfigFn(); + const config = await getConfigFn(req); + const client = await getClient(config); let returnURL = options.returnTo || config.routes.postLogoutRedirect; debug('logout() with return url: %s', returnURL); diff --git a/src/auth0-session/index.ts b/src/auth0-session/index.ts index 4882c4daf..a0378993b 100644 --- a/src/auth0-session/index.ts +++ b/src/auth0-session/index.ts @@ -9,10 +9,18 @@ export { StatelessSession } from './session/stateless-session'; export { AbstractSession, SessionPayload } from './session/abstract-session'; export { StatefulSession, SessionStore } from './session/stateful-session'; export { default as TransientStore } from './transient-store'; -export { Config, SessionConfig, CookieConfig, LoginOptions, LogoutOptions, AuthorizationParameters } from './config'; +export { + Config, + GetConfig, + SessionConfig, + CookieConfig, + LoginOptions, + LogoutOptions, + AuthorizationParameters +} from './config'; export { get as getConfig, ConfigParameters, DeepPartial } from './get-config'; export { default as loginHandler, HandleLogin } from './handlers/login'; export { default as logoutHandler, HandleLogout } from './handlers/logout'; export { default as callbackHandler, CallbackOptions, AfterCallback, HandleCallback } from './handlers/callback'; -export { TokenEndpointResponse, AbstractClient } from './client/abstract-client'; +export { TokenEndpointResponse, AbstractClient, Telemetry } from './client/abstract-client'; export { SessionCache } from './session-cache'; diff --git a/src/auth0-session/session-cache.ts b/src/auth0-session/session-cache.ts index d59fd9068..16424949e 100644 --- a/src/auth0-session/session-cache.ts +++ b/src/auth0-session/session-cache.ts @@ -5,5 +5,5 @@ export interface SessionCache<Req = any, Res = any, Session = { [key: string]: a delete(req: Req, res: Res): Promise<void>; isAuthenticated(req: Req, res: Res): Promise<boolean>; getIdToken(req: Req, res: Res): Promise<string | undefined>; - fromTokenEndpointResponse(tokenSet: TokenEndpointResponse): Session; + fromTokenEndpointResponse(req: Req, res: Res, tokenSet: TokenEndpointResponse): Promise<Session>; } diff --git a/src/auth0-session/session/abstract-session.ts b/src/auth0-session/session/abstract-session.ts index 17bd9a8cc..7b864f080 100644 --- a/src/auth0-session/session/abstract-session.ts +++ b/src/auth0-session/session/abstract-session.ts @@ -36,7 +36,7 @@ const assert = (bool: boolean, msg: string) => { }; export abstract class AbstractSession<Session> { - protected getConfig: () => Config | Promise<Config>; + protected getConfig: (req: Auth0RequestCookies) => Config | Promise<Config>; constructor(getConfig: GetConfig) { this.getConfig = typeof getConfig === 'function' ? getConfig : () => getConfig; @@ -62,7 +62,7 @@ export abstract class AbstractSession<Session> { ): Promise<void>; public async read(req: Auth0RequestCookies): Promise<[Session?, number?]> { - const config = await this.getConfig(); + const config = await this.getConfig(req); const { rollingDuration, absoluteDuration } = config.session; try { @@ -100,7 +100,7 @@ export abstract class AbstractSession<Session> { session: Session | null | undefined, createdAt?: number ): Promise<void> { - const config = await this.getConfig(); + const config = await this.getConfig(req); const { cookie: { transient, ...cookieConfig } } = config.session; diff --git a/src/auth0-session/session/stateful-session.ts b/src/auth0-session/session/stateful-session.ts index e3b64561a..8590a9642 100644 --- a/src/auth0-session/session/stateful-session.ts +++ b/src/auth0-session/session/stateful-session.ts @@ -4,6 +4,7 @@ import { AbstractSession, SessionPayload } from './abstract-session'; import { generateCookieValue, getCookieValue } from '../utils/signed-cookies'; import { signing } from '../utils/hkdf'; import { Auth0RequestCookies, Auth0ResponseCookies } from '../http'; +import { Config } from '../config'; const debug = createDebug('stateful-session'); @@ -30,17 +31,15 @@ export class StatefulSession< private keys?: Uint8Array[]; private store?: SessionStore<Session>; - private async getStore(): Promise<SessionStore<Session>> { + private async getStore(config: Config): Promise<SessionStore<Session>> { if (!this.store) { - const config = await this.getConfig(); this.store = config.session.store as SessionStore<Session>; } return this.store; } - private async getKeys(): Promise<Uint8Array[]> { + private async getKeys(config: Config): Promise<Uint8Array[]> { if (!this.keys) { - const config = await this.getConfig(); const secret = config.secret; const secrets = Array.isArray(secret) ? secret : [secret]; this.keys = await Promise.all(secrets.map(signing)); @@ -49,14 +48,14 @@ export class StatefulSession< } async getSession(req: Auth0RequestCookies): Promise<SessionPayload<Session> | undefined | null> { - const config = await this.getConfig(); + const config = await this.getConfig(req); const { name: sessionName } = config.session; const cookies = req.getCookies(); - const keys = await this.getKeys(); + const keys = await this.getKeys(config); const sessionId = await getCookieValue(sessionName, cookies[sessionName], keys); if (sessionId) { - const store = await this.getStore(); + const store = await this.getStore(config); debug('reading session from %s store', sessionId); return store.get(sessionId); } @@ -73,11 +72,11 @@ export class StatefulSession< cookieOptions: CookieSerializeOptions, isNewSession: boolean ): Promise<void> { - const config = await this.getConfig(); - const store = await this.getStore(); + const config = await this.getConfig(req); + const store = await this.getStore(config); const { name: sessionName, genId } = config.session; const cookies = req.getCookies(); - const keys = await this.getKeys(); + const keys = await this.getKeys(config); let sessionId = await getCookieValue(sessionName, cookies[sessionName], keys); // If this is a new session created by a new login we need to remove the old session @@ -106,14 +105,14 @@ export class StatefulSession< res: Auth0ResponseCookies, cookieOptions: CookieSerializeOptions ): Promise<void> { - const config = await this.getConfig(); + const config = await this.getConfig(req); const { name: sessionName } = config.session; const cookies = req.getCookies(); - const keys = await this.getKeys(); + const keys = await this.getKeys(config); const sessionId = await getCookieValue(sessionName, cookies[sessionName], keys); if (sessionId) { - const store = await this.getStore(); + const store = await this.getStore(config); debug('deleting session %o', sessionId); res.clearCookie(sessionName, cookieOptions); await store.delete(sessionId); diff --git a/src/auth0-session/session/stateless-session.ts b/src/auth0-session/session/stateless-session.ts index 31f2f2d1e..fc4fb6787 100644 --- a/src/auth0-session/session/stateless-session.ts +++ b/src/auth0-session/session/stateless-session.ts @@ -24,9 +24,8 @@ export class StatelessSession< super(config); } - private async getChunkSize(): Promise<number> { + private async getChunkSize(config: Config): Promise<number> { if (this.chunkSize === undefined) { - const config = await this.getConfig(); const { cookie: { transient, ...cookieConfig }, name: sessionName @@ -44,9 +43,8 @@ export class StatelessSession< return this.chunkSize; } - private async getKeys(): Promise<Uint8Array[]> { + public async getKeys(config: Config): Promise<Uint8Array[]> { if (!this.keys) { - const config = await this.getConfig(); const secret = config.secret; const secrets = Array.isArray(secret) ? secret : [secret]; this.keys = await Promise.all(secrets.map(encryption)); @@ -54,13 +52,11 @@ export class StatelessSession< return this.keys; } - public async encrypt(payload: jose.JWTPayload, { iat, uat, exp }: Header): Promise<string> { - const [key] = await this.getKeys(); + public async encrypt(payload: jose.JWTPayload, { iat, uat, exp }: Header, key: Uint8Array): Promise<string> { return await new jose.EncryptJWT({ ...payload }).setProtectedHeader({ alg, enc, uat, iat, exp }).encrypt(key); } - private async decrypt(jwe: string): Promise<jose.JWTDecryptResult> { - const keys = await this.getKeys(); + private async decrypt(jwe: string, keys: Uint8Array[]): Promise<jose.JWTDecryptResult> { let err; for (const key of keys) { try { @@ -73,7 +69,7 @@ export class StatelessSession< } async getSession(req: Auth0RequestCookies): Promise<SessionPayload<Session> | undefined | null> { - const config = await this.getConfig(); + const config = await this.getConfig(req); const { name: sessionName } = config.session; const cookies = req.getCookies(); let existingSessionValue: string | undefined; @@ -106,7 +102,8 @@ export class StatelessSession< .join(''); } if (existingSessionValue) { - const { protectedHeader, payload } = await this.decrypt(existingSessionValue); + const keys = await this.getKeys(config); + const { protectedHeader, payload } = await this.decrypt(existingSessionValue, keys); return { header: protectedHeader as unknown as Header, data: payload as Session }; } return; @@ -121,14 +118,15 @@ export class StatelessSession< exp: number, cookieOptions: CookieSerializeOptions ): Promise<void> { - const config = await this.getConfig(); + const config = await this.getConfig(req); const { name: sessionName } = config.session; const cookies = req.getCookies(); debug('found session, creating signed session cookie(s) with name %o(.i)', sessionName); - const value = await this.encrypt(session, { iat, uat, exp }); + const [key] = await this.getKeys(config); + const value = await this.encrypt(session, { iat, uat, exp }, key); - const chunkSize = await this.getChunkSize(); + const chunkSize = await this.getChunkSize(config); const chunkCount = Math.ceil(value.length / chunkSize); const existingCookies = new Set( @@ -158,7 +156,7 @@ export class StatelessSession< res: Auth0ResponseCookies, cookieOptions: CookieSerializeOptions ): Promise<void> { - const config = await this.getConfig(); + const config = await this.getConfig(req); const { name: sessionName } = config.session; const cookies = req.getCookies(); diff --git a/src/auth0-session/transient-store.ts b/src/auth0-session/transient-store.ts index ca7fcbdd0..cbe52f296 100644 --- a/src/auth0-session/transient-store.ts +++ b/src/auth0-session/transient-store.ts @@ -1,7 +1,7 @@ import { generateCookieValue, getCookieValue } from './utils/signed-cookies'; import { signing } from './utils/hkdf'; import { Config, GetConfig } from './config'; -import { Auth0Request, Auth0Response } from './http'; +import { Auth0Request, Auth0RequestCookies, Auth0Response } from './http'; export interface StoreOptions { sameSite?: boolean | 'lax' | 'strict' | 'none'; @@ -11,15 +11,14 @@ export interface StoreOptions { export default class TransientStore { private keys?: Uint8Array[]; - protected getConfig: () => Config | Promise<Config>; + protected getConfig: (req: Auth0RequestCookies) => Config | Promise<Config>; constructor(getConfig: GetConfig) { this.getConfig = typeof getConfig === 'function' ? getConfig : () => getConfig; } - private async getKeys(): Promise<Uint8Array[]> { + private async getKeys(config: Config): Promise<Uint8Array[]> { if (!this.keys) { - const config = await this.getConfig(); const secret = config.secret; const secrets = Array.isArray(secret) ? secret : [secret]; this.keys = await Promise.all(secrets.map(signing)); @@ -31,7 +30,7 @@ export default class TransientStore { * Set a cookie with a value or a generated nonce. * * @param {String} key Cookie name to use. - * @param {IncomingMessage} _req Server Request object. + * @param {IncomingMessage} req Server Request object. * @param {ServerResponse} res Server Response object. * @param {Object} opts Options object. * @param {String} opts.sameSite SameSite attribute of `None`, `Lax`, or `Strict`. Defaults to `None`. @@ -41,12 +40,12 @@ export default class TransientStore { */ async save( key: string, - _req: Auth0Request, + req: Auth0Request, res: Auth0Response, { sameSite = 'none', value }: StoreOptions ): Promise<string> { const isSameSiteNone = sameSite === 'none'; - const config = await this.getConfig(); + const config = await this.getConfig(req); const { domain, path, secure } = config.transactionCookie; const basicAttr = { httpOnly: true, @@ -54,7 +53,7 @@ export default class TransientStore { domain, path }; - const [signingKey] = await this.getKeys(); + const [signingKey] = await this.getKeys(config); { const cookieValue = await generateCookieValue(key, value, signingKey); @@ -87,10 +86,10 @@ export default class TransientStore { async read(key: string, req: Auth0Request, res: Auth0Response): Promise<string | undefined> { const cookies = req.getCookies(); const cookie = cookies[key]; - const config = await this.getConfig(); + const config = await this.getConfig(req); const cookieConfig = config.transactionCookie; - const verifyingKeys = await this.getKeys(); + const verifyingKeys = await this.getKeys(config); let value = await getCookieValue(key, cookie, verifyingKeys); res.clearCookie(key, cookieConfig); diff --git a/src/config.ts b/src/config.ts index 358af2548..450d3d335 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,371 +1,11 @@ -import type { Agent } from 'https'; -import type { LoginOptions, AuthorizationParameters as OidcAuthorizationParameters } from './auth0-session/config'; -import { SessionStore } from './auth0-session/session/stateful-session'; -import Session from './session/session'; +import type { Config as BaseConfig } from './auth0-session/config'; import { DeepPartial, get as getBaseConfig } from './auth0-session/get-config'; +import type { Auth0Request, Auth0RequestCookies } from './auth0-session/http'; /** * @category server */ -export interface BaseConfig { - /** - * The secret(s) used to derive an encryption key for the user identity in a session cookie and - * to sign the transient cookies used by the login callback. - * Provide a single string secret, but if you want to rotate the secret you can provide an array putting - * the new secret first. - * You can also use the `AUTH0_SECRET` environment variable. - */ - secret: string | Array<string>; - - /** - * Object defining application session cookie attributes. - */ - session: SessionConfig; - - /** - * Boolean value to enable Auth0's proprietary logout feature. - * Since this SDK is for Auth0, it's set to `true` by default. - * Set it to `false` if you don't want to use https://auth0.com/docs/api/authentication#logout. - * You can also use the `AUTH0_LOGOUT` environment variable. - */ - auth0Logout?: boolean; - - /** - * URL parameters used when redirecting users to the authorization server to log in. - * - * If this property is not provided by your application, its default values will be: - * - * ```js - * { - * response_type: 'code', - * scope: 'openid profile email' - * } - * ``` - * - * New values can be passed in to change what is returned from the authorization server - * depending on your specific scenario. Additional custom parameters can be added as well. - * - * **Note:** You must provide the required parameters if this object is set. - * - * ```js - * { - * response_type: 'code', - * scope: 'openid profile email', - * - * // Additional parameters - * acr_value: 'tenant:test-tenant', - * custom_param: 'custom-value' - * }; - * ``` - */ - authorizationParams: AuthorizationParameters; - - /** - * The root URL for the application router, for example `https://localhost`. - * You can also use the `AUTH0_BASE_URL` environment variable. - * If you provide a domain, we will prefix it with `https://`. This can be useful when assigning it to - * `VERCEL_URL` for Vercel deploys. - * - * `NEXT_PUBLIC_AUTH0_BASE_URL` will also be checked if `AUTH0_BASE_URL` is not defined. - */ - baseURL: string; - - /** - * The Client ID for your application. - * You can also use the `AUTH0_CLIENT_ID` environment variable. - */ - clientID: string; - - /** - * The Client Secret for your application. - * Required when requesting access tokens. - * You can also use the `AUTH0_CLIENT_SECRET` environment variable. - */ - clientSecret?: string; - - /** - * Integer value for the system clock's tolerance (leeway) in seconds for ID token verification.` - * Defaults to `60` seconds. - * You can also use the `AUTH0_CLOCK_TOLERANCE` environment variable. - */ - clockTolerance: number; - - /** - * Integer value for the HTTP timeout in milliseconds for authentication requests. - * Defaults to `5000` ms. - * You can also use the `AUTH0_HTTP_TIMEOUT` environment variable. - */ - httpTimeout: number; - - /** - * Instance of an HTTP agent for authentication requests. - * (This is for the Node.js runtime only) - */ - httpAgent?: Agent; - - /** - * Boolean value to opt-out of sending the library and node version to your authorization server - * via the `Auth0-Client` header. Defaults to `true`. - * You can also use the `AUTH0_ENABLE_TELEMETRY` environment variable. - */ - enableTelemetry: boolean; - - /** - * Function that returns an object with URL-safe state values for login. - * Used for passing custom state parameters to your authorization server. - * Can also be passed in to {@link HandleLogin}. - * - * ```js - * { - * ... - * getLoginState(options) { - * return { - * returnTo: options.returnTo || req.originalUrl, - * customState: 'foo' - * }; - * } - * } - * ``` - */ - getLoginState: (options: LoginOptions) => Record<string, any>; - - /** - * Array value of claims to remove from the ID token before storing the cookie session. - * Defaults to `['aud', 'iss', 'iat', 'exp', 'nbf', 'nonce', 'azp', 'auth_time', 's_hash', 'at_hash', 'c_hash']`. - * You can also use the `AUTH0_IDENTITY_CLAIM_FILTER` environment variable. - */ - identityClaimFilter: string[]; - - /** - * Boolean value to log the user out from the identity provider on application logout. Defaults to `true`. - * You can also use the `AUTH0_IDP_LOGOUT` environment variable. - */ - idpLogout: boolean; - - /** - * String value for the expected ID token algorithm. Defaults to 'RS256'. - * You can also use the `AUTH0_ID_TOKEN_SIGNING_ALG` environment variable. - */ - idTokenSigningAlg: string; - - /** - * **REQUIRED** The root URL for the token issuer with no trailing slash. - * This is `https://` plus your Auth0 domain. - * You can also use the `AUTH0_ISSUER_BASE_URL` environment variable. - */ - issuerBaseURL: string; - - /** - * Set a fallback cookie with no `SameSite` attribute when `response_mode` is `form_post`. - * The default `response_mode` for this SDK is `query` so this defaults to `false` - * You can also use the `AUTH0_LEGACY_SAME_SITE_COOKIE` environment variable. - */ - legacySameSiteCookie: boolean; - - /** - * Boolean value to automatically install the login and logout routes. - */ - routes: { - /** - * Either a relative path to the application or a valid URI to an external domain. - * This value must be registered on the authorization server. - * The user will be redirected to this after a logout has been performed. - * You can also use the `AUTH0_POST_LOGOUT_REDIRECT` environment variable. - */ - postLogoutRedirect: string; - - /** - * Relative path to the application callback to process the response from the authorization server. - * Defaults to `/api/auth/callback`. - * You can also use the `AUTH0_CALLBACK` environment variable. - */ - callback: string; - }; - - /** - * Private key for use with `private_key_jwt` clients. - * This should be a string that is the contents of a PEM file. - * You can also use the `AUTH0_CLIENT_ASSERTION_SIGNING_KEY` environment variable. - * - * For Edge runtime, you can also provide an instance of `CryptoKey`. - */ - clientAssertionSigningKey?: string | CryptoKey; - - /** - * The algorithm to sign the client assertion JWT. - * Uses one of `token_endpoint_auth_signing_alg_values_supported` if not specified. - * If the Authorization Server discovery document does not list `token_endpoint_auth_signing_alg_values_supported` - * this property will be required. - * You can also use the `AUTH0_CLIENT_ASSERTION_SIGNING_ALG` environment variable. - */ - clientAssertionSigningAlg?: string; - - /** - * By default, the transaction cookie takes the same settings as the - * session cookie. But you may want to configure the session cookie to be more - * secure in a way that would break the OAuth flow's usage of the transaction - * cookie (Setting SameSite=Strict for example). - * - * You can also use: - * `AUTH0_TRANSACTION_COOKIE_NAME` - * `AUTH0_TRANSACTION_COOKIE_DOMAIN` - * `AUTH0_TRANSACTION_COOKIE_PATH` - * `AUTH0_TRANSACTION_COOKIE_SAME_SITE` - * `AUTH0_TRANSACTION_COOKIE_SECURE` - */ - transactionCookie: Omit<CookieConfig, 'transient' | 'httpOnly'> & { name: string }; -} - -/** - * Configuration parameters used for the application session. - * - * @category Server - */ -export interface SessionConfig { - /** - * String value for the cookie name used for the internal session. - * This value must only include letters, numbers, and underscores. - * Defaults to `appSession`. - * You can also use the `AUTH0_SESSION_NAME` environment variable. - */ - name: string; - - /** - * By default, the session is stateless and stored in an encrypted cookie. But if you want a stateful session - * you can provide a store with `get`, `set` and `destroy` methods to store the session on the server. - */ - store?: SessionStore<Session>; - - /** - * A Function for generating a session id when using a custom session store. - * - * **IMPORTANT** If you override this, you must use a suitable value from your platform to - * prevent collisions. For example, for Node: `require('crypto').randomBytes(16).toString('hex')`. - */ - genId?: <Req = any, SessionType extends { [key: string]: any } = { [key: string]: any }>( - req: Req, - session: SessionType - ) => string | Promise<string>; - - /** - * If you want your session duration to be rolling, resetting everytime the - * user is active on your site, set this to `true`. If you want the session - * duration to be absolute, where the user gets logged out a fixed time after login - * regardless of activity, set this to `false`. - * Defaults to `true`. - * You can also use the `AUTH0_SESSION_ROLLING` environment variable. - */ - rolling: boolean; - - /** - * Integer value, in seconds, for application session rolling duration. - * The amount of time for which the user must be idle for then to be logged out. - * Should be `false` when rolling is `false`. - * Defaults to `86400` seconds (1 day). - * You can also use the AUTH0_SESSION_ROLLING_DURATION environment variable. - */ - rollingDuration: number | false; - - /** - * Integer value, in seconds, for application absolute rolling duration. - * The amount of time after the user has logged in that they will be logged out. - * Set this to `false` if you don't want an absolute duration on your session. - * Defaults to `604800` seconds (7 days). - * You can also use the `AUTH0_SESSION_ABSOLUTE_DURATION` environment variable. - */ - absoluteDuration: boolean | number; - - /** - * Boolean value to enable automatic session saving when using rolling sessions. - * If this is `false`, you must call `touchSession(req, res)` to update the session. - * Defaults to `true`. - * You can also use the `AUTH0_SESSION_AUTO_SAVE` environment variable. - */ - autoSave?: boolean; - - /** - * Boolean value to store the ID token in the session. Storing it can make the session cookie too - * large. - * Defaults to `true`. - * You can also use the `AUTH0_SESSION_STORE_ID_TOKEN` environment variable. - */ - storeIDToken: boolean; - - cookie: CookieConfig; -} - -/** - * Configure how the session cookie and transient cookies are stored. - * - * @category Server - */ -export interface CookieConfig { - /** - * Domain name for the cookie. - * You can also use the `AUTH0_COOKIE_DOMAIN` environment variable. - */ - domain?: string; - - /** - * Path for the cookie. - * Defaults to `/`. - * You should change this to be more restrictive if you application shares a domain with other apps. - * You can also use the `AUTH0_COOKIE_PATH` environment variable. - */ - path?: string; - - /** - * Set to `true` to use a transient cookie (cookie without an explicit expiration). - * Defaults to `false`. - * You can also use the `AUTH0_COOKIE_TRANSIENT` environment variable. - */ - transient: boolean; - - /** - * Flags the cookie to be accessible only by the web server. - * Defaults to `true`. - * You can also use the `AUTH0_COOKIE_HTTP_ONLY` environment variable. - */ - httpOnly: boolean; - - /** - * Marks the cookie to be used over secure channels only. - * Defaults to the protocol of {@link BaseConfig.baseURL}. - * You can also use the `AUTH0_COOKIE_SECURE` environment variable. - */ - secure?: boolean; - - /** - * Value of the SameSite `Set-Cookie` attribute. - * Defaults to `lax` but will be adjusted based on {@link AuthorizationParameters.response_type}. - * You can also use the `AUTH0_COOKIE_SAME_SITE` environment variable. - */ - sameSite: 'lax' | 'strict' | 'none'; -} - -/** - * Authorization parameters that will be passed to the identity provider on login. - * - * The library uses `response_mode: 'query'` and `response_type: 'code'` (with PKCE) by default. - * - * @category Server - */ -export interface AuthorizationParameters extends OidcAuthorizationParameters { - /** - * A space-separated list of scopes that will be requested during authentication. For example, - * `openid profile email offline_access`. - * Defaults to `openid profile email`. - */ - scope: string; - - response_mode: 'query' | 'form_post'; - response_type: 'id_token' | 'code id_token' | 'code'; -} - -/** - * @category server - */ -export interface NextConfig extends Pick<BaseConfig, 'identityClaimFilter'> { +export interface NextConfig extends BaseConfig { /** * Log users in to a specific organization. * @@ -375,11 +15,9 @@ export interface NextConfig extends Pick<BaseConfig, 'identityClaimFilter'> { * If your app supports multiple organizations, you should take a look at {@link AuthorizationParams.organization}. */ organization?: string; - routes: { - callback: string; + routes: BaseConfig['routes'] & { login: string; }; - session: Pick<SessionConfig, 'storeIDToken'>; } /** @@ -405,21 +43,21 @@ export interface NextConfig extends Pick<BaseConfig, 'identityClaimFilter'> { * * ### Required * - * - `AUTH0_SECRET`: See {@link secret}. - * - `AUTH0_ISSUER_BASE_URL`: See {@link issuerBaseURL}. - * - `AUTH0_BASE_URL`: See {@link baseURL}. - * - `AUTH0_CLIENT_ID`: See {@link clientID}. - * - `AUTH0_CLIENT_SECRET`: See {@link clientSecret}. + * - `AUTH0_SECRET`: See {@link BaseConfig.secret}. + * - `AUTH0_ISSUER_BASE_URL`: See {@link BaseConfig.issuerBaseURL}. + * - `AUTH0_BASE_URL`: See {@link BaseConfig.baseURL}. + * - `AUTH0_CLIENT_ID`: See {@link BaseConfig.clientID}. + * - `AUTH0_CLIENT_SECRET`: See {@link BaseConfig.clientSecret}. * * ### Optional * - * - `AUTH0_CLOCK_TOLERANCE`: See {@link clockTolerance}. - * - `AUTH0_HTTP_TIMEOUT`: See {@link httpTimeout}. - * - `AUTH0_ENABLE_TELEMETRY`: See {@link enableTelemetry}. - * - `AUTH0_IDP_LOGOUT`: See {@link idpLogout}. - * - `AUTH0_ID_TOKEN_SIGNING_ALG`: See {@link idTokenSigningAlg}. - * - `AUTH0_LEGACY_SAME_SITE_COOKIE`: See {@link legacySameSiteCookie}. - * - `AUTH0_IDENTITY_CLAIM_FILTER`: See {@link identityClaimFilter}. + * - `AUTH0_CLOCK_TOLERANCE`: See {@link BaseConfig.clockTolerance}. + * - `AUTH0_HTTP_TIMEOUT`: See {@link BaseConfig.httpTimeout}. + * - `AUTH0_ENABLE_TELEMETRY`: See {@link BaseConfig.enableTelemetry}. + * - `AUTH0_IDP_LOGOUT`: See {@link BaseConfig.idpLogout}. + * - `AUTH0_ID_TOKEN_SIGNING_ALG`: See {@link BaseConfig.idTokenSigningAlg}. + * - `AUTH0_LEGACY_SAME_SITE_COOKIE`: See {@link BaseConfig.legacySameSiteCookie}. + * - `AUTH0_IDENTITY_CLAIM_FILTER`: See {@link BaseConfig.identityClaimFilter}. * - `NEXT_PUBLIC_AUTH0_LOGIN`: See {@link NextConfig.routes}. * - `AUTH0_CALLBACK`: See {@link BaseConfig.routes}. * - `AUTH0_POST_LOGOUT_REDIRECT`: See {@link BaseConfig.routes}. @@ -475,7 +113,7 @@ export interface NextConfig extends Pick<BaseConfig, 'identityClaimFilter'> { * * @category Server */ -export type ConfigParameters = DeepPartial<BaseConfig & NextConfig>; +export type ConfigParameters = DeepPartial<NextConfig>; /** * @ignore @@ -505,14 +143,7 @@ const array = (param?: string): string[] | undefined => /** * @ignore */ -export const getLoginUrl = (): string => { - return process.env.NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login'; -}; - -/** - * @ignore - */ -export const getConfig = (params: ConfigParameters = {}): { baseConfig: BaseConfig; nextConfig: NextConfig } => { +export const getConfig = (params: ConfigParameters = {}): NextConfig => { // Don't use destructuring here so that the `DefinePlugin` can replace any env vars specified in `next.config.js` const AUTH0_SECRET = process.env.AUTH0_SECRET; const AUTH0_ISSUER_BASE_URL = process.env.AUTH0_ISSUER_BASE_URL; @@ -618,15 +249,24 @@ export const getConfig = (params: ConfigParameters = {}): { baseConfig: BaseConf } }); - const nextConfig: NextConfig = { + return { + ...baseConfig, + organization: organization || AUTH0_ORGANIZATION, routes: { ...baseConfig.routes, - login: baseParams.routes?.login || getLoginUrl() - }, - identityClaimFilter: baseConfig.identityClaimFilter, - organization: organization || AUTH0_ORGANIZATION, - session: { storeIDToken: baseConfig.session.storeIDToken } + login: baseParams.routes?.login || process.env.NEXT_PUBLIC_AUTH0_LOGIN || '/api/auth/login' + } }; +}; + +export type GetConfig = (req: Auth0Request | Auth0RequestCookies) => Promise<NextConfig> | NextConfig; - return { baseConfig, nextConfig }; +export const configSingletonGetter = (params: ConfigParameters = {}, genId: () => string): GetConfig => { + let config: NextConfig; + return () => { + if (!config) { + config = getConfig({ ...params, session: { genId, ...params.session } }); + } + return config; + }; }; diff --git a/src/edge.ts b/src/edge.ts index fe06e5bf9..4e18fd815 100644 --- a/src/edge.ts +++ b/src/edge.ts @@ -8,18 +8,14 @@ import { HandleLogin, HandleLogout, HandleProfile, - SessionCache, TouchSession, UpdateSession, WithApiAuthRequired, - WithPageAuthRequired, - telemetry + WithPageAuthRequired } from './shared'; import { _initAuth } from './init'; import { setIsUsingNamedExports, setIsUsingOwnInstance } from './utils/instance-check'; -import { getConfig, getLoginUrl } from './config'; -import { withPageAuthRequiredFactory } from './helpers'; -import { EdgeClient } from './auth0-session/client/edge-client'; +import { clientGetter } from './auth0-session/client/edge-client'; import { WithMiddlewareAuthRequired } from './helpers/with-middleware-auth-required'; const genId = () => { @@ -30,7 +26,7 @@ const genId = () => { .join(''); }; -let instance: Auth0Server & { sessionCache: SessionCache }; +let instance: Auth0Server; /** * Initialise your own instance of the SDK. @@ -42,34 +38,29 @@ let instance: Auth0Server & { sessionCache: SessionCache }; export type InitAuth0 = (params?: ConfigParameters) => Auth0Server; // For using managed instance with named exports. -function getInstance(): Auth0Server & { sessionCache: SessionCache } { +function getInstance(): Auth0Server { setIsUsingNamedExports(); if (instance) { return instance; } - const { baseConfig, nextConfig } = getConfig({ session: { genId } }); - const client = new EdgeClient(baseConfig, telemetry); - instance = _initAuth({ baseConfig, nextConfig, client }); + instance = _initAuth({ genId, clientGetter }); return instance; } // For creating own instance. export const initAuth0: InitAuth0 = (params) => { setIsUsingOwnInstance(); - const { baseConfig, nextConfig } = getConfig({ ...params, session: { genId, ...params?.session } }); - const client = new EdgeClient(baseConfig, telemetry); - const { sessionCache, ...publicApi } = _initAuth({ baseConfig, nextConfig, client }); - return publicApi; + return _initAuth({ genId, clientGetter, params }); }; -const getSessionCache = () => getInstance().sessionCache; export const getSession: GetSession = (...args) => getInstance().getSession(...args); export const updateSession: UpdateSession = (...args) => getInstance().updateSession(...args); export const getAccessToken: GetAccessToken = (...args) => getInstance().getAccessToken(...args); export const touchSession: TouchSession = (...args) => getInstance().touchSession(...args); export const withApiAuthRequired: WithApiAuthRequired = (...args) => (getInstance().withApiAuthRequired as any)(...args); -export const withPageAuthRequired: WithPageAuthRequired = withPageAuthRequiredFactory(getLoginUrl(), getSessionCache); +export const withPageAuthRequired: WithPageAuthRequired = ((...args: Parameters<WithPageAuthRequired>) => + getInstance().withPageAuthRequired(...args)) as WithPageAuthRequired; export const handleLogin: HandleLogin = ((...args: Parameters<HandleLogin>) => getInstance().handleLogin(...args)) as HandleLogin; export const handleLogout: HandleLogout = ((...args: Parameters<HandleLogout>) => diff --git a/src/handlers/callback.ts b/src/handlers/callback.ts index b38c874c4..f558eaa2d 100644 --- a/src/handlers/callback.ts +++ b/src/handlers/callback.ts @@ -8,10 +8,9 @@ import { } from '../auth0-session'; import { Session } from '../session'; import { assertReqRes } from '../utils/assert'; -import { BaseConfig, NextConfig } from '../config'; +import { GetConfig, NextConfig } from '../config'; import { CallbackHandlerError, HandlerErrorCause } from '../utils/errors'; import { Auth0NextApiRequest, Auth0NextApiResponse, Auth0NextRequest, Auth0NextResponse } from '../http'; -import { LoginOptions } from './login'; import { AppRouteHandlerFnContext, AuthHandler, getHandler, Handler, OptionsProvider } from './router-helpers'; /** @@ -278,9 +277,9 @@ export type CallbackHandler = Handler<CallbackOptions>; /** * @ignore */ -export default function handleCallbackFactory(handler: BaseHandleCallback, config: NextConfig): HandleCallback { - const appRouteHandler = appRouteHandlerFactory(handler, config); - const pageRouteHandler = pageRouteHandlerFactory(handler, config); +export default function handleCallbackFactory(handler: BaseHandleCallback, getConfig: GetConfig): HandleCallback { + const appRouteHandler = appRouteHandlerFactory(handler, getConfig); + const pageRouteHandler = pageRouteHandlerFactory(handler, getConfig); return getHandler<CallbackOptions>(appRouteHandler, pageRouteHandler) as HandleCallback; } @@ -303,7 +302,7 @@ const applyOptions = ( if (session.user.org_id !== organization) { throw new Error( `Organization Id (org_id) claim value mismatch in the ID token; ` + - `expected "${organization}", found "${session.user.org_id}"` + `expected "${organization}", found "${session.user.org_id}"` ); } } else { @@ -313,7 +312,7 @@ const applyOptions = ( if (session.user.org_name !== organization.toLowerCase()) { throw new Error( `Organization Name (org_name) claim value mismatch in the ID token; ` + - `expected "${organization}", found "${session.user.org_name}"` + `expected "${organization}", found "${session.user.org_name}"` ); } } @@ -338,13 +337,15 @@ const applyOptions = ( */ const appRouteHandlerFactory: ( handler: BaseHandleLogin, - config: NextConfig + getConfig: GetConfig ) => (req: NextRequest, ctx: AppRouteHandlerFnContext, options?: CallbackOptions) => Promise<Response> | Response = - (handler, config) => + (handler, getConfig) => async (req, _ctx, options = {}) => { try { + const auth0Req = new Auth0NextRequest(req); + const nextConfig = await getConfig(auth0Req); const auth0Res = new Auth0NextResponse(new NextResponse()); - await handler(new Auth0NextRequest(req), auth0Res, applyOptions(req, undefined, options, config)); + await handler(auth0Req, auth0Res, applyOptions(req, undefined, options, nextConfig)); return auth0Res.res; } catch (e) { throw new CallbackHandlerError(e as HandlerErrorCause); @@ -356,17 +357,15 @@ const appRouteHandlerFactory: ( */ const pageRouteHandlerFactory: ( handler: BaseHandleCallback, - config: NextConfig + getConfig: GetConfig ) => (req: NextApiRequest, res: NextApiResponse, options?: CallbackOptions) => Promise<void> = - (handler, config) => + (handler, getConfig) => async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise<void> => { try { + const auth0Req = new Auth0NextApiRequest(req); + const nextConfig = await getConfig(auth0Req); assertReqRes(req, res); - return await handler( - new Auth0NextApiRequest(req), - new Auth0NextApiResponse(res), - applyOptions(req, res, options, config) - ); + return await handler(auth0Req, new Auth0NextApiResponse(res), applyOptions(req, res, options, nextConfig)); } catch (e) { throw new CallbackHandlerError(e as HandlerErrorCause); } diff --git a/src/handlers/login.ts b/src/handlers/login.ts index 3e71550e8..c07e48774 100644 --- a/src/handlers/login.ts +++ b/src/handlers/login.ts @@ -7,7 +7,7 @@ import { } from '../auth0-session'; import toSafeRedirect from '../utils/url-helpers'; import { assertReqRes } from '../utils/assert'; -import { BaseConfig, NextConfig } from '../config'; +import { GetConfig, NextConfig } from '../config'; import { HandlerErrorCause, LoginHandlerError } from '../utils/errors'; import { Auth0NextApiRequest, Auth0NextApiResponse, Auth0NextRequest, Auth0NextResponse } from '../http'; import { AppRouteHandlerFnContext, getHandler, OptionsProvider, Handler, AuthHandler } from './router-helpers'; @@ -259,13 +259,9 @@ export type LoginHandler = Handler<LoginOptions>; /** * @ignore */ -export default function handleLoginFactory( - handler: BaseHandleLogin, - nextConfig: NextConfig, - baseConfig: BaseConfig -): HandleLogin { - const appRouteHandler = appRouteHandlerFactory(handler, nextConfig, baseConfig); - const pageRouteHandler = pageRouteHandlerFactory(handler, nextConfig, baseConfig); +export default function handleLoginFactory(handler: BaseHandleLogin, getConfig: GetConfig): HandleLogin { + const appRouteHandler = appRouteHandlerFactory(handler, getConfig); + const pageRouteHandler = pageRouteHandlerFactory(handler, getConfig); return getHandler<LoginOptions>(appRouteHandler, pageRouteHandler) as HandleLogin; } @@ -277,22 +273,21 @@ const applyOptions = ( req: NextApiRequest | NextRequest, options: LoginOptions, dangerousReturnTo: string | undefined | null, - nextConfig: NextConfig, - baseConfig: BaseConfig + config: NextConfig ): BaseLoginOptions => { let opts: BaseLoginOptions; let getLoginState: GetLoginState | undefined; // eslint-disable-next-line prefer-const ({ getLoginState, ...opts } = options); if (dangerousReturnTo) { - const safeBaseUrl = new URL(options.authorizationParams?.redirect_uri || baseConfig.baseURL); + const safeBaseUrl = new URL(options.authorizationParams?.redirect_uri || config.baseURL); const returnTo = toSafeRedirect(dangerousReturnTo, safeBaseUrl); opts = { ...opts, returnTo }; } - if (nextConfig.organization) { + if (config.organization) { opts = { ...opts, - authorizationParams: { organization: nextConfig.organization, ...opts.authorizationParams } + authorizationParams: { organization: config.organization, ...opts.authorizationParams } }; } if (getLoginState) { @@ -306,21 +301,18 @@ const applyOptions = ( */ const appRouteHandlerFactory: ( handler: BaseHandleLogin, - nextConfig: NextConfig, - baseConfig: BaseConfig + getConfig: GetConfig ) => (req: NextRequest, ctx: AppRouteHandlerFnContext, options?: LoginOptions) => Promise<Response> | Response = - (handler, nextConfig, baseConfig) => + (handler, getConfig) => async (req, _ctx, options = {}) => { try { + const auth0Req = new Auth0NextRequest(req); + const config = await getConfig(auth0Req); const url = new URL(req.url); const dangerousReturnTo = url.searchParams.get('returnTo'); const auth0Res = new Auth0NextResponse(new NextResponse()); - await handler( - new Auth0NextRequest(req), - auth0Res, - applyOptions(req, options, dangerousReturnTo, nextConfig, baseConfig) as BaseLoginOptions - ); + await handler(auth0Req, auth0Res, applyOptions(req, options, dangerousReturnTo, config) as BaseLoginOptions); return auth0Res.res; } catch (e) { throw new LoginHandlerError(e as HandlerErrorCause); @@ -332,20 +324,21 @@ const appRouteHandlerFactory: ( */ const pageRouteHandlerFactory: ( handler: BaseHandleLogin, - nextConfig: NextConfig, - baseConfig: BaseConfig + getConfig: GetConfig ) => (req: NextApiRequest, res: NextApiResponse, options?: LoginOptions) => Promise<void> | void = - (handler, nextConfig, baseConfig) => + (handler, getConfig) => async (req, res, options = {}) => { try { + const auth0Req = new Auth0NextApiRequest(req); + const config = await getConfig(auth0Req); assertReqRes(req, res); const dangerousReturnTo = req.query.returnTo && Array.isArray(req.query.returnTo) ? req.query.returnTo[0] : req.query.returnTo; return await handler( - new Auth0NextApiRequest(req), + auth0Req, new Auth0NextApiResponse(res), - applyOptions(req, options, dangerousReturnTo, nextConfig, baseConfig) as BaseLoginOptions + applyOptions(req, options, dangerousReturnTo, config) as BaseLoginOptions ); } catch (e) { throw new LoginHandlerError(e as HandlerErrorCause); diff --git a/src/handlers/logout.ts b/src/handlers/logout.ts index 517382e11..c4f12ba8d 100644 --- a/src/handlers/logout.ts +++ b/src/handlers/logout.ts @@ -4,7 +4,6 @@ import { HandleLogin as BaseHandleLogin, HandleLogout as BaseHandleLogout } from import { assertReqRes } from '../utils/assert'; import { HandlerErrorCause, LogoutHandlerError } from '../utils/errors'; import { Auth0NextApiRequest, Auth0NextApiResponse, Auth0NextRequest, Auth0NextResponse } from '../http'; -import { BaseConfig } from '../config'; import { AppRouteHandlerFnContext, AuthHandler, Handler, getHandler, OptionsProvider } from './router-helpers'; /** diff --git a/src/handlers/profile.ts b/src/handlers/profile.ts index 3d12d80ce..48a09dc74 100644 --- a/src/handlers/profile.ts +++ b/src/handlers/profile.ts @@ -1,10 +1,12 @@ import { NextApiResponse, NextApiRequest } from 'next'; import { NextRequest, NextResponse } from 'next/server'; -import { AbstractClient } from '../auth0-session'; import { SessionCache, Session, fromJson, GetAccessToken } from '../session'; import { assertReqRes } from '../utils/assert'; import { ProfileHandlerError, HandlerErrorCause } from '../utils/errors'; import { AppRouteHandlerFnContext, AuthHandler, getHandler, Handler, OptionsProvider } from './router-helpers'; +import { GetClient } from '../auth0-session/client/abstract-client'; +import { GetConfig } from '../config'; +import { Auth0NextApiRequest, Auth0NextRequest } from '../http'; /** * After refetch handler for page router {@link AfterRefetchPageRoute} and app router {@link AfterRefetchAppRoute}. @@ -124,12 +126,13 @@ export type ProfileHandler = Handler<ProfileOptions>; * @ignore */ export default function profileHandler( - client: AbstractClient, + getConfig: GetConfig, + getClient: GetClient, getAccessToken: GetAccessToken, sessionCache: SessionCache ): HandleProfile { - const appRouteHandler = appRouteHandlerFactory(client, getAccessToken, sessionCache); - const pageRouteHandler = pageRouteHandlerFactory(client, getAccessToken, sessionCache); + const appRouteHandler = appRouteHandlerFactory(getConfig, getClient, getAccessToken, sessionCache); + const pageRouteHandler = pageRouteHandlerFactory(getConfig, getClient, getAccessToken, sessionCache); return getHandler<ProfileOptions>(appRouteHandler, pageRouteHandler) as HandleProfile; } @@ -138,13 +141,16 @@ export default function profileHandler( * @ignore */ const appRouteHandlerFactory: ( - client: AbstractClient, + getConfig: GetConfig, + getClient: GetClient, getAccessToken: GetAccessToken, sessionCache: SessionCache ) => (req: NextRequest, ctx: AppRouteHandlerFnContext, options?: ProfileOptions) => Promise<Response> | Response = - (client, getAccessToken, sessionCache) => + (getConfig, getClient, getAccessToken, sessionCache) => async (req, _ctx, options = {}) => { try { + const config = await getConfig(new Auth0NextRequest(req)); + const client = await getClient(config); const res = new NextResponse(); if (!(await sessionCache.isAuthenticated(req, res))) { @@ -189,14 +195,17 @@ const appRouteHandlerFactory: ( * @ignore */ const pageRouteHandlerFactory: ( - client: AbstractClient, + getConfig: GetConfig, + getClient: GetClient, getAccessToken: GetAccessToken, sessionCache: SessionCache ) => (req: NextApiRequest, res: NextApiResponse, options?: ProfileOptions) => Promise<void> = - (client, getAccessToken, sessionCache) => + (getConfig, getClient, getAccessToken, sessionCache) => async (req: NextApiRequest, res: NextApiResponse, options = {}): Promise<void> => { try { assertReqRes(req, res); + const config = await getConfig(new Auth0NextApiRequest(req)); + const client = await getClient(config); if (!(await sessionCache.isAuthenticated(req, res))) { res.status(204).end(); diff --git a/src/helpers/testing.ts b/src/helpers/testing.ts index 400da27d1..703e3890c 100644 --- a/src/helpers/testing.ts +++ b/src/helpers/testing.ts @@ -26,8 +26,9 @@ export const generateSessionCookie = async ( ): Promise<string> => { const weekInSeconds = 7 * 24 * 60 * 60; const { secret, duration: absoluteDuration = weekInSeconds, ...cookie } = config; - const cookieStoreConfig = { secret, session: { absoluteDuration, cookie } }; - const cookieStore = new StatelessSession(cookieStoreConfig as BaseConfig); + const cookieStoreConfig = { secret, session: { absoluteDuration, cookie } } as BaseConfig; + const cookieStore = new StatelessSession(cookieStoreConfig); const epoch = (Date.now() / 1000) | 0; - return cookieStore.encrypt(session, { iat: epoch, uat: epoch, exp: epoch + absoluteDuration }); + const [key] = await cookieStore.getKeys(cookieStoreConfig); + return cookieStore.encrypt(session, { iat: epoch, uat: epoch, exp: epoch + absoluteDuration }, key); }; diff --git a/src/helpers/with-middleware-auth-required.ts b/src/helpers/with-middleware-auth-required.ts index 2a0ce6730..450b9c2fa 100644 --- a/src/helpers/with-middleware-auth-required.ts +++ b/src/helpers/with-middleware-auth-required.ts @@ -1,5 +1,7 @@ import { NextMiddleware, NextRequest, NextResponse } from 'next/server'; import { SessionCache } from '../session'; +import { GetConfig } from '../config'; +import { Auth0NextRequest } from '../http'; /** * Pass custom options to {@link WithMiddlewareAuthRequired}. @@ -85,12 +87,15 @@ export type WithMiddlewareAuthRequired = ( * @ignore */ export default function withMiddlewareAuthRequiredFactory( - { login, callback }: { login: string; callback: string }, - getSessionCache: () => SessionCache + getConfig: GetConfig, + sessionCache: SessionCache ): WithMiddlewareAuthRequired { return function withMiddlewareAuthRequired(opts?): NextMiddleware { return async function wrappedMiddleware(...args) { const [req] = args; + const { + routes: { login, callback } + } = await getConfig(new Auth0NextRequest(req)); let middleware: NextMiddleware | undefined; const { pathname, origin, search } = req.nextUrl; let returnTo = `${pathname}${search}`; @@ -105,8 +110,6 @@ export default function withMiddlewareAuthRequiredFactory( return; } - const sessionCache = getSessionCache(); - const authRes = NextResponse.next(); const session = await sessionCache.get(req, authRes); if (!session?.user) { diff --git a/src/helpers/with-page-auth-required.ts b/src/helpers/with-page-auth-required.ts index 7aa33c886..e356bacf3 100644 --- a/src/helpers/with-page-auth-required.ts +++ b/src/helpers/with-page-auth-required.ts @@ -3,6 +3,9 @@ import { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult import { Claims, get, SessionCache } from '../session'; import { assertCtx } from '../utils/assert'; import { ParsedUrlQuery } from 'querystring'; +import { GetConfig } from '../config'; +import { Auth0NextRequestCookies } from '../http'; +import { NodeRequest } from '../auth0-session/http'; /** * If you wrap your `getServerSideProps` with {@link WithPageAuthRequired} your props object will be augmented with @@ -180,11 +183,11 @@ export type WithPageAuthRequired = WithPageAuthRequiredPageRouter & WithPageAuth * @ignore */ export default function withPageAuthRequiredFactory( - loginUrl: string, - getSessionCache: () => SessionCache + getConfig: GetConfig, + sessionCache: SessionCache ): WithPageAuthRequired { - const appRouteHandler = appRouteHandlerFactory(loginUrl, getSessionCache); - const pageRouteHandler = pageRouteHandlerFactory(loginUrl, getSessionCache); + const appRouteHandler = appRouteHandlerFactory(getConfig, sessionCache); + const pageRouteHandler = pageRouteHandlerFactory(getConfig, sessionCache); return (( fnOrOpts?: WithPageAuthRequiredPageRouterOptions | AppRouterPageRoute, @@ -201,10 +204,12 @@ export default function withPageAuthRequiredFactory( * @ignore */ const appRouteHandlerFactory = - (loginUrl: string, getSessionCache: () => SessionCache): WithPageAuthRequiredAppRouter => + (getConfig: GetConfig, sessionCache: SessionCache): WithPageAuthRequiredAppRouter => (handler, opts = {}) => async (params) => { - const sessionCache = getSessionCache(); + const { + routes: { login: loginUrl } + } = await getConfig(new Auth0NextRequestCookies()); const [session] = await get({ sessionCache }); if (!session?.user) { const returnTo = typeof opts.returnTo === 'function' ? await opts.returnTo(params) : opts.returnTo; @@ -219,11 +224,13 @@ const appRouteHandlerFactory = * @ignore */ const pageRouteHandlerFactory = - (loginUrl: string, getSessionCache: () => SessionCache): WithPageAuthRequiredPageRouter => + (getConfig: GetConfig, sessionCache: SessionCache): WithPageAuthRequiredPageRouter => ({ getServerSideProps, returnTo } = {}) => async (ctx) => { assertCtx(ctx); - const sessionCache = getSessionCache(); + const { + routes: { login: loginUrl } + } = await getConfig(new NodeRequest(ctx.req)); const session = await sessionCache.get(ctx.req, ctx.res); if (!session?.user) { return { diff --git a/src/index.ts b/src/index.ts index b174483ac..8f5606e2b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,24 +9,20 @@ import { HandleLogin, HandleLogout, HandleProfile, - SessionCache, TouchSession, UpdateSession, WithApiAuthRequired, - WithPageAuthRequired, - telemetry + WithPageAuthRequired } from './shared'; import { _initAuth } from './init'; import { setIsUsingNamedExports, setIsUsingOwnInstance } from './utils/instance-check'; -import { getConfig, getLoginUrl } from './config'; -import { withPageAuthRequiredFactory } from './helpers'; -import { NodeClient } from './auth0-session/client/node-client'; +import { clientGetter } from './auth0-session/client/node-client'; const genId = () => crypto.randomBytes(16).toString('hex'); export type Auth0Server = Omit<Auth0ServerShared, 'withMiddlewareAuthRequired'>; -let instance: Auth0ServerShared & { sessionCache: SessionCache }; +let instance: Auth0ServerShared; /** * Initialise your own instance of the SDK. @@ -38,34 +34,34 @@ let instance: Auth0ServerShared & { sessionCache: SessionCache }; export type InitAuth0 = (params?: ConfigParameters) => Omit<Auth0Server, 'withMiddlewareAuthRequired'>; // For using managed instance with named exports. -function getInstance(): Auth0ServerShared & { sessionCache: SessionCache } { +function getInstance(): Auth0ServerShared { setIsUsingNamedExports(); if (instance) { return instance; } - const { baseConfig, nextConfig } = getConfig({ session: { genId } }); - const client = new NodeClient(baseConfig, telemetry); - instance = _initAuth({ baseConfig, nextConfig, client }); + instance = _initAuth({ genId, clientGetter }); return instance; } // For creating own instance. export const initAuth0: InitAuth0 = (params) => { setIsUsingOwnInstance(); - const { baseConfig, nextConfig } = getConfig({ ...params, session: { genId, ...params?.session } }); - const client = new NodeClient(baseConfig, telemetry); - const { sessionCache, withMiddlewareAuthRequired, ...publicApi } = _initAuth({ baseConfig, nextConfig, client }); + const { withMiddlewareAuthRequired, ...publicApi } = _initAuth({ + genId, + params, + clientGetter + }); return publicApi; }; -const getSessionCache = () => getInstance().sessionCache; export const getSession: GetSession = (...args) => getInstance().getSession(...args); export const updateSession: UpdateSession = (...args) => getInstance().updateSession(...args); export const getAccessToken: GetAccessToken = (...args) => getInstance().getAccessToken(...args); export const touchSession: TouchSession = (...args) => getInstance().touchSession(...args); export const withApiAuthRequired: WithApiAuthRequired = (...args) => (getInstance().withApiAuthRequired as any)(...args); -export const withPageAuthRequired: WithPageAuthRequired = withPageAuthRequiredFactory(getLoginUrl(), getSessionCache); +export const withPageAuthRequired: WithPageAuthRequired = ((...args: Parameters<WithPageAuthRequired>) => + getInstance().withPageAuthRequired(...args)) as WithPageAuthRequired; export const handleLogin: HandleLogin = ((...args: Parameters<HandleLogin>) => getInstance().handleLogin(...args)) as HandleLogin; export const handleLogout: HandleLogout = ((...args: Parameters<HandleLogout>) => diff --git a/src/init.ts b/src/init.ts index 73cb5779f..86b718851 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,25 +1,23 @@ import { - StatelessSession, - StatefulSession, TransientStore, loginHandler as baseLoginHandler, logoutHandler as baseLogoutHandler, callbackHandler as baseCallbackHandler, - AbstractClient + Telemetry } from './auth0-session'; import { handlerFactory, callbackHandler, loginHandler, logoutHandler, profileHandler } from './handlers'; import { sessionFactory, accessTokenFactory, SessionCache, - Session, touchSessionFactory, updateSessionFactory } from './session/'; import { withPageAuthRequiredFactory, withApiAuthRequiredFactory } from './helpers'; -import { ConfigParameters, BaseConfig, NextConfig } from './config'; -import { Auth0Server } from './shared'; +import { configSingletonGetter, ConfigParameters } from './config'; +import { Auth0Server, telemetry } from './shared'; import withMiddlewareAuthRequiredFactory from './helpers/with-middleware-auth-required'; +import { GetClient } from './auth0-session/client/abstract-client'; /** * Initialise your own instance of the SDK. @@ -31,43 +29,40 @@ import withMiddlewareAuthRequiredFactory from './helpers/with-middleware-auth-re export type InitAuth0 = (params?: ConfigParameters) => Auth0Server; export const _initAuth = ({ - baseConfig, - nextConfig, - client + params, + genId, + clientGetter }: { - baseConfig: BaseConfig; - nextConfig: NextConfig; - client: AbstractClient; -}): Auth0Server & { - sessionCache: SessionCache; -} => { + params?: ConfigParameters; + genId: () => string; + clientGetter: (telemetry: Telemetry) => GetClient; +}): Auth0Server => { + const getConfig = configSingletonGetter(params, genId); + const getClient = clientGetter(telemetry); + // Init base layer (with base config) - const transientStore = new TransientStore(baseConfig); + const transientStore = new TransientStore(getConfig); - const sessionStore = baseConfig.session.store - ? new StatefulSession<Session>(baseConfig) - : new StatelessSession<Session>(baseConfig); - const sessionCache = new SessionCache(baseConfig, sessionStore); - const baseHandleLogin = baseLoginHandler(baseConfig, client, transientStore); - const baseHandleLogout = baseLogoutHandler(baseConfig, client, sessionCache); - const baseHandleCallback = baseCallbackHandler(baseConfig, client, sessionCache, transientStore); + const sessionCache = new SessionCache(getConfig); + const baseHandleLogin = baseLoginHandler(getConfig, getClient, transientStore); + const baseHandleLogout = baseLogoutHandler(getConfig, getClient, sessionCache); + const baseHandleCallback = baseCallbackHandler(getConfig, getClient, sessionCache, transientStore); // Init Next layer (with next config) const getSession = sessionFactory(sessionCache); const touchSession = touchSessionFactory(sessionCache); const updateSession = updateSessionFactory(sessionCache); - const getAccessToken = accessTokenFactory(nextConfig, client, sessionCache); + const getAccessToken = accessTokenFactory(getConfig, getClient, sessionCache); const withApiAuthRequired = withApiAuthRequiredFactory(sessionCache); - const withPageAuthRequired = withPageAuthRequiredFactory(nextConfig.routes.login, () => sessionCache); - const handleLogin = loginHandler(baseHandleLogin, nextConfig, baseConfig); + const withPageAuthRequired = withPageAuthRequiredFactory(getConfig, sessionCache); + const handleLogin = loginHandler(baseHandleLogin, getConfig); const handleLogout = logoutHandler(baseHandleLogout); - const handleCallback = callbackHandler(baseHandleCallback, nextConfig); - const handleProfile = profileHandler(client, getAccessToken, sessionCache); + const handleCallback = callbackHandler(baseHandleCallback, getConfig); + const handleProfile = profileHandler(getConfig, getClient, getAccessToken, sessionCache); const handleAuth = handlerFactory({ handleLogin, handleLogout, handleCallback, handleProfile }); - const withMiddlewareAuthRequired = withMiddlewareAuthRequiredFactory(nextConfig.routes, () => sessionCache); + const withMiddlewareAuthRequired = withMiddlewareAuthRequiredFactory(getConfig, sessionCache); return { - sessionCache, getSession, touchSession, updateSession, diff --git a/src/session/cache.ts b/src/session/cache.ts index 4c7ac7c77..406396fc3 100644 --- a/src/session/cache.ts +++ b/src/session/cache.ts @@ -2,7 +2,7 @@ import { IncomingMessage, ServerResponse } from 'http'; import { NextApiRequest, NextApiResponse } from 'next'; import { NextRequest, NextResponse } from 'next/server'; import type { TokenEndpointResponse } from '../auth0-session'; -import { Config, SessionCache as ISessionCache, AbstractSession } from '../auth0-session'; +import { SessionCache as ISessionCache, AbstractSession, StatefulSession, StatelessSession } from '../auth0-session'; import Session, { fromJson, fromTokenEndpointResponse } from './session'; import { Auth0Request, Auth0Response, NodeRequest, NodeResponse } from '../auth0-session/http'; import { @@ -14,11 +14,12 @@ import { Auth0NextResponse } from '../http'; import { isNextApiRequest, isRequest } from '../utils/req-helpers'; +import { GetConfig, NextConfig } from '../config'; type Req = IncomingMessage | NextRequest | NextApiRequest; type Res = ServerResponse | NextResponse | NextApiResponse; -const getAuth0ReqRes = (req: Req, res: Res): [Auth0Request, Auth0Response] => { +export const getAuth0ReqRes = (req: Req, res: Res): [Auth0Request, Auth0Response] => { if (isRequest(req)) { return [new Auth0NextRequest(req as NextRequest), new Auth0NextResponse(res as NextResponse)]; } @@ -31,19 +32,31 @@ const getAuth0ReqRes = (req: Req, res: Res): [Auth0Request, Auth0Response] => { export default class SessionCache implements ISessionCache<Req, Res, Session> { private cache: WeakMap<Req, Session | null | undefined>; private iatCache: WeakMap<Req, number | undefined>; + private sessionStore?: AbstractSession<Session>; - constructor(public config: Config, public sessionStore: AbstractSession<Session>) { + constructor(public getConfig: GetConfig) { this.cache = new WeakMap(); this.iatCache = new WeakMap(); } + public getSessionStore(config: NextConfig): AbstractSession<Session> { + if (!this.sessionStore) { + this.sessionStore = config.session.store + ? new StatefulSession<Session>(config) + : new StatelessSession<Session>(config); + } + return this.sessionStore; + } + private async init(req: Req, res: Res, autoSave = true): Promise<void> { if (!this.cache.has(req)) { const [auth0Req] = getAuth0ReqRes(req, res); - const [json, iat] = await this.sessionStore.read(auth0Req); + const config = await this.getConfig(auth0Req); + const sessionStore = this.getSessionStore(config); + const [json, iat] = await sessionStore.read(auth0Req); this.iatCache.set(req, iat); this.cache.set(req, fromJson(json)); - if (this.config.session.rolling && this.config.session.autoSave && autoSave) { + if (config.session.rolling && config.session.autoSave && autoSave) { await this.save(req, res); } } @@ -51,7 +64,9 @@ export default class SessionCache implements ISessionCache<Req, Res, Session> { async save(req: Req, res: Res): Promise<void> { const [auth0Req, auth0Res] = getAuth0ReqRes(req, res); - await this.sessionStore.save(auth0Req, auth0Res, this.cache.get(req), this.iatCache.get(req)); + const config = await this.getConfig(auth0Req); + const sessionStore = this.getSessionStore(config); + await sessionStore.save(auth0Req, auth0Res, this.cache.get(req), this.iatCache.get(req)); } async create(req: Req, res: Res, session: Session): Promise<void> { @@ -88,8 +103,10 @@ export default class SessionCache implements ISessionCache<Req, Res, Session> { return this.cache.get(req); } - fromTokenEndpointResponse(tokenSet: TokenEndpointResponse): Session { - return fromTokenEndpointResponse(tokenSet, this.config); + async fromTokenEndpointResponse(req: Req, res: Res, tokenSet: TokenEndpointResponse): Promise<Session> { + const [auth0Req] = getAuth0ReqRes(req, res); + const config = await this.getConfig(auth0Req); + return fromTokenEndpointResponse(tokenSet, config); } } @@ -105,13 +122,12 @@ export const get = async ({ if (req && res) { return [await sessionCache.get(req, res)]; } - const { - sessionStore, - config: { - session: { rolling, autoSave } - } - } = sessionCache; const auth0Req = new Auth0NextRequestCookies(); + const config = await sessionCache.getConfig(auth0Req); + const sessionStore = sessionCache.getSessionStore(config); + const { + session: { rolling, autoSave } + } = config; const [session, iat] = await sessionStore.read(auth0Req); if (rolling && autoSave) { await set({ session, sessionCache, iat }); @@ -131,10 +147,12 @@ export const set = async ({ iat?: number; req?: Req; res?: Res; -}) => { +}): Promise<void> => { if (req && res) { return sessionCache.set(req, res, session); } - const { sessionStore } = sessionCache; - await sessionStore.save(new Auth0NextRequestCookies(), new Auth0NextResponseCookies(), session, iat); + const auth0Req = new Auth0NextRequestCookies(); + const config = await sessionCache.getConfig(auth0Req); + const sessionStore = sessionCache.getSessionStore(config); + await sessionStore.save(auth0Req, new Auth0NextResponseCookies(), session, iat); }; diff --git a/src/session/get-access-token.ts b/src/session/get-access-token.ts index 1995069a0..c4834b4f9 100644 --- a/src/session/get-access-token.ts +++ b/src/session/get-access-token.ts @@ -1,11 +1,14 @@ import { IncomingMessage, ServerResponse } from 'http'; import { NextApiRequest, NextApiResponse } from 'next'; -import { AbstractClient } from '../auth0-session'; +import { NextRequest, NextResponse } from 'next/server'; +import { AuthorizationParameters } from '../auth0-session'; import { AccessTokenError, AccessTokenErrorCode } from '../utils/errors'; import { intersect, match } from '../utils/array'; import { Session, SessionCache, fromTokenEndpointResponse, get, set } from '../session'; -import { AuthorizationParameters, NextConfig } from '../config'; -import { NextRequest, NextResponse } from 'next/server'; +import { GetClient } from '../auth0-session/client/abstract-client'; +import { GetConfig } from '../config'; +import { getAuth0ReqRes } from './cache'; +import { Auth0NextRequestCookies } from '../http'; /** * After refresh handler for page router {@link AfterRefreshPageRoute} and app router {@link AfterRefreshAppRoute}. @@ -226,13 +229,16 @@ export type GetAccessToken = ( * @ignore */ export default function accessTokenFactory( - config: NextConfig, - client: AbstractClient, + getConfig: GetConfig, + getClient: GetClient, sessionCache: SessionCache ): GetAccessToken { return async (reqOrOpts?, res?, accessTokenRequest?): Promise<GetAccessTokenResult> => { const options = (res ? accessTokenRequest : reqOrOpts) as AccessTokenRequest | undefined; const req = (res ? reqOrOpts : undefined) as IncomingMessage | NextApiRequest | undefined; + // TODO: clean up + const config = await getConfig(req ? getAuth0ReqRes(req, res as any)[0] : new Auth0NextRequestCookies()); + const client = await getClient(config); const parts = await get({ sessionCache, req, res }); let [session] = parts; diff --git a/src/session/session.ts b/src/session/session.ts index 057334952..b9b50b259 100644 --- a/src/session/session.ts +++ b/src/session/session.ts @@ -1,6 +1,5 @@ import * as jose from 'jose'; import type { TokenEndpointResponse } from '../auth0-session/client/abstract-client'; -import { Config } from '../auth0-session'; import { NextConfig } from '../config'; /** @@ -61,10 +60,7 @@ export default class Session { /** * @ignore */ -export function fromTokenEndpointResponse( - tokenEndpointResponse: TokenEndpointResponse, - config: Config | NextConfig -): Session { +export function fromTokenEndpointResponse(tokenEndpointResponse: TokenEndpointResponse, config: NextConfig): Session { // Get the claims without any OIDC-specific claim. const claims = jose.decodeJwt(tokenEndpointResponse.id_token as string); config.identityClaimFilter.forEach((claim) => { diff --git a/tests/auth0-session/client/edge-client.test.ts b/tests/auth0-session/client/edge-client.test.ts index a4c9afb48..5f37bc7a1 100644 --- a/tests/auth0-session/client/edge-client.test.ts +++ b/tests/auth0-session/client/edge-client.test.ts @@ -8,7 +8,7 @@ import { jwks, makeIdToken } from '../fixtures/cert'; import pkg from '../../../package.json'; import wellKnown from '../fixtures/well-known.json'; import version from '../../../src/version'; -import { EdgeClient } from '../../../src/auth0-session/client/edge-client'; +import { EdgeClient, clientGetter } from '../../../src/auth0-session/client/edge-client'; import { mockFetch } from '../../fixtures/app-router-helpers'; import { Auth0Request } from '../../../src/auth0-session/http'; import { readFileSync } from 'fs'; @@ -50,14 +50,14 @@ const defaultConfig: ConfigParameters = { }; const getClient = async (params: ConfigParameters = {}): Promise<EdgeClient> => { - return new EdgeClient(getConfig({ ...defaultConfig, ...params }), { + return clientGetter({ name: 'nextjs-auth0', version - }); + })(getConfig({ ...defaultConfig, ...params })); }; describe('edge client', function () { - let headersSpy = jest.fn(); + const headersSpy = jest.fn(); beforeEach(() => { mockFetch(); @@ -147,7 +147,7 @@ describe('edge client', function () { idTokenSigningAlg: 'RS256' }); // @ts-ignore - expect((await client.getClient())[1].id_token_signed_response_alg).toEqual('RS256'); + expect(client.client.id_token_signed_response_alg).toEqual('RS256'); }); it('should use discovered logout endpoint by default', async function () { @@ -264,14 +264,10 @@ describe('edge client', function () { ); await expect( - ( - await getClient({ - issuerBaseURL: 'https://op2.example.com', - idpLogout: true - }) - ) - // @ts-ignore - .getClient() + getClient({ + issuerBaseURL: 'https://op2.example.com', + idpLogout: true + }) ).resolves.not.toThrow(); }); @@ -279,9 +275,7 @@ describe('edge client', function () { nock.cleanAll(); nock('https://op.example.com').get('/.well-known/oauth-authorization-server').reply(500); nock('https://op.example.com').get('/.well-known/openid-configuration').reply(500); - await expect((await getClient()).userinfo('token')).rejects.toThrow( - /Discovery requests failing for https:\/\/op.example.com/ - ); + await expect(getClient()).rejects.toThrow(/Discovery requests failing for https:\/\/op.example.com/); }); it('should throw UserInfoError when userinfo fails', async () => { @@ -293,8 +287,7 @@ describe('edge client', function () { }); it('should only support code flow', async () => { - const client = await getClient({ authorizationParams: { response_type: 'id_token' } }); - await expect(client.authorizationUrl({})).rejects.toThrow( + await expect(getClient({ authorizationParams: { response_type: 'id_token' } })).rejects.toThrow( 'This SDK only supports `response_type=code` when used in an Edge runtime.' ); }); diff --git a/tests/auth0-session/client/node-client.test.ts b/tests/auth0-session/client/node-client.test.ts index c68fae9d2..1ba87dbfb 100644 --- a/tests/auth0-session/client/node-client.test.ts +++ b/tests/auth0-session/client/node-client.test.ts @@ -4,7 +4,7 @@ import { jwks } from '../fixtures/cert'; import pkg from '../../../package.json'; import wellKnown from '../fixtures/well-known.json'; import version from '../../../src/version'; -import { NodeClient } from '../../../src/auth0-session/client/node-client'; +import { NodeClient, clientGetter } from '../../../src/auth0-session/client/node-client'; import { UserInfoError } from '../../../src/auth0-session/utils/errors'; const defaultConfig = { @@ -19,10 +19,10 @@ const defaultConfig = { }; const getClient = async (params: ConfigParameters = {}): Promise<NodeClient> => { - return new NodeClient(getConfig({ ...defaultConfig, ...params }), { + return clientGetter({ name: 'nextjs-auth0', version - }); + })(getConfig({ ...defaultConfig, ...params })); }; describe('node client', function () { @@ -70,17 +70,11 @@ describe('node client', function () { }); it('should accept lazy config', async function () { - expect( - () => - new NodeClient( - () => { - throw new Error(); - }, - { - name: 'nextjs-auth0', - version - } - ) + expect(() => + clientGetter({ + name: 'nextjs-auth0', + version + }) ).not.toThrow(); }); @@ -107,7 +101,7 @@ describe('node client', function () { idTokenSigningAlg: 'RS256' }); // @ts-ignore - expect((await client.getClient()).id_token_signed_response_alg).toEqual('RS256'); + expect((await client.client).id_token_signed_response_alg).toEqual('RS256'); }); it('should use discovered logout endpoint by default', async function () { @@ -209,14 +203,10 @@ describe('node client', function () { ); await expect( - ( - await getClient({ - issuerBaseURL: 'https://op2.example.com', - idpLogout: true - }) - ) - // @ts-ignore - .getClient() + getClient({ + issuerBaseURL: 'https://op2.example.com', + idpLogout: true + }) ).resolves.not.toThrow(); }); @@ -224,7 +214,7 @@ describe('node client', function () { nock.cleanAll(); nock('https://op.example.com').get('/.well-known/oauth-authorization-server').reply(500); nock('https://op.example.com').get('/.well-known/openid-configuration').reply(500); - await expect((await getClient()).userinfo('token')).rejects.toThrow( + await expect(getClient).rejects.toThrow( 'Discovery requests failing for https://op.example.com, expected 200 OK, got: 500 Internal Server Error' ); }); diff --git a/tests/auth0-session/fixtures/server.ts b/tests/auth0-session/fixtures/server.ts index e99637982..a5c228430 100644 --- a/tests/auth0-session/fixtures/server.ts +++ b/tests/auth0-session/fixtures/server.ts @@ -25,7 +25,7 @@ import { cert, key } from './https'; import { Claims } from '../../../src/session'; import version from '../../../src/version'; import { NodeRequest, NodeResponse } from '../../../src/auth0-session/http'; -import { NodeClient } from '../../../src/auth0-session/client/node-client'; +import { clientGetter } from '../../../src/auth0-session/client/node-client'; export type SessionResponse = TokenSetParameters & { claims: Claims }; @@ -54,7 +54,11 @@ class TestSessionCache implements SessionCache<IncomingMessage, ServerResponse> const [session] = await this.cookieStore.read(new NodeRequest(req)); return session?.id_token; } - fromTokenEndpointResponse(tokenSet: TokenSet): { [p: string]: any } { + async fromTokenEndpointResponse( + _req: IncomingMessage, + _res: ServerResponse, + tokenSet: TokenSet + ): Promise<{ [p: string]: any }> { return tokenSet; } } @@ -68,15 +72,15 @@ type Handlers = { const createHandlers = (params: ConfigParameters): Handlers => { const config = getConfig(params); - const client = new NodeClient(config, { name: 'nextjs-auth0', version }); + const getClient = clientGetter({ name: 'nextjs-auth0', version }); const transientStore = new TransientStore(config); const cookieStore = params.session?.store ? new StatefulSession<any>(config) : new StatelessSession<any>(config); const sessionCache = new TestSessionCache(cookieStore); return { - handleLogin: loginHandler(config, client, transientStore), - handleLogout: logoutHandler(config, client, sessionCache), - handleCallback: callbackHandler(config, client, sessionCache, transientStore), + handleLogin: loginHandler(config, getClient, transientStore), + handleLogout: logoutHandler(config, getClient, sessionCache), + handleCallback: callbackHandler(config, getClient, sessionCache, transientStore), handleSession: async (req: IncomingMessage, res: ServerResponse) => { const nodeReq = new NodeRequest(req); const [json, iat] = await cookieStore.read(nodeReq); diff --git a/tests/config.test.ts b/tests/config.test.ts index 4022bde67..c6dd7bd7c 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -1,4 +1,4 @@ -import { BaseConfig, NextConfig, getConfig } from '../src/config'; +import { NextConfig, getConfig } from '../src/config'; const getConfigWithEnv = ( env: any = {}, @@ -10,7 +10,7 @@ const getConfigWithEnv = ( AUTH0_CLIENT_ID: '__test_client_id__', AUTH0_CLIENT_SECRET: '__test_client_secret__' } -): { baseConfig: BaseConfig; nextConfig: NextConfig } => { +): NextConfig => { const bkp = process.env; process.env = { ...process.env, @@ -28,8 +28,8 @@ const getConfigWithEnv = ( describe('config params', () => { test('should return an object from empty defaults', () => { - const { baseConfig, nextConfig } = getConfigWithEnv(); - expect(baseConfig).toStrictEqual({ + const nextConfig = getConfigWithEnv(); + expect(nextConfig).toStrictEqual({ secret: '__long_super_secret_secret__', issuerBaseURL: 'https://example.auth0.com', baseURL: 'https://example.com', @@ -65,7 +65,7 @@ describe('config params', () => { sameSite: 'lax' } }, - routes: { callback: '/api/auth/callback', postLogoutRedirect: '' }, + routes: { callback: '/api/auth/callback', postLogoutRedirect: '', login: '/api/auth/login' }, getLoginState: expect.any(Function), identityClaimFilter: [ 'aud', @@ -87,31 +87,8 @@ describe('config params', () => { path: '/', sameSite: 'lax', secure: true - } - }); - expect(nextConfig).toStrictEqual({ - identityClaimFilter: [ - 'aud', - 'iss', - 'iat', - 'exp', - 'nbf', - 'nonce', - 'azp', - 'auth_time', - 's_hash', - 'at_hash', - 'c_hash' - ], - routes: { - login: '/api/auth/login', - callback: '/api/auth/callback', - postLogoutRedirect: '' }, - organization: undefined, - session: { - storeIDToken: true - } + organization: undefined }); }); @@ -128,7 +105,7 @@ describe('config params', () => { AUTH0_COOKIE_SECURE: 'ok', AUTH0_SESSION_ABSOLUTE_DURATION: 'no', AUTH0_SESSION_STORE_ID_TOKEN: '0' - }).baseConfig + }) ).toMatchObject({ auth0Logout: false, enableTelemetry: false, @@ -149,7 +126,7 @@ describe('config params', () => { getConfigWithEnv({ AUTH0_SESSION_ROLLING_DURATION: 'no', AUTH0_SESSION_ROLLING: 'no' - }).baseConfig + }) ).toMatchObject({ session: { rolling: false, @@ -165,7 +142,7 @@ describe('config params', () => { AUTH0_HTTP_TIMEOUT: '9999', AUTH0_SESSION_ROLLING_DURATION: '0', AUTH0_SESSION_ABSOLUTE_DURATION: '1' - }).baseConfig + }) ).toMatchObject({ clockTolerance: 100, httpTimeout: 9999, @@ -181,14 +158,14 @@ describe('config params', () => { expect( getConfigWithEnv({ AUTH0_IDENTITY_CLAIM_FILTER: 'claim1,claim2,claim3' - }).baseConfig + }) ).toMatchObject({ identityClaimFilter: ['claim1', 'claim2', 'claim3'] }); }); test('passed in arguments should take precedence', () => { - const { baseConfig, nextConfig } = getConfigWithEnv( + const nextConfig = getConfigWithEnv( { AUTH0_ORGANIZATION: 'foo' }, @@ -212,7 +189,7 @@ describe('config params', () => { organization: 'bar' } ); - expect(baseConfig).toMatchObject({ + expect(nextConfig).toMatchObject({ authorizationParams: { audience: 'foo', scope: 'openid bar' @@ -228,9 +205,7 @@ describe('config params', () => { transient: false }, name: 'quuuux' - } - }); - expect(nextConfig).toMatchObject({ + }, organization: 'bar' }); }); @@ -239,7 +214,7 @@ describe('config params', () => { expect( getConfigWithEnv({ AUTH0_BASE_URL: 'foo.auth0.com' - }).baseConfig + }) ).toMatchObject({ baseURL: 'https://foo.auth0.com' }); @@ -259,7 +234,7 @@ describe('config params', () => { AUTH0_CLIENT_ID: '__test_client_id__', AUTH0_CLIENT_SECRET: '__test_client_secret__' } - ).baseConfig + ) ).toMatchObject({ baseURL: 'https://public-foo.auth0.com' }); @@ -270,19 +245,16 @@ describe('config params', () => { getConfigWithEnv({ AUTH0_BASE_URL: 'foo.auth0.com', NEXT_PUBLIC_AUTH0_BASE_URL: 'bar.auth0.com' - }).baseConfig + }) ).toMatchObject({ baseURL: 'https://foo.auth0.com' }); }); test('should accept optional callback path', () => { - const { baseConfig, nextConfig } = getConfigWithEnv({ + const nextConfig = getConfigWithEnv({ AUTH0_CALLBACK: '/api/custom-callback' }); - expect(baseConfig).toMatchObject({ - routes: expect.objectContaining({ callback: '/api/custom-callback' }) - }); expect(nextConfig).toMatchObject({ routes: expect.objectContaining({ callback: '/api/custom-callback' }) }); diff --git a/tests/fixtures/app-router-helpers.ts b/tests/fixtures/app-router-helpers.ts index d84eabe25..f2af60c32 100644 --- a/tests/fixtures/app-router-helpers.ts +++ b/tests/fixtures/app-router-helpers.ts @@ -114,7 +114,7 @@ export const getSession = async (config: any, res: NextResponse) => { .getAll() .forEach(({ name, value }: { name: string; value: string }) => value && req.cookies.set(name, value)); - const store = new StatelessSession(getConfig(config).baseConfig); + const store = new StatelessSession(getConfig(config)); const [session] = await store.read(new Auth0NextRequest(req)); return session; }; diff --git a/tests/helpers/testing.test.ts b/tests/helpers/testing.test.ts index 95c7764d0..4d192d5a9 100644 --- a/tests/helpers/testing.test.ts +++ b/tests/helpers/testing.test.ts @@ -4,6 +4,7 @@ import { generateSessionCookie } from '../../src/helpers/testing'; jest.mock('../../src/auth0-session/session/stateless-session'); const encryptMock = jest.spyOn(CookieStore.prototype, 'encrypt'); +jest.spyOn(CookieStore.prototype, 'getKeys').mockReturnValue(Promise.resolve([])); const weekInSeconds = 7 * 24 * 60 * 60; describe('generate-session-cookie', () => { @@ -54,7 +55,7 @@ describe('generate-session-cookie', () => { test('use the provided session', async () => { await generateSessionCookie({ user: { foo: 'bar' } }, { secret: '' }); - expect(encryptMock).toHaveBeenCalledWith({ user: { foo: 'bar' } }, expect.anything()); + expect(encryptMock).toHaveBeenCalledWith({ user: { foo: 'bar' } }, expect.anything(), undefined); }); test('use the current time for the header values', async () => { @@ -63,11 +64,15 @@ describe('generate-session-cookie', () => { const clock = jest.useFakeTimers(); clock.setSystemTime(now); await generateSessionCookie({}, { secret: '' }); - expect(encryptMock).toHaveBeenCalledWith(expect.anything(), { - iat: current, - uat: current, - exp: current + weekInSeconds - }); + expect(encryptMock).toHaveBeenCalledWith( + expect.anything(), + { + iat: current, + uat: current, + exp: current + weekInSeconds + }, + undefined + ); clock.restoreAllMocks(); jest.useRealTimers(); }); diff --git a/tests/index.test.ts b/tests/index.test.ts index 87cfcc679..084717642 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,7 +1,15 @@ import { IncomingMessage, ServerResponse } from 'http'; import { Socket } from 'net'; import { withoutApi } from './fixtures/default-settings'; -import { WithApiAuthRequired, WithPageAuthRequired, InitAuth0, GetSession, ConfigParameters } from '../src'; +import { + WithApiAuthRequired, + WithPageAuthRequired, + InitAuth0, + GetSession, + ConfigParameters, + AppRouteHandlerFn +} from '../src'; +import { NextRequest } from 'next/server'; describe('index', () => { let withPageAuthRequired: WithPageAuthRequired, @@ -31,9 +39,14 @@ describe('index', () => { jest.resetModules(); }); - test('withPageAuthRequired should not create an SDK instance at build time', () => { + test('withPageAuthRequired should not create an SDK instance at build time', async () => { process.env = { ...env, AUTH0_SECRET: undefined }; - expect(() => withApiAuthRequired(jest.fn())).toThrow('"secret" is required'); + await expect(() => + withApiAuthRequired(jest.fn() as AppRouteHandlerFn)(new NextRequest(new URL('http://example.com')), { + params: {} + }) + ).rejects.toThrow('"secret" is required'); + expect(() => withApiAuthRequired(jest.fn())).not.toThrow(); expect(() => withPageAuthRequired()).not.toThrow(); }); diff --git a/tests/session/cache.test.ts b/tests/session/cache.test.ts index cf4c3ad6c..370d10fcd 100644 --- a/tests/session/cache.test.ts +++ b/tests/session/cache.test.ts @@ -1,6 +1,7 @@ import { IncomingMessage, ServerResponse } from 'http'; import { Socket } from 'net'; -import { StatelessSession, getConfig } from '../../src/auth0-session'; +import { StatelessSession } from '../../src/auth0-session'; +import { getConfig } from '../../src/config'; import { get, set } from '../../src/session/cache'; import { ConfigParameters, Session, SessionCache } from '../../src'; import { withoutApi } from '../fixtures/default-settings'; @@ -18,7 +19,8 @@ describe('SessionCache', () => { sessionStore.save = jest.fn(); session = new Session({ sub: '__test_user__' }); session.idToken = '__test_id_token__'; - cache = new SessionCache(config, sessionStore); + cache = new SessionCache(() => config); + cache.getSessionStore = () => sessionStore; req = jest.mocked(new IncomingMessage(new Socket())); res = jest.mocked(new ServerResponse(req)); }; diff --git a/tests/session/session.test.ts b/tests/session/session.test.ts index 3c68c8498..a039776b4 100644 --- a/tests/session/session.test.ts +++ b/tests/session/session.test.ts @@ -2,6 +2,8 @@ import { TokenSet } from 'openid-client'; import { fromJson, fromTokenEndpointResponse } from '../../src/session'; import { makeIdToken } from '../auth0-session/fixtures/cert'; import { Session } from '../../src'; +import { getConfig } from '../../src/config'; +import { withoutApi } from '../fixtures/default-settings'; const routes = { login: '', callback: '', postLogoutRedirect: '' }; @@ -15,12 +17,15 @@ describe('session', () => { describe('from tokenSet', () => { test('should construct a session from a tokenSet', async () => { expect( - fromTokenEndpointResponse(new TokenSet({ id_token: await makeIdToken({ foo: 'bar', bax: 'qux' }) }), { - identityClaimFilter: ['baz'], - routes, - getLoginState, - session: { storeIDToken: true } - }).user + fromTokenEndpointResponse( + new TokenSet({ id_token: await makeIdToken({ foo: 'bar', bax: 'qux' }) }), + getConfig({ + ...withoutApi, + identityClaimFilter: ['baz'], + getLoginState, + session: { storeIDToken: true } + }) + ).user ).toEqual({ aud: '__test_client_id__', bax: 'qux', @@ -36,30 +41,38 @@ describe('session', () => { test('should store the ID Token by default', async () => { expect( - fromTokenEndpointResponse(new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), { - identityClaimFilter: ['baz'], - routes, - getLoginState, - session: { storeIDToken: true } - }).idToken + fromTokenEndpointResponse( + new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), + getConfig({ + ...withoutApi, + identityClaimFilter: ['baz'], + routes, + getLoginState, + session: { storeIDToken: true } + }) + ).idToken ).toBeDefined(); }); test('should not store the ID Token', async () => { expect( - fromTokenEndpointResponse(new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), { - session: { - storeIDToken: false, - name: '', - rolling: false, - rollingDuration: 0, - absoluteDuration: 0, - cookie: { transient: false, httpOnly: false, sameSite: 'lax' } - }, - getLoginState, - identityClaimFilter: ['baz'], - routes - }).idToken + fromTokenEndpointResponse( + new TokenSet({ id_token: await makeIdToken({ foo: 'bar' }) }), + getConfig({ + ...withoutApi, + session: { + storeIDToken: false, + name: 'foo', + rolling: false, + rollingDuration: false, + absoluteDuration: 0, + cookie: { transient: false, httpOnly: false, sameSite: 'lax' } + }, + getLoginState, + identityClaimFilter: ['baz'], + routes + }) + ).idToken ).toBeUndefined(); }); }); From bec7d6c5998ba06ae87cb906235d7568bab2418f Mon Sep 17 00:00:00 2001 From: Adam Mcgrath <adamjmcgrath@gmail.com> Date: Tue, 7 Nov 2023 13:06:04 +0000 Subject: [PATCH 3/7] Bail out of static rendering for pages and routes in app dir --- src/config.ts | 10 +++++++++- tests/config.test.ts | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 450d3d335..3251f45cd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -263,8 +263,16 @@ export type GetConfig = (req: Auth0Request | Auth0RequestCookies) => Promise<Nex export const configSingletonGetter = (params: ConfigParameters = {}, genId: () => string): GetConfig => { let config: NextConfig; - return () => { + return (req) => { if (!config) { + // Bails out of static rendering for Server Components + // Need to query cookies because Server Components don't have access to URL + req.getCookies(); + if ('getUrl' in req) { + // Bail out of static rendering for API Routes + // Reading cookies is not always enough https://github.com/vercel/next.js/issues/49006 + req.getUrl(); + } config = getConfig({ ...params, session: { genId, ...params.session } }); } return config; diff --git a/tests/config.test.ts b/tests/config.test.ts index c6dd7bd7c..4afd999e6 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -1,4 +1,6 @@ -import { NextConfig, getConfig } from '../src/config'; +import { NextRequest } from 'next/server'; +import { NextConfig, getConfig, configSingletonGetter } from '../src/config'; +import { Auth0NextRequest, Auth0NextRequestCookies } from '../src/http'; const getConfigWithEnv = ( env: any = {}, @@ -259,4 +261,26 @@ describe('config params', () => { routes: expect.objectContaining({ callback: '/api/custom-callback' }) }); }); + + test('getConfig should query RSC cookies to bail out of static rendering', async () => { + const req = jest.mocked(new Auth0NextRequestCookies()); + jest.spyOn(req, 'getCookies').mockImplementation(() => { + throw new Error('BAIL'); + }); + const getConfig = configSingletonGetter({}, () => ''); + await expect(() => getConfig(req)).toThrow('BAIL'); + await expect(() => getConfig(req)).not.toThrow('"secret" is required'); + expect(req.getCookies).toHaveBeenCalled(); + }); + + test('getConfig should query API route URL to bail out of static rendering', async () => { + const req = jest.mocked(new Auth0NextRequest(new NextRequest(new URL('http://example.com')))); + jest.spyOn(req, 'getUrl').mockImplementation(() => { + throw new Error('BAIL'); + }); + const getConfig = configSingletonGetter({}, () => ''); + await expect(() => getConfig(req)).toThrow('BAIL'); + await expect(() => getConfig(req)).not.toThrow('"secret" is required'); + expect(req.getUrl).toHaveBeenCalled(); + }); }); From 77b2ef052f0c1ea46ed82c6c4dd33f197f19ba7c Mon Sep 17 00:00:00 2001 From: Adam Mcgrath <adamjmcgrath@gmail.com> Date: Tue, 7 Nov 2023 13:30:45 +0000 Subject: [PATCH 4/7] Prepare experimental release --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85a3c6194..bd9003957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@auth0/nextjs-auth0", - "version": "3.2.0", + "version": "3.2.0-experimental-lazy-conf.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@auth0/nextjs-auth0", - "version": "3.2.0", + "version": "3.2.0-experimental-lazy-conf.0", "license": "MIT", "dependencies": { "@panva/hkdf": "^1.0.2", diff --git a/package.json b/package.json index de45cebaf..e5a2b2c4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@auth0/nextjs-auth0", - "version": "3.2.0", + "version": "3.2.0-experimental-lazy-conf.0", "description": "Next.js SDK for signing in with Auth0", "exports": { ".": "./dist/index.js", diff --git a/src/version.ts b/src/version.ts index 2e09da8c6..d1d7a0067 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export default '3.2.0'; +export default '3.2.0-experimental-lazy-conf.0'; From 62a26260bc541afaa222d160c46f25721049d5e1 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath <adamjmcgrath@gmail.com> Date: Tue, 7 Nov 2023 14:55:16 +0000 Subject: [PATCH 5/7] Fix edge client issue with reusing abort signal --- src/auth0-session/client/abstract-client.ts | 3 +- src/auth0-session/client/edge-client.ts | 150 +++++----- src/auth0-session/client/node-client.ts | 261 ++++++++++-------- .../auth0-session/client/edge-client.test.ts | 26 +- .../auth0-session/client/node-client.test.ts | 31 +-- 5 files changed, 252 insertions(+), 219 deletions(-) diff --git a/src/auth0-session/client/abstract-client.ts b/src/auth0-session/client/abstract-client.ts index 48f92edfd..54ca85f46 100644 --- a/src/auth0-session/client/abstract-client.ts +++ b/src/auth0-session/client/abstract-client.ts @@ -1,5 +1,5 @@ -import { Auth0Request } from '../http'; import { Config } from '../config'; +import { Auth0Request } from '../http'; export type Telemetry = { name: string; @@ -85,6 +85,7 @@ export interface AuthorizationParameters { } export abstract class AbstractClient { + constructor(protected config: Config, protected telemetry: Telemetry) {} abstract authorizationUrl(parameters: Record<string, unknown>): Promise<string>; abstract callbackParams(req: Auth0Request, expectedState: string): Promise<URLSearchParams>; abstract callback( diff --git a/src/auth0-session/client/edge-client.ts b/src/auth0-session/client/edge-client.ts index 4d80ccb48..d8acd6a8f 100644 --- a/src/auth0-session/client/edge-client.ts +++ b/src/auth0-session/client/edge-client.ts @@ -26,17 +26,69 @@ const encodeBase64 = (input: string) => { }; export class EdgeClient extends AbstractClient { - constructor( - private client: oauth.Client, - private as: oauth.AuthorizationServer, - private config: Config, - private httpOptions: oauth.HttpRequestOptions - ) { - super(); + private client?: oauth.Client; + private as?: oauth.AuthorizationServer; + private httpOptions: () => oauth.HttpRequestOptions; + + constructor(protected config: Config, protected telemetry: Telemetry) { + super(config, telemetry); + if (config.authorizationParams.response_type !== 'code') { + throw new Error('This SDK only supports `response_type=code` when used in an Edge runtime.'); + } + + this.httpOptions = () => { + const headers = new Headers(); + if (config.enableTelemetry) { + const { name, version } = telemetry; + headers.set('User-Agent', `${name}/${version}`); + headers.set( + 'Auth0-Client', + encodeBase64( + JSON.stringify({ + name, + version, + env: { + edge: true + } + }) + ) + ); + } + return { + signal: AbortSignal.timeout(this.config.httpTimeout), + headers + }; + }; + } + + private async getClient(): Promise<[oauth.AuthorizationServer, oauth.Client]> { + if (this.as) { + return [this.as, this.client as oauth.Client]; + } + + const issuer = new URL(this.config.issuerBaseURL); + try { + this.as = await oauth + .discoveryRequest(issuer, this.httpOptions()) + .then((response) => oauth.processDiscoveryResponse(issuer, response)); + } catch (e) { + throw new DiscoveryError(e, this.config.issuerBaseURL); + } + + this.client = { + client_id: this.config.clientID, + ...(!this.config.clientAssertionSigningKey && { client_secret: this.config.clientSecret }), + token_endpoint_auth_method: this.config.clientAuthMethod, + id_token_signed_response_alg: this.config.idTokenSigningAlg, + [oauth.clockTolerance]: this.config.clockTolerance + }; + + return [this.as, this.client]; } async authorizationUrl(parameters: Record<string, unknown>): Promise<string> { - const authorizationUrl = new URL(this.as.authorization_endpoint as string); + const [as] = await this.getClient(); + const authorizationUrl = new URL(as.authorization_endpoint as string); authorizationUrl.searchParams.set('client_id', this.config.clientID); Object.entries(parameters).forEach(([key, value]) => { if (value === null || value === undefined) { @@ -48,11 +100,12 @@ export class EdgeClient extends AbstractClient { } async callbackParams(req: Auth0Request, expectedState: string) { + const [as, client] = await this.getClient(); const url = req.getMethod().toUpperCase() === 'GET' ? new URL(req.getUrl()) : new URLSearchParams(await req.getBody()); let result: ReturnType<typeof oauth.validateAuthResponse>; try { - result = oauth.validateAuthResponse(this.as, this.client, url, expectedState); + result = oauth.validateAuthResponse(as, client, url, expectedState); } catch (e) { throw new ApplicationError(e); } @@ -72,6 +125,8 @@ export class EdgeClient extends AbstractClient { checks: OpenIDCallbackChecks, extras: CallbackExtras ): Promise<TokenEndpointResponse> { + const [as, client] = await this.getClient(); + const { clientAssertionSigningKey, clientAssertionSigningAlg } = this.config; let clientPrivateKey = clientAssertionSigningKey as CryptoKey | undefined; @@ -80,21 +135,21 @@ export class EdgeClient extends AbstractClient { clientPrivateKey = await jose.importPKCS8<CryptoKey>(clientPrivateKey, clientAssertionSigningAlg || 'RS256'); } const response = await oauth.authorizationCodeGrantRequest( - this.as, - this.client, + as, + client, parameters, redirectUri, checks.code_verifier as string, { additionalParameters: extras.exchangeBody, ...(clientPrivateKey && { clientPrivateKey }), - ...this.httpOptions + ...this.httpOptions() } ); const result = await oauth.processAuthorizationCodeOpenIDResponse( - this.as, - this.client, + as, + client, response, checks.nonce, checks.max_age @@ -110,14 +165,15 @@ export class EdgeClient extends AbstractClient { } async endSessionUrl(parameters: EndSessionParameters): Promise<string> { - const issuerUrl = new URL(this.as.issuer); + const [as] = await this.getClient(); + const issuerUrl = new URL(as.issuer); if ( this.config.idpLogout && (this.config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && this.config.auth0Logout !== false)) ) { const { id_token_hint, post_logout_redirect_uri, ...extraParams } = parameters; - const auth0LogoutUrl: URL = new URL(urlJoin(this.as.issuer, '/v2/logout')); + const auth0LogoutUrl: URL = new URL(urlJoin(as.issuer, '/v2/logout')); post_logout_redirect_uri && auth0LogoutUrl.searchParams.set('returnTo', post_logout_redirect_uri); auth0LogoutUrl.searchParams.set('client_id', this.config.clientID); Object.entries(extraParams).forEach(([key, value]: [string, string]) => { @@ -128,10 +184,10 @@ export class EdgeClient extends AbstractClient { }); return auth0LogoutUrl.toString(); } - if (!this.as.end_session_endpoint) { + if (!as.end_session_endpoint) { throw new Error('RP Initiated Logout is not supported on your Authorization Server.'); } - const oidcLogoutUrl = new URL(this.as.end_session_endpoint); + const oidcLogoutUrl = new URL(as.end_session_endpoint); Object.entries(parameters).forEach(([key, value]: [string, string]) => { if (value === null || value === undefined) { return; @@ -144,21 +200,23 @@ export class EdgeClient extends AbstractClient { } async userinfo(accessToken: string): Promise<Record<string, unknown>> { - const response = await oauth.userInfoRequest(this.as, this.client, accessToken, this.httpOptions); + const [as, client] = await this.getClient(); + const response = await oauth.userInfoRequest(as, client, accessToken, this.httpOptions()); try { - return await oauth.processUserInfoResponse(this.as, this.client, oauth.skipSubjectCheck, response); + return await oauth.processUserInfoResponse(as, client, oauth.skipSubjectCheck, response); } catch (e) { throw new UserInfoError(e.message); } } async refresh(refreshToken: string, extras: { exchangeBody: Record<string, any> }): Promise<TokenEndpointResponse> { - const res = await oauth.refreshTokenGrantRequest(this.as, this.client, refreshToken, { + const [as, client] = await this.getClient(); + const res = await oauth.refreshTokenGrantRequest(as, client, refreshToken, { additionalParameters: extras.exchangeBody, - ...this.httpOptions + ...this.httpOptions() }); - const result = await oauth.processRefreshTokenResponse(this.as, this.client, res); + const result = await oauth.processRefreshTokenResponse(as, client, res); if (oauth.isOAuth2Error(result)) { throw new AccessTokenError( AccessTokenErrorCode.FAILED_REFRESH_GRANT, @@ -190,51 +248,7 @@ export const clientGetter = (telemetry: Telemetry): ((config: Config) => Promise let client: EdgeClient; return async (config) => { if (!client) { - const headers = new Headers(); - if (config.enableTelemetry) { - const { name, version } = telemetry; - headers.set('User-Agent', `${name}/${version}`); - headers.set( - 'Auth0-Client', - encodeBase64( - JSON.stringify({ - name, - version, - env: { - edge: true - } - }) - ) - ); - } - const httpOptions: oauth.HttpRequestOptions = { - signal: AbortSignal.timeout(config.httpTimeout), - headers - }; - - if (config.authorizationParams.response_type !== 'code') { - throw new Error('This SDK only supports `response_type=code` when used in an Edge runtime.'); - } - - const issuer = new URL(config.issuerBaseURL); - let as: oauth.AuthorizationServer; - try { - as = await oauth - .discoveryRequest(issuer, httpOptions) - .then((response) => oauth.processDiscoveryResponse(issuer, response)); - } catch (e) { - throw new DiscoveryError(e, config.issuerBaseURL); - } - - const oauthClient: oauth.Client = { - client_id: config.clientID, - ...(!config.clientAssertionSigningKey && { client_secret: config.clientSecret }), - token_endpoint_auth_method: config.clientAuthMethod, - id_token_signed_response_alg: config.idTokenSigningAlg, - [oauth.clockTolerance]: config.clockTolerance - }; - - client = new EdgeClient(oauthClient, as, config, httpOptions); + client = new EdgeClient(config, telemetry); } return client; }; diff --git a/src/auth0-session/client/node-client.ts b/src/auth0-session/client/node-client.ts index da56a4b52..e4fa34c58 100644 --- a/src/auth0-session/client/node-client.ts +++ b/src/auth0-session/client/node-client.ts @@ -33,16 +33,142 @@ function sortSpaceDelimitedString(str: string): string { } export class NodeClient extends AbstractClient { - constructor(private client: Client) { - super(); + private client?: Client; + + private async getClient(): Promise<Client> { + if (this.client) { + return this.client; + } + const { + config, + telemetry: { name, version } + } = this; + + const defaultHttpOptions: CustomHttpOptionsProvider = (_url, options) => ({ + ...options, + headers: { + ...options.headers, + 'User-Agent': `${name}/${version}`, + ...(config.enableTelemetry + ? { + 'Auth0-Client': Buffer.from( + JSON.stringify({ + name, + version, + env: { + node: process.version + } + }) + ).toString('base64') + } + : undefined) + }, + timeout: config.httpTimeout, + agent: config.httpAgent + }); + const applyHttpOptionsCustom = (entity: Issuer<Client> | typeof Issuer | Client) => { + entity[custom.http_options] = defaultHttpOptions; + }; + + applyHttpOptionsCustom(Issuer); + let issuer: Issuer<Client>; + try { + issuer = await Issuer.discover(config.issuerBaseURL); + } catch (e) { + throw new DiscoveryError(e, config.issuerBaseURL); + } + applyHttpOptionsCustom(issuer); + + const issuerTokenAlgs = Array.isArray(issuer.id_token_signing_alg_values_supported) + ? issuer.id_token_signing_alg_values_supported + : []; + if (!issuerTokenAlgs.includes(config.idTokenSigningAlg)) { + debug( + 'ID token algorithm %o is not supported by the issuer. Supported ID token algorithms are: %o.', + config.idTokenSigningAlg, + issuerTokenAlgs + ); + } + + const configRespType = sortSpaceDelimitedString(config.authorizationParams.response_type); + const issuerRespTypes = Array.isArray(issuer.response_types_supported) ? issuer.response_types_supported : []; + issuerRespTypes.map(sortSpaceDelimitedString); + if (!issuerRespTypes.includes(configRespType)) { + debug( + 'Response type %o is not supported by the issuer. Supported response types are: %o.', + configRespType, + issuerRespTypes + ); + } + + const configRespMode = config.authorizationParams.response_mode; + const issuerRespModes = Array.isArray(issuer.response_modes_supported) ? issuer.response_modes_supported : []; + if (configRespMode && !issuerRespModes.includes(configRespMode)) { + debug( + 'Response mode %o is not supported by the issuer. Supported response modes are %o.', + configRespMode, + issuerRespModes + ); + } + + let jwks; + if (config.clientAssertionSigningKey) { + const privateKey = createPrivateKey({ key: config.clientAssertionSigningKey as string }); + const jwk = await exportJWK(privateKey); + jwks = { keys: [jwk] }; + } + + this.client = new issuer.Client( + { + client_id: config.clientID, + client_secret: config.clientSecret, + id_token_signed_response_alg: config.idTokenSigningAlg, + token_endpoint_auth_method: config.clientAuthMethod as ClientAuthMethod, + token_endpoint_auth_signing_alg: config.clientAssertionSigningAlg + }, + jwks + ); + applyHttpOptionsCustom(this.client); + + this.client[custom.clock_tolerance] = config.clockTolerance; + const issuerUrl = new URL(issuer.metadata.issuer); + + if (config.idpLogout) { + if ( + this.config.idpLogout && + (this.config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && this.config.auth0Logout !== false)) + ) { + Object.defineProperty(this.client, 'endSessionUrl', { + value(params: EndSessionParameters) { + const { id_token_hint, post_logout_redirect_uri, ...extraParams } = params; + const parsedUrl = new URL(urlJoin(issuer.metadata.issuer, '/v2/logout')); + parsedUrl.searchParams.set('client_id', config.clientID); + post_logout_redirect_uri && parsedUrl.searchParams.set('returnTo', post_logout_redirect_uri); + Object.entries(extraParams).forEach(([key, value]) => { + if (value === null || value === undefined) { + return; + } + parsedUrl.searchParams.set(key, value as string); + }); + return parsedUrl.toString(); + } + }); + } else if (!issuer.end_session_endpoint) { + debug('the issuer does not support RP-Initiated Logout'); + } + } + + return this.client; } async authorizationUrl(parameters: Record<string, unknown>): Promise<string> { - return this.client.authorizationUrl(parameters); + const client = await this.getClient(); + return client.authorizationUrl(parameters); } async callbackParams(req: Auth0Request) { - const obj: CallbackParamsType = this.client.callbackParams({ + const client = await this.getClient(); + const obj: CallbackParamsType = client.callbackParams({ method: req.getMethod(), url: req.getUrl(), body: await req.getBody() @@ -57,8 +183,9 @@ export class NodeClient extends AbstractClient { extras: CallbackExtras ): Promise<TokenEndpointResponse> { const params = Object.fromEntries(parameters.entries()); + const client = await this.getClient(); try { - return await this.client.callback(redirectUri, params, checks, extras); + return await client.callback(redirectUri, params, checks, extras); } catch (err) { if (err instanceof errors.OPError) { throw new IdentityProviderError(err); @@ -72,20 +199,23 @@ export class NodeClient extends AbstractClient { } async endSessionUrl(parameters: EndSessionParameters): Promise<string> { - return this.client.endSessionUrl(parameters); + const client = await this.getClient(); + return client.endSessionUrl(parameters); } async userinfo(accessToken: string): Promise<Record<string, unknown>> { + const client = await this.getClient(); try { - return await this.client.userinfo(accessToken); + return await client.userinfo(accessToken); } catch (e) { throw new UserInfoError(e.message); } } async refresh(refreshToken: string, extras: { exchangeBody: Record<string, any> }): Promise<TokenEndpointResponse> { + const client = await this.getClient(); try { - return await this.client.refresh(refreshToken, extras); + return await client.refresh(refreshToken, extras); } catch (e) { throw new AccessTokenError( AccessTokenErrorCode.FAILED_REFRESH_GRANT, @@ -112,120 +242,7 @@ export const clientGetter = (telemetry: Telemetry): ((config: Config) => Promise let client: NodeClient; return async (config) => { if (!client) { - const { name, version } = telemetry; - - const defaultHttpOptions: CustomHttpOptionsProvider = (_url, options) => ({ - ...options, - headers: { - ...options.headers, - 'User-Agent': `${name}/${version}`, - ...(config.enableTelemetry - ? { - 'Auth0-Client': Buffer.from( - JSON.stringify({ - name, - version, - env: { - node: process.version - } - }) - ).toString('base64') - } - : undefined) - }, - timeout: config.httpTimeout, - agent: config.httpAgent - }); - const applyHttpOptionsCustom = (entity: Issuer<Client> | typeof Issuer | Client) => { - entity[custom.http_options] = defaultHttpOptions; - }; - - applyHttpOptionsCustom(Issuer); - let issuer: Issuer<Client>; - try { - issuer = await Issuer.discover(config.issuerBaseURL); - } catch (e) { - throw new DiscoveryError(e, config.issuerBaseURL); - } - applyHttpOptionsCustom(issuer); - - const issuerTokenAlgs = Array.isArray(issuer.id_token_signing_alg_values_supported) - ? issuer.id_token_signing_alg_values_supported - : []; - if (!issuerTokenAlgs.includes(config.idTokenSigningAlg)) { - debug( - 'ID token algorithm %o is not supported by the issuer. Supported ID token algorithms are: %o.', - config.idTokenSigningAlg, - issuerTokenAlgs - ); - } - - const configRespType = sortSpaceDelimitedString(config.authorizationParams.response_type); - const issuerRespTypes = Array.isArray(issuer.response_types_supported) ? issuer.response_types_supported : []; - issuerRespTypes.map(sortSpaceDelimitedString); - if (!issuerRespTypes.includes(configRespType)) { - debug( - 'Response type %o is not supported by the issuer. Supported response types are: %o.', - configRespType, - issuerRespTypes - ); - } - - const configRespMode = config.authorizationParams.response_mode; - const issuerRespModes = Array.isArray(issuer.response_modes_supported) ? issuer.response_modes_supported : []; - if (configRespMode && !issuerRespModes.includes(configRespMode)) { - debug( - 'Response mode %o is not supported by the issuer. Supported response modes are %o.', - configRespMode, - issuerRespModes - ); - } - - let jwks; - if (config.clientAssertionSigningKey) { - const privateKey = createPrivateKey({ key: config.clientAssertionSigningKey as string }); - const jwk = await exportJWK(privateKey); - jwks = { keys: [jwk] }; - } - - const oidcClient = new issuer.Client( - { - client_id: config.clientID, - client_secret: config.clientSecret, - id_token_signed_response_alg: config.idTokenSigningAlg, - token_endpoint_auth_method: config.clientAuthMethod as ClientAuthMethod, - token_endpoint_auth_signing_alg: config.clientAssertionSigningAlg - }, - jwks - ); - applyHttpOptionsCustom(oidcClient); - - oidcClient[custom.clock_tolerance] = config.clockTolerance; - const issuerUrl = new URL(issuer.metadata.issuer); - - if (config.idpLogout) { - if (config.auth0Logout || (issuerUrl.hostname.match('\\.auth0\\.com$') && config.auth0Logout !== false)) { - Object.defineProperty(oidcClient, 'endSessionUrl', { - value(params: EndSessionParameters) { - const { id_token_hint, post_logout_redirect_uri, ...extraParams } = params; - const parsedUrl = new URL(urlJoin(issuer.metadata.issuer, '/v2/logout')); - parsedUrl.searchParams.set('client_id', config.clientID); - post_logout_redirect_uri && parsedUrl.searchParams.set('returnTo', post_logout_redirect_uri); - Object.entries(extraParams).forEach(([key, value]) => { - if (value === null || value === undefined) { - return; - } - parsedUrl.searchParams.set(key, value as string); - }); - return parsedUrl.toString(); - } - }); - } else if (!issuer.end_session_endpoint) { - debug('the issuer does not support RP-Initiated Logout'); - } - } - - client = new NodeClient(oidcClient); + client = new NodeClient(config, telemetry); } return client; }; diff --git a/tests/auth0-session/client/edge-client.test.ts b/tests/auth0-session/client/edge-client.test.ts index 5f37bc7a1..48b7805c9 100644 --- a/tests/auth0-session/client/edge-client.test.ts +++ b/tests/auth0-session/client/edge-client.test.ts @@ -8,7 +8,7 @@ import { jwks, makeIdToken } from '../fixtures/cert'; import pkg from '../../../package.json'; import wellKnown from '../fixtures/well-known.json'; import version from '../../../src/version'; -import { EdgeClient, clientGetter } from '../../../src/auth0-session/client/edge-client'; +import { EdgeClient } from '../../../src/auth0-session/client/edge-client'; import { mockFetch } from '../../fixtures/app-router-helpers'; import { Auth0Request } from '../../../src/auth0-session/http'; import { readFileSync } from 'fs'; @@ -50,14 +50,14 @@ const defaultConfig: ConfigParameters = { }; const getClient = async (params: ConfigParameters = {}): Promise<EdgeClient> => { - return clientGetter({ + return new EdgeClient(getConfig({ ...defaultConfig, ...params }), { name: 'nextjs-auth0', version - })(getConfig({ ...defaultConfig, ...params })); + }); }; describe('edge client', function () { - const headersSpy = jest.fn(); + let headersSpy = jest.fn(); beforeEach(() => { mockFetch(); @@ -147,7 +147,7 @@ describe('edge client', function () { idTokenSigningAlg: 'RS256' }); // @ts-ignore - expect(client.client.id_token_signed_response_alg).toEqual('RS256'); + expect((await client.getClient())[1].id_token_signed_response_alg).toEqual('RS256'); }); it('should use discovered logout endpoint by default', async function () { @@ -264,10 +264,14 @@ describe('edge client', function () { ); await expect( - getClient({ - issuerBaseURL: 'https://op2.example.com', - idpLogout: true - }) + ( + await getClient({ + issuerBaseURL: 'https://op2.example.com', + idpLogout: true + }) + ) + // @ts-ignore + .getClient() ).resolves.not.toThrow(); }); @@ -275,7 +279,9 @@ describe('edge client', function () { nock.cleanAll(); nock('https://op.example.com').get('/.well-known/oauth-authorization-server').reply(500); nock('https://op.example.com').get('/.well-known/openid-configuration').reply(500); - await expect(getClient()).rejects.toThrow(/Discovery requests failing for https:\/\/op.example.com/); + await expect((await getClient()).userinfo('token')).rejects.toThrow( + /Discovery requests failing for https:\/\/op.example.com/ + ); }); it('should throw UserInfoError when userinfo fails', async () => { diff --git a/tests/auth0-session/client/node-client.test.ts b/tests/auth0-session/client/node-client.test.ts index 1ba87dbfb..1aa983d51 100644 --- a/tests/auth0-session/client/node-client.test.ts +++ b/tests/auth0-session/client/node-client.test.ts @@ -4,7 +4,7 @@ import { jwks } from '../fixtures/cert'; import pkg from '../../../package.json'; import wellKnown from '../fixtures/well-known.json'; import version from '../../../src/version'; -import { NodeClient, clientGetter } from '../../../src/auth0-session/client/node-client'; +import { NodeClient } from '../../../src/auth0-session/client/node-client'; import { UserInfoError } from '../../../src/auth0-session/utils/errors'; const defaultConfig = { @@ -19,10 +19,10 @@ const defaultConfig = { }; const getClient = async (params: ConfigParameters = {}): Promise<NodeClient> => { - return clientGetter({ + return new NodeClient(getConfig({ ...defaultConfig, ...params }), { name: 'nextjs-auth0', version - })(getConfig({ ...defaultConfig, ...params })); + }); }; describe('node client', function () { @@ -69,15 +69,6 @@ describe('node client', function () { expect(headerProps).not.toContain('auth0-client'); }); - it('should accept lazy config', async function () { - expect(() => - clientGetter({ - name: 'nextjs-auth0', - version - }) - ).not.toThrow(); - }); - it('should not strip new headers', async function () { const client = await getClient(); const response = await client.userinfo('__test_token__'); @@ -101,7 +92,7 @@ describe('node client', function () { idTokenSigningAlg: 'RS256' }); // @ts-ignore - expect((await client.client).id_token_signed_response_alg).toEqual('RS256'); + expect((await client.getClient()).id_token_signed_response_alg).toEqual('RS256'); }); it('should use discovered logout endpoint by default', async function () { @@ -203,10 +194,14 @@ describe('node client', function () { ); await expect( - getClient({ - issuerBaseURL: 'https://op2.example.com', - idpLogout: true - }) + ( + await getClient({ + issuerBaseURL: 'https://op2.example.com', + idpLogout: true + }) + ) + // @ts-ignore + .getClient() ).resolves.not.toThrow(); }); @@ -214,7 +209,7 @@ describe('node client', function () { nock.cleanAll(); nock('https://op.example.com').get('/.well-known/oauth-authorization-server').reply(500); nock('https://op.example.com').get('/.well-known/openid-configuration').reply(500); - await expect(getClient).rejects.toThrow( + await expect((await getClient()).userinfo('token')).rejects.toThrow( 'Discovery requests failing for https://op.example.com, expected 200 OK, got: 500 Internal Server Error' ); }); From 9d91b5897b1b5ad46736cf744bf483caa689d4f0 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath <adamjmcgrath@gmail.com> Date: Mon, 13 Nov 2023 09:23:01 +0000 Subject: [PATCH 6/7] revert experimental version --- package-lock.json | 4 ++-- package.json | 2 +- src/version.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f8bcc1e9..3e1d6df60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@auth0/nextjs-auth0", - "version": "3.2.0-experimental-lazy-conf.0", + "version": "3.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@auth0/nextjs-auth0", - "version": "3.2.0-experimental-lazy-conf.0", + "version": "3.2.0", "license": "MIT", "dependencies": { "@panva/hkdf": "^1.0.2", diff --git a/package.json b/package.json index 4384a04fb..508057c41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@auth0/nextjs-auth0", - "version": "3.2.0-experimental-lazy-conf.0", + "version": "3.2.0", "description": "Next.js SDK for signing in with Auth0", "exports": { ".": "./dist/index.js", diff --git a/src/version.ts b/src/version.ts index d1d7a0067..2e09da8c6 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export default '3.2.0-experimental-lazy-conf.0'; +export default '3.2.0'; From ed41013cd71085500f83cbc59c6a8149193219b5 Mon Sep 17 00:00:00 2001 From: Adam Mcgrath <adamjmcgrath@gmail.com> Date: Mon, 13 Nov 2023 13:02:43 +0000 Subject: [PATCH 7/7] Remove old TODO --- src/session/get-access-token.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/session/get-access-token.ts b/src/session/get-access-token.ts index c4834b4f9..ac9a88454 100644 --- a/src/session/get-access-token.ts +++ b/src/session/get-access-token.ts @@ -236,7 +236,6 @@ export default function accessTokenFactory( return async (reqOrOpts?, res?, accessTokenRequest?): Promise<GetAccessTokenResult> => { const options = (res ? accessTokenRequest : reqOrOpts) as AccessTokenRequest | undefined; const req = (res ? reqOrOpts : undefined) as IncomingMessage | NextApiRequest | undefined; - // TODO: clean up const config = await getConfig(req ? getAuth0ReqRes(req, res as any)[0] : new Auth0NextRequestCookies()); const client = await getClient(config);