Support wildcard redirect_uri patterns for OAuth clients
theta42/proxy fronts an arbitrary number of hosts behind SSO, each with its own callback URL (https://<host>/__proxy_auth/callback) — proxy's own code comment already assumed "a wildcard redirect URI covers all", but no wildcard matching existed here, so every proxied host's callback had to be registered on the shared OAuth client individually or /oauth/authorize would reject it with InvalidRedirectURI. Add `*` (one hostname label) / `**` (any number of labels) wildcard support to redirect_uri matching, e.g. `https://**.example.com/__proxy_auth/callback` now covers every host proxy fronts under example.com. Exact matches still work exactly as before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
+5
-1
@@ -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).
|
||||
|
||||
Binary file not shown.
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,9 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Redirect URIs <small class="text-muted">(one per line)</small></label>
|
||||
<textarea id="edit-redirect_uris" class="form-control shadow font-monospace" rows="3"></textarea>
|
||||
<small class="field-help text-muted d-block">
|
||||
<code>*</code> matches one hostname label, <code>**</code> matches any number of labels.
|
||||
</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Scopes</label>
|
||||
@@ -271,6 +274,12 @@
|
||||
<label class="form-label">Redirect URIs <small class="text-muted">(one per line)</small></label>
|
||||
<textarea class="form-control shadow font-monospace" name="redirect_uris" rows="3"
|
||||
placeholder="https://ha.example.com/auth/external/callback" validate=":1"></textarea>
|
||||
<small class="field-help text-muted d-block">
|
||||
<code>*</code> matches one hostname label and <code>**</code> matches any
|
||||
number of labels, e.g. <code>https://*.example.com/__proxy_auth/callback</code>
|
||||
covers every host theta42/proxy fronts under example.com without registering
|
||||
each one individually.
|
||||
</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Scopes</label>
|
||||
|
||||
Reference in New Issue
Block a user