Separate related domains (ROR) from the in-domain sign-in allow-list
RealmConfig.origins is again purely an allow-list of sign-in sites within the realm's domain (unset = rp-id and all subdomains), restoring the restriction semantics the realm rework had silently turned into an always-open subtree. Cross-domain ROR origins move to their own RealmConfig.related_origins field — always additive, capped, validated to be outside the rp-id domain, and the sole source of the /.well-known/webauthn document. Admin API POST/PATCH accept related_origins; misfiled entries are rejected (cross-domain in origins, in-domain in related_origins). Admin UI: the realm dialog edits the two lists separately with end-user-oriented explanations (allowed sign-in sites vs. related domains + the well-known note); the Realms section intro explains the multi-domain model, and the table shows sign-in site and related domain counts.
This commit is contained in:
+4
-2
@@ -73,12 +73,14 @@ E.g. Org admin cannot see anything of the other orgs that he has no admin access
|
||||
| PATCH | /auth/api/admin/oidc-clients/{uuid}/reset-secret | Reset client secret | 200/401/403 |
|
||||
| DELETE | /auth/api/admin/oidc-clients/{uuid} | Delete OIDC client | 200/401/403 |
|
||||
| GET | /auth/api/admin/realms/ | List realms (rp-ids) with derived URLs | 200/401/403 |
|
||||
| POST | /auth/api/admin/realms/ | Create realm `{rp_id, rp_name?, auth_host?, origins?}` | 200/400/401/403 |
|
||||
| PATCH | /auth/api/admin/realms/{rp_id} | Update realm rp_name/auth_host/origins | 200/400/401/403 |
|
||||
| POST | /auth/api/admin/realms/ | Create realm `{rp_id, rp_name?, auth_host?, origins?, related_origins?}` | 200/400/401/403 |
|
||||
| PATCH | /auth/api/admin/realms/{rp_id} | Update realm rp_name/auth_host/origins/related_origins | 200/400/401/403 |
|
||||
| DELETE | /auth/api/admin/realms/{rp_id} | Delete realm (refused while credentials remain) | 200/400/401/403 |
|
||||
|
||||
Realm endpoints require the `auth:admin` permission; writes additionally require recent authentication (5 minutes). Changes are validated cross-realm and apply immediately.
|
||||
|
||||
`origins` is an allow-list of sign-in sites *within* the realm's domain (empty = the rp-id and all subdomains may authenticate). `related_origins` lists *other* domains that may assert this realm'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 list are rejected: cross-domain entries in `origins`, in-domain entries in `related_origins`.
|
||||
|
||||
### WebSockets: /auth/ws/*
|
||||
|
||||
| Path | Used for | Notes |
|
||||
|
||||
+28
-19
@@ -61,7 +61,7 @@ no redirects, no cross-domain cookies. Browser support is universal
|
||||
Model: realm `company.com` with related origin `https://app2.com`. A page
|
||||
on `app2.com` calls WebAuthn with `rpId: "company.com"`; the passkey is
|
||||
scoped to `company.com`; `clientDataJSON.origin` is `https://app2.com`,
|
||||
which the backend validates against the realm's allow-list.
|
||||
which the backend validates against the realm's related origins.
|
||||
|
||||
Server side: paskia's `Passkey` passes `expected_origin=<the
|
||||
pre-validated origin>` and `expected_rp_id=self.rp_id`; the webauthn
|
||||
@@ -69,12 +69,14 @@ library string-compares origin and rp-id separately. The frontend never
|
||||
chooses `rpId` client-side — ceremony options arrive from the server over
|
||||
the WS. On top of that:
|
||||
|
||||
- Origin rule: an origin is valid if it is in the rp-id subtree **or
|
||||
explicitly listed in the realm's configured origins**. Explicit listing
|
||||
is the trust boundary.
|
||||
- Origin rule: `origins` and `related_origins` are separate fields.
|
||||
An in-domain origin (rp-id or subdomain) is valid unless the realm's
|
||||
`origins` allow-list is set, in which case it must be listed there. An
|
||||
origin on another domain is valid only when listed in the realm's
|
||||
`related_origins` — explicit related listing is the trust boundary.
|
||||
- `GET /.well-known/webauthn` on the canonical rp-id host serves
|
||||
`{"origins": [...]}` from the realm's configured non-subdomain origins
|
||||
(404 when there are none).
|
||||
`{"origins": [...]}` from the realm's related origins (404 when there
|
||||
are none).
|
||||
- Dispatch resolution treats a Host matching a configured related-origin
|
||||
hostname as belonging to that origin's realm (exact match only —
|
||||
`www.app2.com` does not follow `app2.com`).
|
||||
@@ -151,7 +153,8 @@ class RealmConfig(msgspec.Struct, omit_defaults=True):
|
||||
rp_id: str
|
||||
rp_name: str | None = None
|
||||
auth_host: str | None = None # this realm's dedicated auth host
|
||||
origins: list[str] | None = None # subdomain origins AND related origins (§2.A)
|
||||
origins: list[str] | None = None # allow-list of in-domain sign-in sites
|
||||
related_origins: list[str] | None = None # cross-domain ROR origins (§2.A)
|
||||
|
||||
class Config(msgspec.Struct, omit_defaults=True):
|
||||
realms: list[RealmConfig] # at least one; first entry is the default realm
|
||||
@@ -163,11 +166,14 @@ class Config(msgspec.Struct, omit_defaults=True):
|
||||
- The first entry is the default realm, used only where a default is
|
||||
genuinely needed (bootstrap reset-link URL, startup box ordering,
|
||||
master-admin entry point) — never for dispatch.
|
||||
- **Origin validation**: each configured origin is either in the rp-id
|
||||
subtree (classic) or an explicit related origin. Related origins are
|
||||
counted and capped (default 5) and must not collide with another
|
||||
realm's rp-id/auth-host/related origins. These rules are enforced both
|
||||
at startup and at admin write time. Origins are never _implicitly_
|
||||
- **Origin validation** — two separate concerns: `origins` entries must
|
||||
be within the rp-id domain (an allow-list; unset = the rp-id and all
|
||||
subdomains may authenticate). `related_origins` entries must be
|
||||
outside it, are capped (default 5), and must not collide with another
|
||||
realm's rp-id/auth-host/related origins nor fall inside another
|
||||
realm's domain. Misfiled entries (cross-domain in `origins`, in-domain
|
||||
in `related_origins`) are rejected. These rules are enforced both at
|
||||
startup and at admin write time. Origins are never _implicitly_
|
||||
cross-domain.
|
||||
|
||||
### 3.2 CLI: bootstrap (`paskia init`) vs. serve (`paskia`)
|
||||
@@ -362,9 +368,9 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No
|
||||
`PATCH/DELETE /auth/api/admin/realms/{rp_id}`. Writes require recent
|
||||
authentication (5 minutes).
|
||||
- Create: `rp_id` + optional `rp_name` (defaults to the rp-id),
|
||||
`auth_host`, `origins`; full §3.1 validation (cap, cross-realm
|
||||
collisions); registry rebuilt immediately, including the realm's
|
||||
`Passkey` instance and OIDC provider entry.
|
||||
`auth_host`, `origins`, `related_origins`; full §3.1 validation (cap,
|
||||
cross-realm collisions); registry rebuilt immediately, including the
|
||||
realm's `Passkey` instance and OIDC provider entry.
|
||||
- Update: same validation against the would-be combined config.
|
||||
Changing a realm's rp-id itself is **not supported** (it would orphan
|
||||
every credential stamped with the old rp-id) — delete and recreate
|
||||
@@ -373,13 +379,16 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No
|
||||
carries the realm's rp-id (re-enroll or delete those credentials
|
||||
first); cascades nothing else (users/orgs are global).
|
||||
- The admin UI has a Realms section with a table (rp-id, name,
|
||||
effective auth host, origin count), per-row edit/delete and an
|
||||
add-realm dialog. The dialog's connectivity probe fetches
|
||||
effective auth host, sign-in site and related domain counts),
|
||||
per-row edit/delete and an add-realm dialog. The dialog edits the
|
||||
in-domain allow-list and the related domains as two separate lists
|
||||
with their own explanations. Its connectivity probe fetches
|
||||
`<origin>/auth/api/settings` and compares the returned rp-id against
|
||||
the edited realm — a related origin served by this instance answers
|
||||
with the realm's rp_id. Connectivity/mismatch results are warnings;
|
||||
only malformed entries and an auth host outside the rp-id domain
|
||||
block saving.
|
||||
malformed entries, misfiled entries (cross-domain in the allow-list,
|
||||
in-domain in related domains), and an auth host outside the rp-id
|
||||
domain block saving.
|
||||
- Credential listings: `Credential.rp_id` serializes automatically into
|
||||
user-info and admin user detail responses; the frontend shows an rp-id
|
||||
badge **only when `credential.rp_id !== settings.rp_id`** — single-
|
||||
|
||||
Reference in New Issue
Block a user