diff --git a/API.md b/API.md index 6814c41..f6cc00f 100644 --- a/API.md +++ b/API.md @@ -905,7 +905,9 @@ Returns the OIDC discovery document with endpoint URLs, supported scopes, and si **Query Parameters:** - `response_type` — Must be `code` - `client_id` — Registered OAuth client ID -- `redirect_uri` — Must exactly match a URI registered for the client +- `redirect_uri` — Must match a URI registered for the client, either exactly + or against a registered wildcard pattern (`*` = one hostname label, `**` = + any number of labels) - `scope` — Space-separated: `openid`, `profile`, `email` - `state` — Opaque value returned unchanged in the redirect - `code_challenge` — PKCE challenge (SHA-256 of code_verifier, base64url-encoded) diff --git a/docs/oauth.md b/docs/oauth.md index d57795a..88e6944 100644 --- a/docs/oauth.md +++ b/docs/oauth.md @@ -36,7 +36,11 @@ An OAuth client represents an app that authenticates against the SSO. Each has: - `client_id` (UUID) + `client_secret` (bcrypt-hashed; the **raw secret is shown once** when the client is created or rotated — save it immediately). - `name`, `description`, `created_by` (the admin uid that created it). -- `redirect_uris` — allowed callback URLs (must match exactly). +- `redirect_uris` — allowed callback URLs. Each entry matches exactly, or may + use `*` (one hostname label) / `**` (any number of labels) as a wildcard — + e.g. `https://*.example.com/__proxy_auth/callback` covers every host + theta42/proxy fronts under `example.com`, so you don't have to register + each proxied host's callback individually. - `scopes` — requested scopes (default `openid profile email groups`). - `allowed_groups` — restrict the client to members of specific SSO groups (empty = any valid user). diff --git a/nodejs/routes/oauth.js b/nodejs/routes/oauth.js index ec75cb3..2a0b8a1 100644 Binary files a/nodejs/routes/oauth.js and b/nodejs/routes/oauth.js differ diff --git a/nodejs/tests/redirect_uri.test.js b/nodejs/tests/redirect_uri.test.js new file mode 100644 index 0000000..43cfd68 --- /dev/null +++ b/nodejs/tests/redirect_uri.test.js @@ -0,0 +1,66 @@ +'use strict'; + +const { redirectUriAllowed } = require('../routes/oauth'); + +// Pure logic, no LDAP/Redis needed -- regression coverage for the bug where +// theta42/proxy's per-host SSO callback (a different URL per proxied host, +// e.g. https://site.example.com/__proxy_auth/callback) could never match a +// single OAuth client's redirect_uris list without registering every host's +// callback individually. `*`/`**` wildcard support lets one registered +// pattern (e.g. https://*.example.com/__proxy_auth/callback) cover a whole +// domain's worth of proxied hosts. +describe('redirectUriAllowed', () => { + test('exact match still works with no wildcard present', () => { + expect(redirectUriAllowed( + ['https://app.example.com/cb'], + 'https://app.example.com/cb' + )).toBe(true); + }); + + test('rejects a uri that is not registered', () => { + expect(redirectUriAllowed( + ['https://app.example.com/cb'], + 'https://app.example.com/cb2' + )).toBe(false); + }); + + test('* matches exactly one hostname label', () => { + expect(redirectUriAllowed( + ['https://*.example.com/__proxy_auth/callback'], + 'https://site.example.com/__proxy_auth/callback' + )).toBe(true); + }); + + test('* does not span multiple labels', () => { + expect(redirectUriAllowed( + ['https://*.example.com/__proxy_auth/callback'], + 'https://site.nl.example.com/__proxy_auth/callback' + )).toBe(false); + }); + + test('** spans multiple labels', () => { + expect(redirectUriAllowed( + ['https://**.example.com/__proxy_auth/callback'], + 'https://site.nl.example.com/__proxy_auth/callback' + )).toBe(true); + }); + + test('scheme mismatch is not allowed even with a wildcard', () => { + expect(redirectUriAllowed( + ['https://*.example.com/__proxy_auth/callback'], + 'http://site.example.com/__proxy_auth/callback' + )).toBe(false); + }); + + test('a wildcard pattern does not match an unrelated domain', () => { + expect(redirectUriAllowed( + ['https://**.example.com/__proxy_auth/callback'], + 'https://evil.com/__proxy_auth/callback' + )).toBe(false); + }); + + test('empty/missing patterns list rejects everything', () => { + expect(redirectUriAllowed([], 'https://app.example.com/cb')).toBe(false); + expect(redirectUriAllowed(undefined, 'https://app.example.com/cb')).toBe(false); + }); +}); diff --git a/nodejs/views/oauth_clients.ejs b/nodejs/views/oauth_clients.ejs index 78374b4..eeaefe6 100644 --- a/nodejs/views/oauth_clients.ejs +++ b/nodejs/views/oauth_clients.ejs @@ -22,6 +22,9 @@
* matches one hostname label, ** matches any number of labels.
+
* matches one hostname label and ** matches any
+ number of labels, e.g. https://*.example.com/__proxy_auth/callback
+ covers every host theta42/proxy fronts under example.com without registering
+ each one individually.
+