'**.example.com' covers the apex and subdomains at any depth;
'*.example.com' covers exactly one subdomain level (neither apex nor
deeper) — analogous to permission scope wildcards, and sidestepping the
DNS/TLS/nginx ambiguity around '*.'. This also allows excluding the apex
where wanted. The seeded/default entry becomes '**.{rp-id}' (init,
add-domain, legacy empty-origins conversion, branch-era '*' sanitize
rewrite).
- Domain dialog submits one origins map (in-domain + related together);
classification is derived, the submit-time split is gone
- Placeholder-row machinery deleted: an empty list now means 'nothing
allowed'; new domains get a real pre-filled '*.{rp-id}' row that
follows rp-id edits until touched
- Plain '*' is invalid; wildcards only within the domain
- Editing the domain in use: when no auth host is marked and the admin's
current page origin would no longer be allowed to run ceremonies, Save
is disabled with an explanatory error (mirrors the backend guard)
- Origin list display: single table with derived related badges; '*'
sort special case removed
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.
DomainConfig.related is gone: origins holds both in-domain sign-in sites
and related origins, classified by whether the entry lies within the
rp-id. Misfiling is impossible by construction, so validation/sanitize
lose their reclassification paths.
Origins are now always explicit: an empty table allows nothing (a
related-only domain is a valid configuration). Plain '*' is rejected —
wildcards must be under the rp-id ('*.{rp-id}'). New databases, added
domains and legacy conversions seed '*.{rp-id}' (legacy empty origins
meant allow-all). Passkey's implicit allow-all default is gone; the
admin API takes a single origins map and the lockout guard refuses
emptying the table on the domain in use.
MultiSite.md now describes the shipped feature for administrators —
managing domains in the admin panel, sign-in sites, the auth host,
related origins, cross-domain sign-in, OIDC discovery, CLI and 1.x
upgrade notes — instead of the internal design narrative. oidc.md:
realms -> domains, instance-global provider, current auth-code shapes.
- Empty-origins default shows as a '*' placeholder row that is not
persisted unless edited (open+save no longer tightens any-scheme to
https-only)
- Foreign wildcards are flagged invalid instead of being classified as
related origins; over-cap related list disables Save
- Single-label rp-ids accepted (matching backend validate_rp_id)
- Auth-host mark follows row edits; row menu state resets on dialog close
- rp-id/origin keys lowercased for classification and submit
- settings cache: stale in-flight responses no longer overwrite a forced
refresh
- Remove the dead oidc-edit dialog path and other unused code; fix stale
comments (realm→domain, '*' semantics, per-domain discovery URLs)
- validate_config: reject multiple auth-host marks per domain;
sanitize_config clears extras (first wins) and coerces junk entry
values to presence-only
- origin_key: lowercase keys, strip trailing dots (bare hosts/wildcards)
- Passkey._allowlisted: tolerate trailing-dot wildcard bases
- wschat: stamp remote-flow sessions with the session host's domain,
not the approver's
- auth_host redirects: keep the port (redirect to the configured auth
host instead of the normalized, port-less current host)
- update_domain: required fields (wholesale replace) — no silent wipes
- admin: fix pre-existing lockout-guard order in org permission removal;
permission PATCH keeps domain restriction when omitted; 400 instead of
500 on unknown permission UUIDs
- Drop dead code: db.update_config/set_session_host/delete_reset_token,
Session.metadata, oidjwt.clear_key, background aliases,
avatar.current_avatar_url/media_root, wsutil.require_pow
- Prune stale/duplicated comments and docstrings
- DB.oidc is a single OIDC (one key, one client set); hosts are issuer
aliases. OIDCCode drops its rp_id field; client CRUD is not keyed by
domain.
- No cross-domain auth-host fallback: a domain without its own auth host
uses its own hosts; several domains may share one auth host (nested
rp-ids) with deterministic best-suffix resolution.
- '*' origin shorthand expands to '*.{rp-id}'; legacy wildcards convert
as-is; related origins may point at/inside another domain's rp-id.
- Admin UI and docs updated to match.
- 'paskia init [rp-id] [rp-name]' and 'paskia migrate [rp-id]' are now
positional; comma separation and the --rp-id/--rp-name flags are gone.
- With an existing paskia.kantadb, init adds the rp-id as a new domain
(seeding its OIDC provider) or updates an existing domain's rp-name.
- Origin allow-list semantics clarified: the bare '*' entry allows
anything within the rp-id domain on any scheme and port (also the
empty-list default and its display in the admin UI, replacing the
synthetic '*.rp-id' row); '*.x' wildcards are https-only; exact entries
match scheme, host and port. Legacy '*.rp-id' wildcards migrate to '*'
to preserve their any-scheme meaning.
Only rp-id and rp-name are essential bootstrap-time configuration;
origins and auth hosts are set up afterwards via the admin interface.
Removes --origin/--auth-host from init and from devserver, and the
now-unused hostutil.validate_auth_host.
Finish the realm→domain terminology removal across source, tests, e2e
and docs. The stored config drops all lists: Config.domains is keyed by
rp-id, DomainConfig.origins/related are objects keyed by host (https://
omitted), values True or OriginEntry(auth_host=True). The default/primary
domain concept is gone; ordering is display-time. Tests and e2e updated
to the new API shapes (not run). Database re-migrated from the legacy
backup into the new format.
Serving never refuses to start because of stored realm config: the
registry build sanitizes best-effort and warns — misfiled origin entries
are reclassified (a cross-domain origins entry is served as a related
origin) or dropped, collisions resolve first-come-wins, over-cap related
lists truncate, unsalvageable realms are skipped. Fixing the stored
config stays the admin interface's job, and it stays reachable on any
working realm. Only a config with no servable realm at all is fatal.
Admin realm writes stay strict and gain self-lockout guards: an update
that would leave the admin's current host unable to run ceremonies for
the realm they are on is refused (unless an auth host takes over
ceremonies), and deleting the realm currently in use is refused.
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.
Serve never converts databases: with no paskia.kantadb it points at
'paskia init', or at 'paskia migrate' when legacy *.paskiadb candidates
exist. migrate converts a lone candidate, or the one named by --rp-id
when several exist; the rest stay in place. devserver fails fast with
the same hint. gitignore covers paskia.kantadb and *.converted-bak.
README quick start and configuration updated for init/serve split and
paskia.kantadb; API.md server-config endpoints replaced with admin realm
CRUD and the well-known webauthn endpoint; proxy guides point at the
realm auth-host setting; oidc.md documents per-realm issuers; MultiSite.md
rewritten from the implementation plan into documentation of the shipped
mechanics, policy model, and design rationale.
- global-setup bootstraps via one-shot 'paskia init --rp-id
localhost,test.localhost' in the test-data directory (which doubles as
the server cwd, dropping the removed PASKIA_DB), captures the reset
token from init output (stdout+stderr), then spawns plain serve.
- New 50-multirealm spec: per-host settings dispatch, 421 for unknown
hosts, /.well-known/webauthn 404 until a related origin is added via
the admin realm API (and removed again), and a full cross-realm remote
login: requester on test.localhost, permit on localhost with a fresh
virtual-authenticator passkey, session validated on test.localhost.
Asserts the profile enrollment prompt and realm badge render.
- New fixtures/remote-auth.ts drives the remote-auth WS protocol in
browser context, including the PBKDF2 PoW.
- devserver bootstraps via one-shot 'paskia init' when no database
exists (multi --rp-id, --rp-name/--auth-host/--origin apply to the
default realm), then runs plain 'paskia' serve which reads all realm
configuration from the database; legacy *.paskiadb is adopted by
serve without init.
- Caddy origins iterate all bootstrap rp-ids.
- vite.config.js accepts a comma-separated PASKIA_AUTH_HOST list and
proxies /.well-known/webauthn to the backend so ROR works in dev.
- caddy/auth/setup forwards /.well-known/openid-configuration and
/.well-known/webauthn to paskia (they must not be swallowed by a
static /.well-known/* file handler); Caddyfile.dev updated to match
the generated dev config.
- Admin: replace Server Options dialog with per-realm management —
realms table on the overview, add/edit/delete realm dialog backed by
/auth/api/admin/realms/. Origins may be any well-formed origin;
non-subdomain ones are related origins (ROR, max 5) and the dialog
points at the .well-known/webauthn URL that must list them.
Connectivity checks compare against the edited realm's rp-id and
degrade to warnings instead of blocking saves.
- Host mode (limited profile) now keys off own_auth_host so realms
sharing another realm's auth host serve the full profile locally.
- Credential list shows a realm badge on passkeys registered for a
different rp-id than the current realm.
- Profile shows an enrollment prompt when the user has no passkey for
the current realm (e.g. after a cross-realm remote login).
- Remote auth permit shows the requesting realm when it differs from
the approver's own.
- settings cache can be force-refreshed after realm changes.
- conftest: bootstrap seeds a localhost realm Config; realm_registry
fixture builds the runtime registry; avatar storage redirected to a
per-test tmp dir; credentials/sessions stamped with the test realm.
- test_cli rewritten for the init/serve split, incl. legacy adoption.
- TestServerConfig replaced by TestRealms covering the realm CRUD API,
cross-realm validation, delete guards and effective-auth-host fallback.
- Avatar/OIDC tests updated for per-realm providers and realm-derived
URLs; obsolete PASKIA_DB path tests removed.
- DispatchMiddleware (outermost app middleware) resolves Host to a realm:
HTTP 421 for unknown hosts, WS closed pre-accept (1008); cross-realm WS
only via the origin realm's effective auth host. Current realm exposed
via request.state.realm and the current_realm() contextvar.
- Credentials and sessions are scoped by realm rp_id: authentication only
matches credentials of the dispatched realm; sessions record rp_id.
- Auth codes (OIDC and cookie exchange) are stamped with the issuing
realm and verified at redemption; remote-auth permits mint the exchange
code for the *requesting* device's realm.
- OIDC provider state (clients, signing keys) is per realm; token,
userinfo, keys and backchannel-logout endpoints use the dispatched
realm; refresh re-stamps the session issuer.
- /.well-known/webauthn serves the realm's related origins (ROR).
- Admin /server-config replaced by /realms CRUD (validated cross-realm,
registry rebuilt on change); permission domains may reference any
realm's hosts or clients; /settings reports the realm's own vs
effective auth host.
- paskia.globals and the runtime-backed hostutil helpers are gone.
- Config is now a realm list (first = default); DB.oidc keyed by rp-id
- Database at fixed paskia.kantadb; user files under paskia.data/users/
- Legacy <rp-id>.paskiadb reader/converter in db/legacy.py (to be deleted eventually)
- paskia init / paskia serve CLI split; serve adopts a lone legacy database
- Realm registry (paskia/realms.py) with cross-realm validation
- Per-realm OIDC keys in oidjwt; backchannel logout uses Session.rp_id/issuer
- Schema migrations discarded; on-disk legacy format assumed current
/auth/api/forward?public=1 passes requests through with a Remote-Public
header (anonymous/forbidden/authenticated) instead of 401/403, so routes
can allow anonymous visitors while still identifying logged-in users.
Reauth (max_age) still requires the auth flow. Documented in Headers.md,
api/forward.md, Integration.md and all proxy guides.
perm=a|b+c now means (a or b) and c; repeated perm args remain ANDed.
Out-of-spec values (empty alternatives, chars outside the scope charset,
stray %2B) are rejected with 400 instead of being silently misparsed;
extra spaces between groups are tolerated. Forward endpoint 400/500
details name /auth/api/forward as origin without echoing query args.
Wildcards are now filename-like: * stays within a :- or /-separated
segment, ** spans segments, partial segments allowed. Slash added to
allowed scope characters for path-based permissions.
The vendored db/logging module duplicated what kanta now provides:
diff formatting, UUID-to-label resolution via logfmt callbacks, unsafe
character filtering and value truncation. Censoring of oidc.key material
moves into the format_log_uuid logfmt callback in db.lifecycle, taking
care to hide only the value, not the 'key' path component itself.