diff --git a/packages/auth0-express/MIGRATION.md b/packages/auth0-express/MIGRATION.md index 63f4436..e98bfb4 100644 --- a/packages/auth0-express/MIGRATION.md +++ b/packages/auth0-express/MIGRATION.md @@ -632,7 +632,10 @@ res.redirect(`/auth/login?${params.toString()}`); ```
-All Supported Authorization Parameters +Commonly Used Authorization Parameters + +Any query parameter on `/auth/login` is forwarded to `/authorize` **except** a reserved set +(see below). These are the ones integrators pass most often: | Parameter | Purpose | Example | |-----------|---------|---------| @@ -642,6 +645,17 @@ res.redirect(`/auth/login?${params.toString()}`); | `ui_locales` | UI language | `es`, `fr` | | `screen_hint` | Skip login/signup UI | `signup` | | `max_age` | Max age in seconds | `3600` | +| `organization` | Organization to log into | `org_123` | +| `connection` | Connection to use | `google-oauth2` | + +**Reserved (never forwarded from the query):** the SDK strips protocol- and routing-critical +parameters so a crafted login link cannot control them — `response_type`, `state`, +`code_challenge`, `code_challenge_method`, `client_id`, `redirect_uri`, `nonce`, `scope`, the +target-API family (`audience`, `aud`, `resource`, `resources`, `resource_indicator`), the +Request-Object family (`request`, `request_uri`, `id_token_hint`, `claims`, `response_mode`), and +`authorization_details`. To set any of these, call +[`req.auth0.client.startInteractiveLogin`](../../README.md) directly instead of relying on +query-string forwarding.
diff --git a/packages/auth0-express/src/handlers/login-handler.spec.ts b/packages/auth0-express/src/handlers/login-handler.spec.ts index 653b70c..2a19fcb 100644 --- a/packages/auth0-express/src/handlers/login-handler.spec.ts +++ b/packages/auth0-express/src/handlers/login-handler.spec.ts @@ -149,8 +149,27 @@ describe('login handler - query parameter sanitization', () => { }; describe('OAuth protocol parameter blocklist', () => { - // These params are completely absent from the authorization URL because the SDK does not include them - test.each(['state', 'nonce'])('strips %s from the authorization URL', async (param) => { + // These params are completely absent from the authorization URL because the SDK does not + // include them. Assert full absence (stronger than `!== 'evil'`): a regression that forwarded + // the param with any other value would still be caught. + test.each([ + 'state', + 'nonce', + // Target-API family — the SDK routes the target via the typed `audience` only. + 'audience', + 'aud', + 'resource', + 'resources', + 'resource_indicator', + // Request-Object and related params must not be user-forwardable. + 'request', + 'request_uri', + 'id_token_hint', + 'claims', + 'response_mode', + // Rich Authorization Requests grant details. + 'authorization_details', + ])('strips %s from the authorization URL', async (param) => { const app = createConfiguredApp(appConfig); const res = await request(app).get('/auth/login').query({ [param]: 'evil' }); @@ -176,6 +195,15 @@ describe('login handler - query parameter sanitization', () => { }); describe('safe parameters still pass through', () => { + test('still forwards prompt and login_hint (intentionally not reserved)', async () => { + const app = createConfiguredApp(appConfig); + const res = await request(app).get('/auth/login').query({ prompt: 'none', login_hint: 'a@b.com' }); + expect(res.status).toBe(302); + const url = new URL(res.headers['location']?.toString() ?? ''); + expect(url.searchParams.get('prompt')).toBe('none'); + expect(url.searchParams.get('login_hint')).toBe('a@b.com'); + }); + test('allows safe params when mixed with dangerous ones', async () => { const app = createConfiguredApp(appConfig); diff --git a/packages/auth0-express/src/handlers/login-handler.ts b/packages/auth0-express/src/handlers/login-handler.ts index 40a5ed4..558a2ef 100644 --- a/packages/auth0-express/src/handlers/login-handler.ts +++ b/packages/auth0-express/src/handlers/login-handler.ts @@ -22,7 +22,26 @@ const RESERVED_OAUTH_PARAMS = new Set([ 'redirect_uri', 'nonce', 'scope', + // Target-API params are one family: the transaction only records `audience`, so a link + // supplying an alias (`resource`, etc.) would mint a token for that target while it is stored + // under our own audience key, and `getAccessToken()` would later hand the app a token minted + // for someone else's resource. Reserve the whole family, matching auth0-auth-js's denylist. 'audience', + 'aud', + 'resource', + 'resources', + 'resource_indicator', + // Request Objects and related params must be SDK/tenant-controlled, not + // user-supplied via a login link. prompt/login_hint are + // intentionally NOT reserved — integrators commonly forward them. + 'request', + 'request_uri', + 'id_token_hint', + 'claims', + 'response_mode', + // Rich Authorization Requests: a crafted link must not be able to inject its own grant + // details, which an app reading `authorizationDetails` in its callback would then act on. + 'authorization_details', ]); /**