Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion packages/auth0-express/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,10 @@ res.redirect(`/auth/login?${params.toString()}`);
```

<details>
<summary><strong>All Supported Authorization Parameters</strong></summary>
<summary><strong>Commonly Used Authorization Parameters</strong></summary>

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 |
|-----------|---------|---------|
Expand All @@ -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.

</details>

Expand Down
32 changes: 30 additions & 2 deletions packages/auth0-express/src/handlers/login-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
Expand All @@ -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');
});
Comment thread
frederikprijck marked this conversation as resolved.

test('allows safe params when mixed with dangerous ones', async () => {
const app = createConfiguredApp(appConfig);

Expand Down
19 changes: 19 additions & 0 deletions packages/auth0-express/src/handlers/login-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Comment thread
frederikprijck marked this conversation as resolved.
'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',
Comment thread
frederikprijck marked this conversation as resolved.
// 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',
]);
Comment thread
nandan-bhat marked this conversation as resolved.

/**
Expand Down