From db9c4af8f2a22ddca4f601f5eecbd00f7f3cfb3a Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 10:47:53 +0200 Subject: [PATCH 01/12] feat(auth0-fastify): add InvalidConfigurationError --- packages/auth0-fastify/src/errors/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/auth0-fastify/src/errors/index.ts b/packages/auth0-fastify/src/errors/index.ts index 03d49c1..a9acc42 100644 --- a/packages/auth0-fastify/src/errors/index.ts +++ b/packages/auth0-fastify/src/errors/index.ts @@ -6,3 +6,12 @@ export class MissingStoreOptionsError extends Error { this.name = 'MissingStoreOptionsError'; } } + +export class InvalidConfigurationError extends Error { + public code: string = 'invalid_configuration_error'; + + constructor(message?: string) { + super(message ?? 'The provided configuration is invalid.'); + this.name = 'InvalidConfigurationError'; + } +} From 9f7cfe3bd4adec02ed815e92bc7f656f0f259108 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 10:51:26 +0200 Subject: [PATCH 02/12] feat(auth0-fastify): add app-base-url module with static and dynamic resolution --- .../auth0-fastify/src/app-base-url.spec.ts | 48 ++++++++++ packages/auth0-fastify/src/app-base-url.ts | 88 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 packages/auth0-fastify/src/app-base-url.spec.ts create mode 100644 packages/auth0-fastify/src/app-base-url.ts diff --git a/packages/auth0-fastify/src/app-base-url.spec.ts b/packages/auth0-fastify/src/app-base-url.spec.ts new file mode 100644 index 0000000..f0a7e20 --- /dev/null +++ b/packages/auth0-fastify/src/app-base-url.spec.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from 'vitest'; +import { InvalidConfigurationError } from './errors/index.js'; +import { normalizeAppBaseUrl, resolveAppBaseUrl } from './app-base-url.js'; + +// Minimal stand-in for the parts of FastifyRequest the module reads. +function fakeRequest(host: string | undefined, protocol: string) { + return { host, protocol } as unknown as Parameters[1]; +} + +describe('normalizeAppBaseUrl', () => { + test('string returns static mode', () => { + expect(normalizeAppBaseUrl('https://app.example.com')).toEqual({ + mode: 'static', + value: 'https://app.example.com', + }); + }); + + test('undefined returns dynamic mode', () => { + expect(normalizeAppBaseUrl(undefined)).toEqual({ mode: 'dynamic' }); + }); + + test('non-absolute string throws', () => { + expect(() => normalizeAppBaseUrl('not-a-url')).toThrow(InvalidConfigurationError); + }); +}); + +describe('resolveAppBaseUrl (static + dynamic)', () => { + test('static returns configured value regardless of request', () => { + const config = normalizeAppBaseUrl('https://app.example.com'); + expect(resolveAppBaseUrl(config, fakeRequest('other.example.com', 'http'))).toBe( + 'https://app.example.com' + ); + }); + + test('dynamic infers from request host and protocol', () => { + const config = normalizeAppBaseUrl(undefined); + expect(resolveAppBaseUrl(config, fakeRequest('app.example.com', 'https'))).toBe( + 'https://app.example.com' + ); + }); + + test('dynamic throws when host is missing', () => { + const config = normalizeAppBaseUrl(undefined); + expect(() => resolveAppBaseUrl(config, fakeRequest(undefined, 'https'))).toThrow( + InvalidConfigurationError + ); + }); +}); diff --git a/packages/auth0-fastify/src/app-base-url.ts b/packages/auth0-fastify/src/app-base-url.ts new file mode 100644 index 0000000..d83e67c --- /dev/null +++ b/packages/auth0-fastify/src/app-base-url.ts @@ -0,0 +1,88 @@ +import type { + FastifyRequest, + RouteGenericInterface, + RawServerBase, + RawServerDefault, + RawRequestDefaultExpression, +} from 'fastify'; +import { InvalidConfigurationError } from './errors/index.js'; + +/** + * The normalized form of the `appBaseUrl` option. + * - `static`: a single fixed base URL. + * - `allowlist`: infer per request, but the inferred origin must be in `origins`. + * - `dynamic`: infer per request with no restriction. + */ +export type AppBaseUrlConfig = + | { mode: 'static'; value: string } + | { mode: 'allowlist'; origins: Set } + | { mode: 'dynamic' }; + +const assertAbsoluteUrl = (value: string): string => { + try { + new URL(value); + } catch { + throw new InvalidConfigurationError('appBaseUrl must be an absolute URL.'); + } + return value; +}; + +/** + * Validate the `appBaseUrl` option once at plugin init and reduce it to an + * `AppBaseUrlConfig` the per-request resolver can act on cheaply. + */ +export function normalizeAppBaseUrl(appBaseUrl: string | string[] | undefined): AppBaseUrlConfig { + if (typeof appBaseUrl === 'string') { + return { mode: 'static', value: assertAbsoluteUrl(appBaseUrl) }; + } + + if (Array.isArray(appBaseUrl)) { + if (appBaseUrl.length === 0) { + throw new InvalidConfigurationError('appBaseUrl must not be an empty array.'); + } + const origins = new Set(appBaseUrl.map((entry) => new URL(assertAbsoluteUrl(entry)).origin)); + return { mode: 'allowlist', origins }; + } + + return { mode: 'dynamic' }; +} + +const inferFromRequest = < + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression +>( + request: FastifyRequest +): string => { + const host = request.host; + if (!host) { + throw new InvalidConfigurationError('Unable to infer appBaseUrl: missing host.'); + } + return assertAbsoluteUrl(`${request.protocol}://${host}`); +}; + +/** + * Resolve the base URL for the current request based on the normalized config. + * Throws `InvalidConfigurationError` when inference fails or an inferred origin + * is not in the allow-list. + */ +export function resolveAppBaseUrl< + RawServer extends RawServerBase = RawServerDefault, + RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression +>( + config: AppBaseUrlConfig, + request: FastifyRequest +): string { + if (config.mode === 'static') { + return config.value; + } + + const inferred = inferFromRequest(request); + + if (config.mode === 'allowlist' && !config.origins.has(new URL(inferred).origin)) { + throw new InvalidConfigurationError( + `The inferred origin "${new URL(inferred).origin}" is not in the configured appBaseUrl allow-list.` + ); + } + + return inferred; +} From 69e69c3053bad89ce310c3fefab870abda922262 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 10:58:25 +0200 Subject: [PATCH 03/12] refactor(auth0-fastify): avoid double URL parse in allow-list check --- packages/auth0-fastify/src/app-base-url.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/auth0-fastify/src/app-base-url.ts b/packages/auth0-fastify/src/app-base-url.ts index d83e67c..04cca50 100644 --- a/packages/auth0-fastify/src/app-base-url.ts +++ b/packages/auth0-fastify/src/app-base-url.ts @@ -78,10 +78,13 @@ export function resolveAppBaseUrl< const inferred = inferFromRequest(request); - if (config.mode === 'allowlist' && !config.origins.has(new URL(inferred).origin)) { - throw new InvalidConfigurationError( - `The inferred origin "${new URL(inferred).origin}" is not in the configured appBaseUrl allow-list.` - ); + if (config.mode === 'allowlist') { + const origin = new URL(inferred).origin; + if (!config.origins.has(origin)) { + throw new InvalidConfigurationError( + `The inferred origin "${origin}" is not in the configured appBaseUrl allow-list.` + ); + } } return inferred; From 1d9c834ee6c989eef93871951bd4d48efba437fb Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 10:59:37 +0200 Subject: [PATCH 04/12] test(auth0-fastify): cover app-base-url allow-list resolution --- .../auth0-fastify/src/app-base-url.spec.ts | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/auth0-fastify/src/app-base-url.spec.ts b/packages/auth0-fastify/src/app-base-url.spec.ts index f0a7e20..83a940a 100644 --- a/packages/auth0-fastify/src/app-base-url.spec.ts +++ b/packages/auth0-fastify/src/app-base-url.spec.ts @@ -46,3 +46,38 @@ describe('resolveAppBaseUrl (static + dynamic)', () => { ); }); }); + +describe('normalizeAppBaseUrl (allow-list)', () => { + test('array returns allowlist mode with normalized origins', () => { + const config = normalizeAppBaseUrl(['https://a.example.com', 'https://b.example.com/ignored-path']); + expect(config.mode).toBe('allowlist'); + if (config.mode !== 'allowlist') throw new Error('expected allowlist'); + expect([...config.origins].sort()).toEqual(['https://a.example.com', 'https://b.example.com']); + }); + + test('empty array throws', () => { + expect(() => normalizeAppBaseUrl([])).toThrow(InvalidConfigurationError); + }); + + test('array with a non-absolute entry throws', () => { + expect(() => normalizeAppBaseUrl(['https://a.example.com', 'nope'])).toThrow( + InvalidConfigurationError + ); + }); +}); + +describe('resolveAppBaseUrl (allow-list)', () => { + test('returns inferred origin when it matches the allow-list', () => { + const config = normalizeAppBaseUrl(['https://a.example.com', 'https://b.example.com']); + expect(resolveAppBaseUrl(config, fakeRequest('b.example.com', 'https'))).toBe( + 'https://b.example.com' + ); + }); + + test('throws when inferred origin is not in the allow-list', () => { + const config = normalizeAppBaseUrl(['https://a.example.com']); + expect(() => resolveAppBaseUrl(config, fakeRequest('evil.example.com', 'https'))).toThrow( + InvalidConfigurationError + ); + }); +}); From 7dc14ddfad7ff563430425fc62669aa7ba2a8890 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 11:01:36 +0200 Subject: [PATCH 05/12] feat(auth0-fastify): enforce secure session cookies for dynamic appBaseUrl --- .../auth0-fastify/src/app-base-url.spec.ts | 31 ++++++++++++++++- packages/auth0-fastify/src/app-base-url.ts | 34 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/auth0-fastify/src/app-base-url.spec.ts b/packages/auth0-fastify/src/app-base-url.spec.ts index 83a940a..c0f2e20 100644 --- a/packages/auth0-fastify/src/app-base-url.spec.ts +++ b/packages/auth0-fastify/src/app-base-url.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; import { InvalidConfigurationError } from './errors/index.js'; -import { normalizeAppBaseUrl, resolveAppBaseUrl } from './app-base-url.js'; +import { normalizeAppBaseUrl, resolveAppBaseUrl, resolveSecureCookie } from './app-base-url.js'; // Minimal stand-in for the parts of FastifyRequest the module reads. function fakeRequest(host: string | undefined, protocol: string) { @@ -81,3 +81,32 @@ describe('resolveAppBaseUrl (allow-list)', () => { ); }); }); + +describe('resolveSecureCookie', () => { + const dynamic = { mode: 'dynamic' } as const; + const stat = { mode: 'static', value: 'https://app.example.com' } as const; + + test('dynamic + unset defaults to true', () => { + expect(resolveSecureCookie(dynamic, undefined, false)).toBe(true); + }); + + test('dynamic + explicit false in production throws', () => { + expect(() => resolveSecureCookie(dynamic, false, true)).toThrow(InvalidConfigurationError); + }); + + test('dynamic + explicit false outside production is honored', () => { + expect(resolveSecureCookie(dynamic, false, false)).toBe(false); + }); + + test('dynamic + explicit true is honored', () => { + expect(resolveSecureCookie(dynamic, true, true)).toBe(true); + }); + + test('static returns the configured value unchanged (undefined)', () => { + expect(resolveSecureCookie(stat, undefined, true)).toBeUndefined(); + }); + + test('static returns the configured value unchanged (false in production)', () => { + expect(resolveSecureCookie(stat, false, true)).toBe(false); + }); +}); diff --git a/packages/auth0-fastify/src/app-base-url.ts b/packages/auth0-fastify/src/app-base-url.ts index 04cca50..a125b95 100644 --- a/packages/auth0-fastify/src/app-base-url.ts +++ b/packages/auth0-fastify/src/app-base-url.ts @@ -89,3 +89,37 @@ export function resolveAppBaseUrl< return inferred; } + +/** + * Resolve the effective `secure` flag for the session cookie. + * + * When the base URL is dynamic or allow-listed there is no single static + * protocol to derive `secure` from, so we default it to `true` and forbid an + * explicit downgrade in production (anti-downgrade). When the base URL is a + * static string, the configured value passes through unchanged so the caller + * keeps the existing protocol-based default behavior. + * + * @param config The normalized appBaseUrl config. + * @param configuredSecure The `secure` value the app set, or `undefined` if unset. + * @param isProduction Whether the process is running in production. + */ +export function resolveSecureCookie( + config: AppBaseUrlConfig, + configuredSecure: boolean | undefined, + isProduction: boolean +): boolean | undefined { + if (config.mode === 'static') { + return configuredSecure; + } + + if (configuredSecure === false) { + if (isProduction) { + throw new InvalidConfigurationError( + 'Insecure session cookies (secure: false) are not allowed in production when appBaseUrl is dynamic or an allow-list.' + ); + } + return false; + } + + return true; +} From a78c6cc30699829741046fc8f9fb69f55550786d Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 11:28:25 +0200 Subject: [PATCH 06/12] feat(auth0-fastify): support dynamic and allow-list appBaseUrl in the plugin --- packages/auth0-fastify/src/index.ts | 128 ++++++++++++++-------------- 1 file changed, 62 insertions(+), 66 deletions(-) diff --git a/packages/auth0-fastify/src/index.ts b/packages/auth0-fastify/src/index.ts index 8d3b64a..f3a7767 100644 --- a/packages/auth0-fastify/src/index.ts +++ b/packages/auth0-fastify/src/index.ts @@ -13,6 +13,8 @@ import type { DomainResolver } from '@auth0/auth0-server-js'; import type { DiscoveryCacheOptions, SessionConfiguration, SessionStore, StoreOptions } from './types.js'; import { createRouteUrl, toSafeRedirect } from './utils.js'; import { FastifyCookieHandler } from './store/fastify-cookie-handler.js'; +import { normalizeAppBaseUrl, resolveAppBaseUrl, resolveSecureCookie } from './app-base-url.js'; +import { InvalidConfigurationError } from './errors/index.js'; export * from './types.js'; export type { DomainResolver } from '@auth0/auth0-server-js'; @@ -25,6 +27,7 @@ export type { } from '@auth0/auth0-server-js'; export { CookieTransactionStore } from '@auth0/auth0-server-js'; export { TokenExchangeError, MissingClientAuthError } from '@auth0/auth0-server-js'; +export { InvalidConfigurationError } from './errors/index.js'; declare module 'fastify' { /** @@ -49,38 +52,6 @@ declare module 'fastify' { } } -const assertAppBaseUrl = (value: string): string => { - try { - new URL(value); - } catch { - throw new Error('appBaseUrl must be an absolute URL.'); - } - return value; -}; - -const getHeaderValue = (value: string | string[] | undefined): string | undefined => - Array.isArray(value) ? value[0] : value; - -const inferAppBaseUrlFromRequest = < - RawServer extends RawServerBase = RawServerDefault, - RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression ->( - request: FastifyRequest -): string => { - const hostHeader = getHeaderValue(request.headers['x-forwarded-host']) ?? getHeaderValue(request.headers.host); - if (!hostHeader) { - throw new Error('Unable to infer appBaseUrl: missing host header.'); - } - - const forwardedProto = getHeaderValue(request.headers['x-forwarded-proto']); - const protocol = (forwardedProto ?? request.protocol).toString().split(',')[0]?.trim(); - if (!protocol) { - throw new Error('Unable to infer appBaseUrl: missing protocol.'); - } - - return assertAppBaseUrl(`${protocol}://${hostHeader}`); -}; - type Auth0FastifyCommonOptions< RawServer extends RawServerBase = RawServerDefault, RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, @@ -133,23 +104,22 @@ export type Auth0FastifyOptions< RawServer extends RawServerBase = RawServerDefault, RawRequest extends RawRequestDefaultExpression = RawRequestDefaultExpression, RawReply extends RawReplyDefaultExpression = RawReplyDefaultExpression -> = - | (Auth0FastifyCommonOptions & { - domain: string; - /** - * The base URL of the application used for redirects and callbacks. - * Required when using a static domain. - */ - appBaseUrl: string; - }) - | (Auth0FastifyCommonOptions & { - domain: DomainResolver>; - /** - * Optional base URL used for redirects and callbacks. - * When omitted, the base URL is inferred from the request host/proto. - */ - appBaseUrl?: string; - }); +> = Auth0FastifyCommonOptions & { + domain: string | DomainResolver>; + /** + * The base URL(s) of the application, used for redirects and callbacks. + * + * - `string`: a static base URL (existing behavior). + * - `string[]`: an allow-list. The base URL is inferred per request and must + * match one of these origins, otherwise the request is rejected with HTTP 500. + * - omitted: inferred per request from `request.host`/`request.protocol`. + * + * When omitted or an array and running behind a proxy, configure Fastify's + * `trustProxy` so `request.host`/`request.protocol` are derived from the + * forwarded headers your proxy sets. + */ + appBaseUrl?: string | string[]; +}; export default fp(async function auth0Fastify< RawServer extends RawServerBase = RawServerDefault, @@ -160,17 +130,34 @@ export default fp(async function auth0Fastify< options: Auth0FastifyOptions ) { const callbackPath = options.routes?.callback ?? '/auth/callback'; - const staticAppBaseUrl = options.appBaseUrl ? assertAppBaseUrl(options.appBaseUrl) : undefined; - if (typeof options.domain === 'string' && !staticAppBaseUrl) { - throw new Error('appBaseUrl is required when domain is a string.'); - } - const staticRedirectUri = staticAppBaseUrl ? createRouteUrl(callbackPath, staticAppBaseUrl) : undefined; - - const resolveAppBaseUrl = (request: FastifyRequest): string => { - if (staticAppBaseUrl) { - return staticAppBaseUrl; + const appBaseUrlConfig = normalizeAppBaseUrl(options.appBaseUrl); + const staticRedirectUri = + appBaseUrlConfig.mode === 'static' ? createRouteUrl(callbackPath, appBaseUrlConfig.value) : undefined; + + const isProduction = process.env.NODE_ENV === 'production'; + const resolvedSecure = resolveSecureCookie( + appBaseUrlConfig, + options.sessionConfiguration?.cookie?.secure, + isProduction + ); + + const resolveBaseUrl = (request: FastifyRequest): string => + resolveAppBaseUrl(appBaseUrlConfig, request); + + const resolveOr500 = ( + request: FastifyRequest, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reply: any + ): string | undefined => { + try { + return resolveBaseUrl(request); + } catch (e) { + if (e instanceof InvalidConfigurationError) { + reply.code(500).send(e.message); + return undefined; + } + throw e; } - return inferAppBaseUrlFromRequest(request); }; const auth0Client = new ServerClient>({ @@ -192,6 +179,7 @@ export default fp(async function auth0Fastify< ? new StatefulStateStore( { ...options.sessionConfiguration, + cookie: { ...options.sessionConfiguration?.cookie, secure: resolvedSecure }, secret: options.sessionSecret, store: options.sessionStore, }, @@ -200,6 +188,7 @@ export default fp(async function auth0Fastify< : new StatelessStateStore( { ...options.sessionConfiguration, + cookie: { ...options.sessionConfiguration?.cookie, secure: resolvedSecure }, secret: options.sessionSecret, }, new FastifyCookieHandler() @@ -227,7 +216,8 @@ export default fp(async function auth0Fastify< >, reply ) => { - const appBaseUrl = resolveAppBaseUrl(request); + const appBaseUrl = resolveOr500(request, reply); + if (!appBaseUrl) return; const dangerousReturnTo = request.query.returnTo; const sanitizedReturnTo = toSafeRedirect(dangerousReturnTo || '/', appBaseUrl); const redirectUri = createRouteUrl(callbackPath, appBaseUrl); @@ -248,7 +238,8 @@ export default fp(async function auth0Fastify< ); fastify.get(options.routes?.callback ?? '/auth/callback', async (request, reply) => { - const appBaseUrl = resolveAppBaseUrl(request); + const appBaseUrl = resolveOr500(request, reply); + if (!appBaseUrl) return; const { appState } = await auth0Client.completeInteractiveLogin<{ returnTo: string } | undefined>( createRouteUrl(request.url, appBaseUrl), { request, reply } @@ -258,7 +249,8 @@ export default fp(async function auth0Fastify< }); fastify.get(options.routes?.logout ?? '/auth/logout', async (request, reply) => { - const appBaseUrl = resolveAppBaseUrl(request); + const appBaseUrl = resolveOr500(request, reply); + if (!appBaseUrl) return; const logoutUrl = await auth0Client.logout({ returnTo: appBaseUrl.toString() }, { request, reply }); reply.redirect(logoutUrl.href); @@ -318,7 +310,8 @@ export default fp(async function auth0Fastify< }); } - const appBaseUrl = resolveAppBaseUrl(request); + const appBaseUrl = resolveOr500(request, reply); + if (!appBaseUrl) return; const sanitizedReturnTo = toSafeRedirect(dangerousReturnTo || '/', appBaseUrl); const callbackPath = options.routes?.connectCallback ?? '/auth/connect/callback'; const redirectUri = createRouteUrl(callbackPath, appBaseUrl); @@ -341,7 +334,8 @@ export default fp(async function auth0Fastify< ); fastify.get(options.routes?.connectCallback ?? '/auth/connect/callback', async (request, reply) => { - const appBaseUrl = resolveAppBaseUrl(request); + const appBaseUrl = resolveOr500(request, reply); + if (!appBaseUrl) return; const { appState } = await fastify.auth0Client!.completeLinkUser<{ returnTo: string }>( createRouteUrl(request.url, appBaseUrl), { @@ -375,7 +369,8 @@ export default fp(async function auth0Fastify< }); } - const appBaseUrl = resolveAppBaseUrl(request); + const appBaseUrl = resolveOr500(request, reply); + if (!appBaseUrl) return; const sanitizedReturnTo = toSafeRedirect(dangerousReturnTo || '/', appBaseUrl); const callbackPath = options.routes?.unconnectCallback ?? '/auth/unconnect/callback'; const redirectUri = createRouteUrl(callbackPath, appBaseUrl); @@ -397,7 +392,8 @@ export default fp(async function auth0Fastify< ); fastify.get(options.routes?.unconnectCallback ?? '/auth/unconnect/callback', async (request, reply) => { - const appBaseUrl = resolveAppBaseUrl(request); + const appBaseUrl = resolveOr500(request, reply); + if (!appBaseUrl) return; const { appState } = await fastify.auth0Client!.completeUnlinkUser<{ returnTo: string }>( createRouteUrl(request.url, appBaseUrl), { From af4af92a2c675004668ea88271e20cd4a7007867 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 11:57:41 +0200 Subject: [PATCH 07/12] test(auth0-fastify): cover dynamic, allow-list and secure-cookie appBaseUrl behavior --- packages/auth0-fastify/src/index.spec.ts | 94 +++++++++++++++++++----- 1 file changed, 74 insertions(+), 20 deletions(-) diff --git a/packages/auth0-fastify/src/index.spec.ts b/packages/auth0-fastify/src/index.spec.ts index 9092058..e1e0fea 100644 --- a/packages/auth0-fastify/src/index.spec.ts +++ b/packages/auth0-fastify/src/index.spec.ts @@ -3,7 +3,7 @@ import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; import { generateToken } from './test-utils/tokens.js'; import Fastify from 'fastify'; -import plugin from './index.js'; +import plugin, { InvalidConfigurationError } from './index.js'; import { StateData } from '@auth0/auth0-server-js'; import { decrypt, encrypt } from './test-utils/encryption.js'; @@ -118,7 +118,7 @@ test('auth/login redirects to authorize when not using a root appBaseUrl', async }); test('auth/login infers appBaseUrl from request when using a domain resolver', async () => { - const fastify = Fastify(); + const fastify = Fastify({ trustProxy: true }); fastify.register(plugin, { domain: async () => domain, clientId: '', @@ -142,7 +142,7 @@ test('auth/login infers appBaseUrl from request when using a domain resolver', a }); test('auth/login prefers forwarded host/proto when inferring appBaseUrl', async () => { - const fastify = Fastify(); + const fastify = Fastify({ trustProxy: true }); fastify.register(plugin, { domain: async () => domain, clientId: '', @@ -165,8 +165,8 @@ test('auth/login prefers forwarded host/proto when inferring appBaseUrl', async expect(url.searchParams.get('redirect_uri')).toBe('https://public.example.com/auth/callback'); }); -test('auth/login fails when appBaseUrl cannot be inferred', async () => { - const fastify = Fastify(); +test('auth/logout infers appBaseUrl from request when using a domain resolver', async () => { + const fastify = Fastify({ trustProxy: true }); fastify.register(plugin, { domain: async () => domain, clientId: '', @@ -176,20 +176,24 @@ test('auth/login fails when appBaseUrl cannot be inferred', async () => { const res = await fastify.inject({ method: 'GET', - url: '/auth/login', + url: '/auth/logout', headers: { - host: '', - 'x-forwarded-proto': '', + host: 'app.example.com', + 'x-forwarded-proto': 'https', }, }); + const url = new URL(res.headers['location']?.toString() ?? ''); - expect(res.statusCode).toBe(500); + expect(res.statusCode).toBe(302); + const returnTo = + url.searchParams.get('returnTo') ?? url.searchParams.get('post_logout_redirect_uri'); + expect(returnTo).toBe('https://app.example.com'); }); -test('auth/logout infers appBaseUrl from request when using a domain resolver', async () => { - const fastify = Fastify(); +test('infers appBaseUrl for a static domain when appBaseUrl is omitted', async () => { + const fastify = Fastify({ trustProxy: true }); fastify.register(plugin, { - domain: async () => domain, + domain: domain, clientId: '', clientSecret: '', sessionSecret: '', @@ -197,7 +201,7 @@ test('auth/logout infers appBaseUrl from request when using a domain resolver', const res = await fastify.inject({ method: 'GET', - url: '/auth/logout', + url: '/auth/login', headers: { host: 'app.example.com', 'x-forwarded-proto': 'https', @@ -206,22 +210,53 @@ test('auth/logout infers appBaseUrl from request when using a domain resolver', const url = new URL(res.headers['location']?.toString() ?? ''); expect(res.statusCode).toBe(302); - const returnTo = - url.searchParams.get('returnTo') ?? url.searchParams.get('post_logout_redirect_uri'); - expect(returnTo).toBe('https://app.example.com'); + expect(url.searchParams.get('redirect_uri')).toBe('https://app.example.com/auth/callback'); }); -test('requires appBaseUrl when using a static domain', async () => { - const fastify = Fastify(); +test('auth/login infers appBaseUrl from an allow-list when the origin matches', async () => { + const fastify = Fastify({ trustProxy: true }); fastify.register(plugin, { - // @ts-expect-error appBaseUrl required for static domain domain: domain, clientId: '', clientSecret: '', + appBaseUrl: ['https://app.example.com', 'https://other.example.com'], sessionSecret: '', }); - await expect(fastify.ready()).rejects.toThrowError('appBaseUrl is required when domain is a string.'); + const res = await fastify.inject({ + method: 'GET', + url: '/auth/login', + headers: { + host: 'other.example.com', + 'x-forwarded-proto': 'https', + }, + }); + const url = new URL(res.headers['location']?.toString() ?? ''); + + expect(res.statusCode).toBe(302); + expect(url.searchParams.get('redirect_uri')).toBe('https://other.example.com/auth/callback'); +}); + +test('auth/login returns 500 when the inferred origin is not in the allow-list', async () => { + const fastify = Fastify({ trustProxy: true }); + fastify.register(plugin, { + domain: domain, + clientId: '', + clientSecret: '', + appBaseUrl: ['https://app.example.com'], + sessionSecret: '', + }); + + const res = await fastify.inject({ + method: 'GET', + url: '/auth/login', + headers: { + host: 'evil.example.com', + 'x-forwarded-proto': 'https', + }, + }); + + expect(res.statusCode).toBe(500); }); test('auth/login should put the appState in the transaction store', async () => { @@ -1465,3 +1500,22 @@ test('customTokenExchange forwards the organization to the token endpoint', asyn expect(res.statusCode).toBe(200); expect(capturedOrganization).toBe('org_123'); }); + +test('rejects insecure session cookies in production when appBaseUrl is dynamic', async () => { + const previous = process.env.NODE_ENV; + process.env.NODE_ENV = 'production'; + try { + const fastify = Fastify({ trustProxy: true }); + fastify.register(plugin, { + domain: domain, + clientId: '', + clientSecret: '', + sessionSecret: '', + sessionConfiguration: { cookie: { secure: false } }, + }); + + await expect(fastify.ready()).rejects.toThrowError(InvalidConfigurationError); + } finally { + process.env.NODE_ENV = previous; + } +}); From 14e38540a5f007888de0681d8ac5afa20f0e6011 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 12:00:20 +0200 Subject: [PATCH 08/12] docs(auth0-fastify): document dynamic and allow-list appBaseUrl --- packages/auth0-fastify/EXAMPLES.md | 68 ++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/packages/auth0-fastify/EXAMPLES.md b/packages/auth0-fastify/EXAMPLES.md index ade2803..f1ce20f 100644 --- a/packages/auth0-fastify/EXAMPLES.md +++ b/packages/auth0-fastify/EXAMPLES.md @@ -11,6 +11,7 @@ - [Performing a delegation exchange without a session](#performing-a-delegation-exchange-without-a-session) - [Using actor tokens for delegation](#using-actor-tokens-for-delegation) - [Authenticating within an organization](#authenticating-within-an-organization) +- [Dynamic Application Base URL](#dynamic-application-base-url) - [Multiple Custom Domains (MCD)](#multiple-custom-domains-mcd) ## Configuration @@ -273,6 +274,66 @@ fastify.post('/custom-token-exchange', async (request, reply) => { }); ``` +## Dynamic Application Base URL + +`appBaseUrl` controls the origin used to build `redirect_uri` and +`post_logout_redirect_uri`. It accepts three forms: + +- **Static string** — a single fixed base URL (the common case): + + ```ts + fastify.register(fastifyAuth0, { + domain: '', + clientId: '', + clientSecret: '', + sessionSecret: '', + appBaseUrl: 'https://my-app.com', + }); + ``` + +- **Allow-list (array)** — the base URL is inferred per request from + `request.host`/`request.protocol`, but the inferred origin must match one of + the listed origins, otherwise the request is rejected with HTTP 500: + + ```ts + fastify.register(fastifyAuth0, { + domain: '', + clientId: '', + clientSecret: '', + sessionSecret: '', + appBaseUrl: ['https://brand-1.my-app.com', 'https://brand-2.my-app.com'], + }); + ``` + +- **Omitted** — the base URL is inferred per request with no restriction. Make + sure every inferred origin is registered in Auth0 as an `Allowed Callback URL` + and `Allowed Logout URL`. + +Dynamic and allow-list modes work with both a static string `domain` and a +`DomainResolver` (see [Multiple Custom Domains](#multiple-custom-domains-mcd)). + +### Trusting forwarded headers + +When `appBaseUrl` is omitted or an array, the base URL is derived from Fastify's +`request.host` and `request.protocol`. Behind a proxy, enable Fastify's +[`trustProxy`](https://fastify.dev/docs/latest/Reference/Server/#trustproxy) so +these reflect the `X-Forwarded-Host`/`X-Forwarded-Proto` headers your proxy +sets: + +```ts +const fastify = Fastify({ trustProxy: true }); +``` + +Your proxy must sanitize and overwrite `Host` and `X-Forwarded-Host` before they +reach the app. Without a trusted proxy validating these headers, an attacker can +influence the inferred base URL and cause malicious redirects. + +### Secure session cookies + +In dynamic and allow-list modes, session cookies default to `secure: true`. +Setting `secure: false` while `NODE_ENV === 'production'` throws +`InvalidConfigurationError` to prevent an accidental downgrade. + ## Multiple Custom Domains (MCD) `Multiple Custom Domains` (MCD) lets you resolve the Auth0 domain per request while using a single Fastify plugin instance. This is useful when one application serves multiple customer domains (for example, `brand-1.my-app.com` and `brand-2.my-app.com`), each mapped to a different `Auth0` custom domain. @@ -335,10 +396,9 @@ const domainResolver: DomainResolver = (storeOptions) => { Resolver mode means `domain` is configured as a resolver function. The plugin then passes per-request `storeOptions` into the underlying `ServerClient` so it can choose the correct `Auth0` domain for the current request. - When you use the mounted routes, `{ request, reply }` is passed automatically. - If you call `fastify.auth0Client` directly from your own routes, continue to pass `{ request, reply }` to those methods. -- If `appBaseUrl` is provided, that static value is used for callback and logout URLs. -- If `appBaseUrl` is omitted, the SDK infers the base URL from request headers. - -If you omit `appBaseUrl`, make sure every inferred origin is registered in Auth0 as an `Allowed Callback URL` and `Allowed Logout URL`. +- `appBaseUrl` behaves the same as documented in + [Dynamic Application Base URL](#dynamic-application-base-url): provide a static + string, an allow-list array, or omit it to infer per request. From 6e723b53f6c76a3c63b3c00531a9d6839c816a4c Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 13:40:21 +0200 Subject: [PATCH 09/12] docs(examples): add dynamic application base URL example app --- .../example-fastify-web-dynamic/.env.example | 11 ++ .../example-fastify-web-dynamic/README.md | 98 ++++++++++++++ .../example-fastify-web-dynamic/package.json | 24 ++++ .../public/img/auth0.png | Bin 0 -> 9142 bytes .../example-fastify-web-dynamic/src/index.ts | 125 ++++++++++++++++++ .../example-fastify-web-dynamic/tsconfig.json | 31 +++++ .../views/index.ejs | 15 +++ .../views/layout.ejs | 63 +++++++++ .../views/private.ejs | 5 + .../views/public.ejs | 5 + 10 files changed, 377 insertions(+) create mode 100644 examples/example-fastify-web-dynamic/.env.example create mode 100644 examples/example-fastify-web-dynamic/README.md create mode 100644 examples/example-fastify-web-dynamic/package.json create mode 100644 examples/example-fastify-web-dynamic/public/img/auth0.png create mode 100644 examples/example-fastify-web-dynamic/src/index.ts create mode 100644 examples/example-fastify-web-dynamic/tsconfig.json create mode 100644 examples/example-fastify-web-dynamic/views/index.ejs create mode 100644 examples/example-fastify-web-dynamic/views/layout.ejs create mode 100644 examples/example-fastify-web-dynamic/views/private.ejs create mode 100644 examples/example-fastify-web-dynamic/views/public.ejs diff --git a/examples/example-fastify-web-dynamic/.env.example b/examples/example-fastify-web-dynamic/.env.example new file mode 100644 index 0000000..eb62901 --- /dev/null +++ b/examples/example-fastify-web-dynamic/.env.example @@ -0,0 +1,11 @@ +AUTH0_DOMAIN= +AUTH0_CLIENT_ID= +AUTH0_CLIENT_SECRET= +AUTH0_SESSION_SECRET= + +# APP_BASE_URL selects the mode this example runs in: +# - Leave it unset/empty for DYNAMIC mode (base URL inferred from every request). +# - A single URL for STATIC mode, e.g. APP_BASE_URL=http://localhost:3000 +# - Comma-separated URLs for ALLOW-LIST mode, e.g. +# APP_BASE_URL=https://brand-1.localtest.me:3000,https://brand-2.localtest.me:3000 +APP_BASE_URL= diff --git a/examples/example-fastify-web-dynamic/README.md b/examples/example-fastify-web-dynamic/README.md new file mode 100644 index 0000000..56d62be --- /dev/null +++ b/examples/example-fastify-web-dynamic/README.md @@ -0,0 +1,98 @@ +# Fastify Dynamic Application Base URL Example + +This example demonstrates how to use `auth0-fastify` with a **dynamic application +base URL** — where the URL used to build `redirect_uri` and +`post_logout_redirect_uri` is resolved per request instead of being hard-coded. + +This is useful when a single application serves multiple hosts (for example +`brand-1.my-app.com` and `brand-2.my-app.com`) behind a proxy. + +For a classic single-host setup, see the +[`example-fastify-web`](../example-fastify-web) example instead. + +## How it works + +Two pieces make this work: + +1. The Fastify server is created with `trustProxy: true`, so Fastify derives + `request.host` / `request.protocol` from the `X-Forwarded-Host` / + `X-Forwarded-Proto` headers your proxy sets. +2. `appBaseUrl` is **not** a single hard-coded string. The SDK infers the base + URL from those request accessors. + +> [!IMPORTANT] +> When inferring the base URL, your proxy **must** sanitize and overwrite the +> `Host` and `X-Forwarded-Host` headers before they reach the app. Without a +> trusted proxy validating these headers, an attacker can influence the inferred +> base URL and cause malicious redirects. + +## Install dependencies + +```bash +npm install +``` + +## Configuration + +Rename `.env.example` to `.env` and configure your Auth0 application: + +```ts +AUTH0_DOMAIN=YOUR_AUTH0_DOMAIN +AUTH0_CLIENT_ID=YOUR_AUTH0_CLIENT_ID +AUTH0_CLIENT_SECRET=YOUR_AUTH0_CLIENT_SECRET +AUTH0_SESSION_SECRET=YOUR_AUTH0_SESSION_SECRET +APP_BASE_URL= +``` + +Generate a session secret with `openssl`: + +```shell +openssl rand -hex 64 +``` + +`APP_BASE_URL` selects which mode the example runs in: + +| `APP_BASE_URL` value | Mode | Behavior | +|----------------------|------|----------| +| unset / empty | **Dynamic** | Base URL inferred from every request, with no restriction. | +| a single URL | **Static** | A fixed base URL (same as the classic example). | +| comma-separated URLs | **Allow-list** | Base URL inferred per request, but the origin must match one of the listed values, otherwise the request is rejected with HTTP 500. | + +Whichever inferred origins you use, register each one in Auth0 as an +**Allowed Callback URL** and **Allowed Logout URL**. + +## Run + +```bash +npm run start +``` + +The application has 3 routes: + +- `/`: The home route. It shows the **resolved request origin** so you can see + what the SDK infers for the current request. +- `/public`: A public route that can be accessed without authentication. +- `/private`: A private route that can only be accessed by authenticated users. + +## Trying out dynamic inference locally + +Because inference depends on the host/proto of each request, you need to send +different forwarded headers to see it change. A quick way is `curl` (note that +the SDK honors these headers only because the server enables `trustProxy`): + +```bash +# Inferred origin = https://brand-1.example.com +curl -s -H "X-Forwarded-Host: brand-1.example.com" \ + -H "X-Forwarded-Proto: https" \ + http://localhost:3000/ + +# Inferred origin = https://brand-2.example.com +curl -s -H "X-Forwarded-Host: brand-2.example.com" \ + -H "X-Forwarded-Proto: https" \ + http://localhost:3000/ +``` + +For a full login round-trip across hosts, put a real reverse proxy (nginx, +Caddy, etc.) in front of the app and have it set `X-Forwarded-Host` / +`X-Forwarded-Proto` per virtual host. Hostnames such as `*.localtest.me` +(which resolve to `127.0.0.1`) are handy for local multi-host testing. diff --git a/examples/example-fastify-web-dynamic/package.json b/examples/example-fastify-web-dynamic/package.json new file mode 100644 index 0000000..a929105 --- /dev/null +++ b/examples/example-fastify-web-dynamic/package.json @@ -0,0 +1,24 @@ +{ + "name": "example-fastify-web-dynamic", + "version": "1.0.0", + "description": "", + "type": "module", + "scripts": { + "start": "tsx src/index.ts --project tsconfig.json", + "build": "tsc --project tsconfig.json" + }, + "devDependencies": { + "@types/ejs": "^3.1.5", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "typescript": "~5.8.2" + }, + "dependencies": { + "@auth0/auth0-fastify": "*", + "@fastify/static": "^8.1.1", + "@fastify/view": "^10.0.2", + "dotenv": "^16.4.7", + "ejs": "^3.1.10", + "fastify": "^5.2.1" + } +} diff --git a/examples/example-fastify-web-dynamic/public/img/auth0.png b/examples/example-fastify-web-dynamic/public/img/auth0.png new file mode 100644 index 0000000000000000000000000000000000000000..c5c260f3c0e0f4a871b3300399e77eee0625c6b4 GIT binary patch literal 9142 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 literal 0 HcmV?d00001 diff --git a/examples/example-fastify-web-dynamic/src/index.ts b/examples/example-fastify-web-dynamic/src/index.ts new file mode 100644 index 0000000..ecdd1b8 --- /dev/null +++ b/examples/example-fastify-web-dynamic/src/index.ts @@ -0,0 +1,125 @@ +import Fastify, { FastifyReply, FastifyRequest } from 'fastify'; +import fastifyStatic from '@fastify/static'; +import fastifyView from '@fastify/view'; +import fastifyAuth0 from '@auth0/auth0-fastify'; +import ejs from 'ejs'; +import 'dotenv/config'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// `trustProxy: true` makes Fastify derive `request.host` / `request.protocol` +// from the `X-Forwarded-Host` / `X-Forwarded-Proto` headers your proxy sets. +// The SDK relies on those accessors to infer the application base URL per +// request, so this is required for dynamic and allow-list modes behind a proxy. +const fastify = Fastify({ + logger: true, + trustProxy: true, +}); + +// Fix to use __dirname in ES modules +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +fastify.register(fastifyStatic, { + root: path.join(__dirname, '../public'), +}); + +fastify.register(fastifyView, { + engine: { + ejs: ejs, + }, + root: './views', + layout: 'layout.ejs', +}); + +// `APP_BASE_URL` drives which mode this example runs in: +// - unset/empty -> dynamic: the base URL is inferred from every request. +// - single URL -> static: a fixed base URL (classic single-host setup). +// - comma-separated URLs -> allow-list: inferred per request, but the origin must +// be one of the listed values or the request is rejected. +const parseAppBaseUrl = (value: string | undefined): string | string[] | undefined => { + if (!value) { + return undefined; + } + + const entries = value + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0); + + if (entries.length === 0) { + return undefined; + } + + return entries.length === 1 ? entries[0] : entries; +}; + +const appBaseUrl = parseAppBaseUrl(process.env.APP_BASE_URL); + +fastify.log.info( + { appBaseUrl: appBaseUrl ?? '(inferred per request)' }, + 'Configured appBaseUrl mode' +); + +fastify.register(fastifyAuth0, { + domain: process.env.AUTH0_DOMAIN as string, + clientId: process.env.AUTH0_CLIENT_ID as string, + clientSecret: process.env.AUTH0_CLIENT_SECRET as string, + appBaseUrl, + sessionSecret: process.env.AUTH0_SESSION_SECRET as string, +}); + +fastify.get('/', async (request, reply) => { + const user = await fastify.auth0Client!.getUser({ request, reply }); + + return reply.viewAsync('index.ejs', { + isLoggedIn: !!user, + user: user, + origin: `${request.protocol}://${request.host}`, + }); +}); + +async function hasSessionPreHandler(request: FastifyRequest, reply: FastifyReply) { + const session = await fastify.auth0Client!.getSession({ request, reply }); + + if (!session) { + reply.redirect(`/auth/login?returnTo=${request.url}`); + } +} + +fastify.get('/public', async (request, reply) => { + const user = await fastify.auth0Client!.getUser({ request, reply }); + + return reply.viewAsync('public.ejs', { + isLoggedIn: !!user, + user, + origin: `${request.protocol}://${request.host}`, + }); +}); + +fastify.get( + '/private', + { + preHandler: hasSessionPreHandler, + }, + async (request, reply) => { + const user = await fastify.auth0Client!.getUser({ request, reply }); + + return reply.viewAsync('private.ejs', { + isLoggedIn: !!user, + user, + origin: `${request.protocol}://${request.host}`, + }); + } +); + +const start = async () => { + try { + await fastify.listen({ port: 3000 }); + } catch (err) { + fastify.log.error(err); + process.exit(1); + } +}; + +start(); diff --git a/examples/example-fastify-web-dynamic/tsconfig.json b/examples/example-fastify-web-dynamic/tsconfig.json new file mode 100644 index 0000000..7173a2d --- /dev/null +++ b/examples/example-fastify-web-dynamic/tsconfig.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "incremental": false, + "isolatedModules": true, + "lib": [ + "es2022", + "DOM", + "DOM.Iterable" + ], + "module": "NodeNext", + "moduleDetection": "force", + "moduleResolution": "NodeNext", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "outDir": "dist", + "rootDir": "src" + }, + "ts-node": { + "esm": true, + "compilerOptions": { + "module": "nodenext", + } + } +} diff --git a/examples/example-fastify-web-dynamic/views/index.ejs b/examples/example-fastify-web-dynamic/views/index.ejs new file mode 100644 index 0000000..9ab9ec8 --- /dev/null +++ b/examples/example-fastify-web-dynamic/views/index.ejs @@ -0,0 +1,15 @@ +<% if(isLoggedIn){ %> <% if(locals.user){ %> +

Hello, <%= locals.user.name %>!

+<% } %> +<% } else{ %> +

You are not logged in.

+<% } %> + +

+ Resolved request origin: <%= locals.origin %> +

+

+ This is the value the SDK infers as the application base URL for this request + (from request.protocol and request.host). Change the + forwarded host/proto and it changes with the request. +

diff --git a/examples/example-fastify-web-dynamic/views/layout.ejs b/examples/example-fastify-web-dynamic/views/layout.ejs new file mode 100644 index 0000000..7d42191 --- /dev/null +++ b/examples/example-fastify-web-dynamic/views/layout.ejs @@ -0,0 +1,63 @@ + + + + + + Auth0-Fastify Web Application demo + + + + +
<%- body %>
+ + + diff --git a/examples/example-fastify-web-dynamic/views/private.ejs b/examples/example-fastify-web-dynamic/views/private.ejs new file mode 100644 index 0000000..b0e138a --- /dev/null +++ b/examples/example-fastify-web-dynamic/views/private.ejs @@ -0,0 +1,5 @@ +This is a private page. + +

+ Resolved request origin: <%= locals.origin %> +

diff --git a/examples/example-fastify-web-dynamic/views/public.ejs b/examples/example-fastify-web-dynamic/views/public.ejs new file mode 100644 index 0000000..a044eb4 --- /dev/null +++ b/examples/example-fastify-web-dynamic/views/public.ejs @@ -0,0 +1,5 @@ +This is a public page. + +

+ Resolved request origin: <%= locals.origin %> +

From da30d2b46b028d3d85014c7c4402c080a4984ea9 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 13:50:20 +0200 Subject: [PATCH 10/12] test(examples): add integration test for dynamic appBaseUrl example --- .../example-fastify-web-dynamic/package.json | 8 +- .../src/index.spec.ts | 91 +++++++++++++++++++ .../example-fastify-web-dynamic/src/index.ts | 9 +- 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 examples/example-fastify-web-dynamic/src/index.spec.ts diff --git a/examples/example-fastify-web-dynamic/package.json b/examples/example-fastify-web-dynamic/package.json index a929105..3ceffe8 100644 --- a/examples/example-fastify-web-dynamic/package.json +++ b/examples/example-fastify-web-dynamic/package.json @@ -5,13 +5,17 @@ "type": "module", "scripts": { "start": "tsx src/index.ts --project tsconfig.json", - "build": "tsc --project tsconfig.json" + "build": "tsc --project tsconfig.json", + "test": "vitest run", + "test:ci": "vitest run" }, "devDependencies": { "@types/ejs": "^3.1.5", + "msw": "^2.11.3", "ts-node": "^10.9.2", "tsx": "^4.19.2", - "typescript": "~5.8.2" + "typescript": "~5.8.2", + "vitest": "^3.0.5" }, "dependencies": { "@auth0/auth0-fastify": "*", diff --git a/examples/example-fastify-web-dynamic/src/index.spec.ts b/examples/example-fastify-web-dynamic/src/index.spec.ts new file mode 100644 index 0000000..804220d --- /dev/null +++ b/examples/example-fastify-web-dynamic/src/index.spec.ts @@ -0,0 +1,91 @@ +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; +import { setupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; +import type { FastifyInstance } from 'fastify'; + +// The app reads process.env at import time to build its appBaseUrl config and to +// register the plugin, so these must be set BEFORE importing it. We run the +// example in ALLOW-LIST mode: the base URL is inferred per request, but the +// inferred origin must be one of the listed values. +const DOMAIN = 'example.auth0.local'; + +// Under `fastify.inject` with `trustProxy` enabled, `request.host` comes from the +// `host` header and `request.protocol` defaults to `http`, so the inferred +// origin is `http://`. The allow-list entries match that form. +const BRAND_1_ORIGIN = 'http://brand-1.localhost:3000'; +const BRAND_2_ORIGIN = 'http://brand-2.localhost:3000'; + +process.env.AUTH0_DOMAIN = DOMAIN; +process.env.AUTH0_CLIENT_ID = ''; +process.env.AUTH0_CLIENT_SECRET = ''; +process.env.AUTH0_SESSION_SECRET = ''; +process.env.APP_BASE_URL = `${BRAND_1_ORIGIN},${BRAND_2_ORIGIN}`; + +// A minimal OIDC discovery document. /auth/login only needs the +// authorization_endpoint to build the 302, so that is all we mock. +const discoveryDocument = (domain: string) => ({ + issuer: `https://${domain}/`, + authorization_endpoint: `https://${domain}/authorize`, + token_endpoint: `https://${domain}/oauth/token`, + end_session_endpoint: `https://${domain}/logout`, +}); + +const server = setupServer( + http.get(`https://${DOMAIN}/.well-known/openid-configuration`, () => HttpResponse.json(discoveryDocument(DOMAIN))) +); + +let fastify: FastifyInstance; + +beforeAll(async () => { + server.listen({ onUnhandledRequest: 'bypass' }); + // Import after env is set so the module builds its appBaseUrl config correctly. + // The start() call is guarded, so importing does not bind a port. + ({ fastify } = await import('./index.js')); + await fastify.ready(); +}); + +afterEach(() => server.resetHandlers()); +afterAll(async () => { + server.close(); + await fastify.close(); +}); + +describe('example-fastify-web-dynamic: per-host base URL resolution', () => { + test('login from an allow-listed host infers that origin for the redirect_uri', async () => { + const res = await fastify.inject({ + method: 'GET', + url: '/auth/login', + headers: { host: 'brand-1.localhost:3000' }, + }); + const location = new URL(res.headers['location']?.toString() ?? ''); + + expect(res.statusCode).toBe(302); + expect(location.host).toBe(DOMAIN); + expect(location.pathname).toBe('/authorize'); + expect(location.searchParams.get('redirect_uri')).toBe(`${BRAND_1_ORIGIN}/auth/callback`); + }); + + test('login from a second allow-listed host infers that origin for the redirect_uri', async () => { + const res = await fastify.inject({ + method: 'GET', + url: '/auth/login', + headers: { host: 'brand-2.localhost:3000' }, + }); + const location = new URL(res.headers['location']?.toString() ?? ''); + + expect(res.statusCode).toBe(302); + expect(location.host).toBe(DOMAIN); + expect(location.pathname).toBe('/authorize'); + expect(location.searchParams.get('redirect_uri')).toBe(`${BRAND_2_ORIGIN}/auth/callback`); + }); + + test('login from a host not in the allow-list is rejected', async () => { + const res = await fastify.inject({ + method: 'GET', + url: '/auth/login', + headers: { host: 'evil.localhost:3000' }, + }); + + expect(res.statusCode).toBe(500); + }); +}); diff --git a/examples/example-fastify-web-dynamic/src/index.ts b/examples/example-fastify-web-dynamic/src/index.ts index ecdd1b8..6ea21ac 100644 --- a/examples/example-fastify-web-dynamic/src/index.ts +++ b/examples/example-fastify-web-dynamic/src/index.ts @@ -122,4 +122,11 @@ const start = async () => { } }; -start(); +// Start the server only when this file is run directly (e.g. `npm start`), not +// when it is imported (e.g. by the test suite, which drives `fastify` via +// `fastify.inject`). +if (process.argv[1] === __filename) { + start(); +} + +export { fastify }; From e877ebf82336c1d83576e2b97d40142414830e91 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 14:00:43 +0200 Subject: [PATCH 11/12] ci: build and test examples in a dedicated workflow --- .github/workflows/examples.yml | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/examples.yml diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml new file mode 100644 index 0000000..72ea6a2 --- /dev/null +++ b/.github/workflows/examples.yml @@ -0,0 +1,46 @@ +name: Build and Test Examples + +on: + merge_group: + workflow_dispatch: + pull_request: + branches: [main, early-access] + push: + branches: [main, early-access] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + example-fastify-web-dynamic: + name: Dynamic Application Base URL + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Setup Node.js with npm caching + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + package-manager-cache: false + + - name: Update npm + run: npm install -g npm@11.10.0 + + - name: Install dependencies + run: npm install + + - name: Build auth0-fastify + run: npm run build -w @auth0/auth0-fastify + + - name: Build example-fastify-web-dynamic + run: npm run build -w example-fastify-web-dynamic + + - name: Test example-fastify-web-dynamic + run: npm run test:ci -w example-fastify-web-dynamic From 15d04e52ebdeae1a09622462773d8e3354526bc9 Mon Sep 17 00:00:00 2001 From: Frederik Prijck Date: Fri, 19 Jun 2026 15:16:21 +0200 Subject: [PATCH 12/12] chore(examples): address PR review feedback - examples.yml: set persist-credentials: false on checkout - index.ts: use absolute @fastify/view root (cwd-independent) - layout.ejs: HTML-escape user.name output (<%= instead of <%-) - index.spec.ts: restore mutated process.env in teardown --- .github/workflows/examples.yml | 2 ++ .../src/index.spec.ts | 19 +++++++++++++++++++ .../example-fastify-web-dynamic/src/index.ts | 2 +- .../views/layout.ejs | 2 +- 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 72ea6a2..b3d98a2 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -23,6 +23,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Setup Node.js with npm caching uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 diff --git a/examples/example-fastify-web-dynamic/src/index.spec.ts b/examples/example-fastify-web-dynamic/src/index.spec.ts index 804220d..3eb1f0b 100644 --- a/examples/example-fastify-web-dynamic/src/index.spec.ts +++ b/examples/example-fastify-web-dynamic/src/index.spec.ts @@ -15,6 +15,17 @@ const DOMAIN = 'example.auth0.local'; const BRAND_1_ORIGIN = 'http://brand-1.localhost:3000'; const BRAND_2_ORIGIN = 'http://brand-2.localhost:3000'; +// Capture the original values so we can restore them in teardown and keep this +// suite from leaking env state into other tests. +const ENV_KEYS = [ + 'AUTH0_DOMAIN', + 'AUTH0_CLIENT_ID', + 'AUTH0_CLIENT_SECRET', + 'AUTH0_SESSION_SECRET', + 'APP_BASE_URL', +] as const; +const originalEnv = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]])); + process.env.AUTH0_DOMAIN = DOMAIN; process.env.AUTH0_CLIENT_ID = ''; process.env.AUTH0_CLIENT_SECRET = ''; @@ -48,6 +59,14 @@ afterEach(() => server.resetHandlers()); afterAll(async () => { server.close(); await fastify.close(); + // Restore the env vars we mutated so this suite does not affect others. + for (const key of ENV_KEYS) { + if (originalEnv[key] === undefined) { + delete process.env[key]; + } else { + process.env[key] = originalEnv[key]; + } + } }); describe('example-fastify-web-dynamic: per-host base URL resolution', () => { diff --git a/examples/example-fastify-web-dynamic/src/index.ts b/examples/example-fastify-web-dynamic/src/index.ts index 6ea21ac..7507235 100644 --- a/examples/example-fastify-web-dynamic/src/index.ts +++ b/examples/example-fastify-web-dynamic/src/index.ts @@ -28,7 +28,7 @@ fastify.register(fastifyView, { engine: { ejs: ejs, }, - root: './views', + root: path.join(__dirname, '../views'), layout: 'layout.ejs', }); diff --git a/examples/example-fastify-web-dynamic/views/layout.ejs b/examples/example-fastify-web-dynamic/views/layout.ejs index 7d42191..dd09dda 100644 --- a/examples/example-fastify-web-dynamic/views/layout.ejs +++ b/examples/example-fastify-web-dynamic/views/layout.ejs @@ -42,7 +42,7 @@