Skip to content

Commit 9712a82

Browse files
fix(auth0-express): reserve OIDC Request-Object params in login handler (#25)
1 parent a7a42d5 commit 9712a82

3 files changed

Lines changed: 64 additions & 3 deletions

File tree

packages/auth0-express/MIGRATION.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,10 @@ res.redirect(`/auth/login?${params.toString()}`);
632632
```
633633
634634
<details>
635-
<summary><strong>All Supported Authorization Parameters</strong></summary>
635+
<summary><strong>Commonly Used Authorization Parameters</strong></summary>
636+
637+
Any query parameter on `/auth/login` is forwarded to `/authorize` **except** a reserved set
638+
(see below). These are the ones integrators pass most often:
636639
637640
| Parameter | Purpose | Example |
638641
|-----------|---------|---------|
@@ -642,6 +645,17 @@ res.redirect(`/auth/login?${params.toString()}`);
642645
| `ui_locales` | UI language | `es`, `fr` |
643646
| `screen_hint` | Skip login/signup UI | `signup` |
644647
| `max_age` | Max age in seconds | `3600` |
648+
| `organization` | Organization to log into | `org_123` |
649+
| `connection` | Connection to use | `google-oauth2` |
650+
651+
**Reserved (never forwarded from the query):** the SDK strips protocol- and routing-critical
652+
parameters so a crafted login link cannot control them — `response_type`, `state`,
653+
`code_challenge`, `code_challenge_method`, `client_id`, `redirect_uri`, `nonce`, `scope`, the
654+
target-API family (`audience`, `aud`, `resource`, `resources`, `resource_indicator`), the
655+
Request-Object family (`request`, `request_uri`, `id_token_hint`, `claims`, `response_mode`), and
656+
`authorization_details`. To set any of these, call
657+
[`req.auth0.client.startInteractiveLogin`](../../README.md) directly instead of relying on
658+
query-string forwarding.
645659
646660
</details>
647661

packages/auth0-express/src/handlers/login-handler.spec.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,27 @@ describe('login handler - query parameter sanitization', () => {
149149
};
150150

151151
describe('OAuth protocol parameter blocklist', () => {
152-
// These params are completely absent from the authorization URL because the SDK does not include them
153-
test.each(['state', 'nonce'])('strips %s from the authorization URL', async (param) => {
152+
// These params are completely absent from the authorization URL because the SDK does not
153+
// include them. Assert full absence (stronger than `!== 'evil'`): a regression that forwarded
154+
// the param with any other value would still be caught.
155+
test.each([
156+
'state',
157+
'nonce',
158+
// Target-API family — the SDK routes the target via the typed `audience` only.
159+
'audience',
160+
'aud',
161+
'resource',
162+
'resources',
163+
'resource_indicator',
164+
// Request-Object and related params must not be user-forwardable.
165+
'request',
166+
'request_uri',
167+
'id_token_hint',
168+
'claims',
169+
'response_mode',
170+
// Rich Authorization Requests grant details.
171+
'authorization_details',
172+
])('strips %s from the authorization URL', async (param) => {
154173
const app = createConfiguredApp(appConfig);
155174

156175
const res = await request(app).get('/auth/login').query({ [param]: 'evil' });
@@ -176,6 +195,15 @@ describe('login handler - query parameter sanitization', () => {
176195
});
177196

178197
describe('safe parameters still pass through', () => {
198+
test('still forwards prompt and login_hint (intentionally not reserved)', async () => {
199+
const app = createConfiguredApp(appConfig);
200+
const res = await request(app).get('/auth/login').query({ prompt: 'none', login_hint: 'a@b.com' });
201+
expect(res.status).toBe(302);
202+
const url = new URL(res.headers['location']?.toString() ?? '');
203+
expect(url.searchParams.get('prompt')).toBe('none');
204+
expect(url.searchParams.get('login_hint')).toBe('a@b.com');
205+
});
206+
179207
test('allows safe params when mixed with dangerous ones', async () => {
180208
const app = createConfiguredApp(appConfig);
181209

packages/auth0-express/src/handlers/login-handler.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,26 @@ const RESERVED_OAUTH_PARAMS = new Set([
2222
'redirect_uri',
2323
'nonce',
2424
'scope',
25+
// Target-API params are one family: the transaction only records `audience`, so a link
26+
// supplying an alias (`resource`, etc.) would mint a token for that target while it is stored
27+
// under our own audience key, and `getAccessToken()` would later hand the app a token minted
28+
// for someone else's resource. Reserve the whole family, matching auth0-auth-js's denylist.
2529
'audience',
30+
'aud',
31+
'resource',
32+
'resources',
33+
'resource_indicator',
34+
// Request Objects and related params must be SDK/tenant-controlled, not
35+
// user-supplied via a login link. prompt/login_hint are
36+
// intentionally NOT reserved — integrators commonly forward them.
37+
'request',
38+
'request_uri',
39+
'id_token_hint',
40+
'claims',
41+
'response_mode',
42+
// Rich Authorization Requests: a crafted link must not be able to inject its own grant
43+
// details, which an app reading `authorizationDetails` in its callback would then act on.
44+
'authorization_details',
2645
]);
2746

2847
/**

0 commit comments

Comments
 (0)