Docs + e2e: single origins table, explicit-origins semantics
MultiSite.md/API.md/README: one allowed-origins list per domain where
out-of-domain entries are related origins; empty list allows nothing;
plain '*' rejected; new domains and legacy conversions seed '*.{rp-id}'.
E2E multidomain spec: new payload shape; restores return to the seeded
wildcard state.
This commit is contained in:
@@ -224,7 +224,7 @@ Set the auth host in the admin panel's Domains section.
|
||||
|
||||
One Paskia instance can serve several domains (rp-ids) from the same database: users, orgs and permissions are shared, while passkeys are registered per domain. The master admin adds domains in the admin panel's Domains section; no restart is needed.
|
||||
|
||||
A domain can also let *other* domain names use its passkeys via WebAuthn [Related Origin Requests](https://passkeys.dev/docs/advanced/related-origins/) — add them as related domains in the domain settings, and paskia serves the required `/.well-known/webauthn` declaration on the domain's main site. This is separate from the allowed-sign-in-sites list, which only restricts which subdomains of the domain's own rp-id may authenticate (empty = the rp-id and all subdomains).
|
||||
A domain can also let *other* domain names use its passkeys via WebAuthn [Related Origin Requests](https://passkeys.dev/docs/advanced/related-origins/) — list them in the domain's allowed origins (they show as related domains), and paskia serves the required `/.well-known/webauthn` declaration on the domain's main site. In-domain entries of the same list restrict which sites of the domain's own name may authenticate (a new domain defaults to `*.{domain}` — the whole domain over https).
|
||||
|
||||
See [Multi-Site documentation](docs/MultiSite.md) for details.
|
||||
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ E.g. Org admin cannot see anything of the other orgs that he has no admin access
|
||||
|
||||
Domain endpoints require the `auth:admin` permission; writes additionally require recent authentication (5 minutes). Changes are validated cross-domain and apply immediately.
|
||||
|
||||
`origins` is an object keyed by sign-in sites *within* the domain (bare hosts, `*.` wildcards matching the base domain and subdomains over https only — any scheme and port under localhost — full origins when not https, or the bare `*` as shorthand for a wildcard over the rp-id itself); an empty object means the rp-id and all subdomains may authenticate on any scheme. A value of `true` marks presence; `{"auth_host": true}` additionally marks the entry as the domain's authentication host. `related` is an object keyed by *other* domains that may assert this domain's rp-id (WebAuthn Related Origin Requests, max 5); those are published at `/.well-known/webauthn` on the rp-id host. Entries filed under the wrong map are rejected: cross-domain entries in `origins`, in-domain entries in `related`.
|
||||
`origins` is a single object keyed by all of the domain's sign-in sites. Entries *within* the rp-id domain are in-domain sites: bare hosts, `*.` wildcards under the rp-id (matching the base domain and subdomains over https only — any scheme and port under localhost), or full origins when not https; the plain `*` wildcard is not accepted. Entries *outside* the rp-id domain are related origins that may assert this domain's rp-id (WebAuthn Related Origin Requests, max 5, no wildcards); those are published at `/.well-known/webauthn` on the rp-id host. An empty object allows nothing of the domain itself. A value of `true` marks presence; `{"auth_host": true}` additionally marks an in-domain entry as the domain's authentication host.
|
||||
|
||||
### WebSockets: /auth/ws/*
|
||||
|
||||
|
||||
+15
-12
@@ -50,22 +50,23 @@ well.
|
||||
|
||||
## Allowed origins (sign-in sites)
|
||||
|
||||
This list controls which sites may sign in with the domain's passkeys:
|
||||
This list controls which sites may sign in with the domain's passkeys.
|
||||
Everything is explicit: an empty list allows nothing of the domain itself
|
||||
(related origins, below, still work — a domain can in principle run
|
||||
entirely on related origins). A new domain starts with one entry,
|
||||
`*.{domain}`, which suits most deployments.
|
||||
|
||||
- **Empty list (the default):** the domain and all its subdomains may
|
||||
sign in, on any scheme. This suits most deployments; the domain dialog
|
||||
shows the default as a `*` placeholder row.
|
||||
- **Once you add entries, the list becomes an allow-list** — only listed
|
||||
sites may sign in:
|
||||
Entry forms:
|
||||
|
||||
- `*.company.com` — the domain and all its subdomains, https only. Under
|
||||
`localhost`, wildcards match any scheme and any port, as a development
|
||||
convenience.
|
||||
- `app.company.com` — exactly this host, https only.
|
||||
- `*.company.com` — the base domain and all its subdomains, https only.
|
||||
Under `localhost`, wildcards match any scheme and any port, as a
|
||||
development convenience.
|
||||
- `http://localhost:8080` — a full origin with scheme, for non-https
|
||||
exceptions.
|
||||
- `*` — shorthand for `*.{domain}`: the whole domain over https.
|
||||
|
||||
Entries must be inside the domain. An entry on a different domain name
|
||||
Wildcards must stay within the domain, and plain `*` is not accepted — it
|
||||
would suggest "anything goes". An entry on a different domain name
|
||||
automatically becomes a related origin (🔗) instead — see below.
|
||||
|
||||
## The auth host (🔑)
|
||||
@@ -165,7 +166,9 @@ model:
|
||||
aside to `{domain}.paskiadb.converted-bak`. With several legacy
|
||||
databases, the positional argument selects one by name. Legacy wildcard
|
||||
origins convert as-is (https only, except any scheme and port under
|
||||
`localhost`).
|
||||
`localhost`); a legacy database without configured origins — where that
|
||||
meant the whole domain was allowed — gets an explicit `*.{domain}`
|
||||
entry.
|
||||
- Origins, auth hosts and related origins are no longer environment
|
||||
settings — they live in the database and are managed in the admin
|
||||
panel's Domains section. `PASKIA_AUTH_HOST` remains only as a
|
||||
|
||||
@@ -82,12 +82,13 @@ test.describe('Multi-domain E2E', () => {
|
||||
const domains = await list.json()
|
||||
expect(domains.map((r: any) => r.rp_id).sort()).toEqual(['localhost', 'test.localhost'])
|
||||
const localhostDomain = domains.find((r: any) => r.rp_id === 'localhost')
|
||||
expect(localhostDomain.origins).toEqual({})
|
||||
expect(localhostDomain.origins).toEqual({ '*.localhost': true })
|
||||
|
||||
// Add a related origin (unrelated domain) to the localhost domain
|
||||
// Add a related origin (unrelated domain) to the localhost domain —
|
||||
// same origins table; classification is derived from the rp-id
|
||||
const patch = await page.request.patch(`${baseUrl}/auth/api/admin/domains/localhost`, {
|
||||
headers,
|
||||
data: { rp_name: '', origins: {}, related: { 'app.example.com': true } },
|
||||
data: { rp_name: '', origins: { '*.localhost': true, 'app.example.com': true } },
|
||||
})
|
||||
expect(patch.ok()).toBeTruthy()
|
||||
|
||||
@@ -105,10 +106,10 @@ test.describe('Multi-domain E2E', () => {
|
||||
expect(relResp.ok()).toBeTruthy()
|
||||
expect((await relResp.json()).rp_id).toBe('localhost')
|
||||
|
||||
// Restore: remove related origins again so later tests see the pristine state
|
||||
// Restore: back to the pristine seeded state for later tests
|
||||
const restore = await page.request.patch(`${baseUrl}/auth/api/admin/domains/localhost`, {
|
||||
headers,
|
||||
data: { rp_name: '', origins: {}, related: {} },
|
||||
data: { rp_name: '', origins: { '*.localhost': true } },
|
||||
})
|
||||
expect(restore.ok()).toBeTruthy()
|
||||
const after = await page.request.get(`${baseUrl}/.well-known/webauthn`)
|
||||
@@ -137,7 +138,7 @@ test.describe('Multi-domain E2E', () => {
|
||||
// *.localhost hostname to loopback, so the auth host is reachable.
|
||||
const patch = await page.request.patch(`${baseUrl}/auth/api/admin/domains/test.localhost`, {
|
||||
headers,
|
||||
data: { rp_name: '', origins: { [`http://${authHost}`]: { auth_host: true } }, related: {} },
|
||||
data: { rp_name: '', origins: { [`http://${authHost}`]: { auth_host: true }, '*.test.localhost': true } },
|
||||
})
|
||||
expect(patch.ok()).toBeTruthy()
|
||||
|
||||
@@ -163,10 +164,11 @@ test.describe('Multi-domain E2E', () => {
|
||||
expect(redir.status()).toBe(307)
|
||||
expect(redir.headers()['location']).toMatch(/^http:\/\/auth\.test\.localhost(:\d+)?\/$/)
|
||||
} finally {
|
||||
// Restore: no origins, no auth host (later tests expect pristine state)
|
||||
// Restore: back to the pristine seeded state (later tests sign in on
|
||||
// test.localhost, and an empty table would allow nothing)
|
||||
const restore = await page.request.patch(`${baseUrl}/auth/api/admin/domains/test.localhost`, {
|
||||
headers,
|
||||
data: { rp_name: '', origins: {}, related: {} },
|
||||
data: { rp_name: '', origins: { '*.test.localhost': true } },
|
||||
})
|
||||
expect(restore.ok()).toBeTruthy()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user