Files
paskia/docs/MultiSite.md
T
LeoVasanko 84985501f5 MultiSite: one instance serves authentication across many domains (#4)
- Serve multiple domains (RP IDs) from one instance: host-based dispatch,
  per-domain credentials and sessions, domains managed at runtime in the
  admin UI — previously one RP per instance
- Cross-domain sign-in via Related Origin Requests: per-domain related-origins
  list with a served .well-known/webauthn document
- Explicit per-domain origin lists with shell-glob wildcards (**. for apex +
  any subdomain depth, *. for one level), editable in the admin UI with
  validation and self-lockout guards
- Per-domain auth hosts: the account/admin UI can live on a different host
  per domain, no longer confined to subdomains of a single RP
- CLI: 'paskia init <rp-id [rp-name]' initializes or adds a domain to an
  existing database; 'paskia migrate' converts legacy databases

BREAKING CHANGES (v2.0):
- Database schema: config is now per-domain and credentials/sessions carry
  an rp_id — existing databases must be converted with 'paskia migrate'
- Origins are now explicit: main implicitly allowed every subdomain of the
  RP; configure '**.' origins to reproduce that behavior
- CLI: the flat '--rp-id/--rp-name/--origin/--auth/--save' flags are
  replaced by the 'init' and 'migrate' subcommandsReviewed-on: #4
2026-09-07 22:14:42 +00:00

9.4 KiB

Multiple Domains (Multi-Site)

One Paskia instance on one port serves several domains from a single database. Typical uses:

  • app1.company.com and app2.com cannot share passkeys, but user management should stay under one roof.
  • A few alternative brand names should accept the same passkeys.

Shared vs. per-domain

Shared across all domains: user accounts, organizations, roles, permissions, and OIDC clients. A user account exists once and can sign in on every domain.

Per domain: passkeys and sessions.

  • A passkey is registered to one domain name (enforced by the browser): a passkey created for company.com works on company.com and its subdomains, never on an unrelated name — unless that name is configured as a related origin. A user active on two domains simply holds one passkey per domain.
  • A session is bound to the exact host that issued it.

The simplest case: several sites on one domain

Multi-site does not require several domains. Sites under one name — app1.company.com, app2.company.com and so on — share the domain company.com: one passkey works on all of them (WebAuthn natively allows the domain and its subdomains), and the default **.company.com origin entry already lets every one of them sign in. Add explicit entries only to restrict which sites may sign in, and mark an auth host (see below) if you want sign-in centralized on one site.

Managing domains

The admin panel's Domains section (master admins only) lists every domain with its allowed origins. There you can add, edit and delete domains; changes apply immediately without a restart and require a recent sign-in (within 5 minutes). The 🔑 and 🔗 markers in the origins column identify the auth host and related origins (below).

A domain consists of:

  • Domain (rp-id) — the domain name passkeys belong to, e.g. company.com. ("rp-id" is the WebAuthn term; read it as "domain name".) It cannot be changed after creation, because existing passkeys are bound to it — delete and re-create the domain instead.
  • Display name (rp-name) — branding shown in sign-in dialogs and registered with passkeys.
  • Allowed origins — the sites where this domain's passkeys may sign in, plus any related origins.

Deleting a domain is refused while any passkey is still registered to it, when it is the last remaining domain, or when it is the domain you are currently using. Users and organizations are never deleted with a domain — they are shared. An edit that would lock you out (your current site could no longer run passkey ceremonies for that domain) is refused as well.

Allowed origins (sign-in sites)

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.

Entry forms:

  • **.company.com — the domain itself and its subdomains at any depth, https only. The default entry of a new domain.
  • *.company.com — exactly one subdomain level: app.company.com yes, but neither the apex company.com nor a.b.company.com. Use this when the apex or deeper subdomains should not serve sign-ins.
  • app.company.com — exactly this host, https only.
  • http://localhost:8080 — a full origin with scheme, for non-https exceptions.

Under localhost, both wildcard forms match any scheme and any port, as a development convenience. An entry on a different domain name automatically becomes a related origin (🔗) instead — see below.

Wildcard syntax

Wildcards follow the shell-glob convention — the same one permission scopes use (* within a segment, ** across segments, see the perm argument): ** spans any number of hostname labels including none, * spans exactly one. Wildcards must stay within the domain, and plain * is not accepted — it would suggest "anything goes".

Conventions elsewhere differ: DNS, TLS and nginx take *.example.com to mean subdomains only (TLS: exactly one level), while browser-extension match patterns take it as apex plus any depth. The */** split sidesteps that ambiguity, and the less obvious forms *example.com and .example.com remain unsupported on purpose.

The auth host (🔑)

Marking one allowed origin as the auth host (row menu ⋮ → "Set as auth host") centralizes the account and admin interface on that site, e.g. auth.company.com:

  • On the auth host the web UI is served at the site root (/ instead of /auth/), and all passkey operations for the domain happen there.
  • The domain's other sites show only a minimal profile page at /auth/ with logout and a link to the full profile; their sign-in dialogs talk to the auth host behind the scenes. Every sign-in site still needs to be listed in (or covered by) the allowed origins.

The auth host is strictly per-domain — domains never borrow each other's auth host. To consolidate several domains on one sign-in site, that site must lie under each domain's name (nested domains, e.g. domains company.com and auth.company.com) and be marked on each of them.

Sometimes a few different domain names should accept the same passkeys — for example after a rebrand, when app2.com should keep working with existing company.com passkeys. Adding app2.com to company.com's allowed origins makes it a related origin: browsers then let app2.com use company.com passkeys directly — no redirects, no cross-domain cookies. (This uses the WebAuthn "Related Origin Requests" mechanism, which is why the UI also says ROR.)

Rules:

  • At most 5 related origins per domain — a browser limit. This is for a small family of equally trusted sites, not for hundreds of customer domains; use separate domains for those.
  • Exact hosts only — no wildcards — and always outside the domain's own name.
  • The browser verifies the setup against https://<domain>/.well-known/webauthn. Paskia serves that document automatically when it hosts the domain's main site; if the main site is hosted elsewhere, copy the JSON document shown in the domain dialog and publish it there. The dialog also checks the published document for you.
  • A related origin shares the domain's security boundary completely — do not mix trust levels within one domain.
  • When several domains could claim a host: a host that is a configured domain name always serves its own domain; otherwise an explicit related origin listing wins over merely falling under another domain's name.

Passkeys never move between domains. If you later consolidate separate domains onto one, users re-enroll: sign in once via remote authorization (below), then register a new passkey for the common domain from the profile page.

Signing in across domains

Users exist once, but need a passkey per domain. Two mechanisms smooth this over:

  • Remote authorization: a user without a passkey for the current domain can start a login request and approve it from any device already signed in — on any domain of the instance. The approval screen shows which site is requesting access.
  • Enroll on the spot: when the signed-in user has no passkey for the current domain, the profile page offers "Add Passkey for {domain}", so everyday sign-in stays local from then on.

OIDC with multiple domains

OIDC clients are shared by the whole instance: register a client once and it works through every domain. Each domain serves its own discovery URL (https://<host>/.well-known/openid-configuration), listed in the admin OIDC client view. Have each app pick one discovery URL and use it consistently, so its tokens always validate against the same issuer.

Command line

The admin panel covers all domain management after bootstrap. On the command line:

  • paskia init [domain] [name] — creates the database paskia.kantadb with the first domain. Run again with an existing database to add another domain (or update a display name).
  • paskia migrate [domain] — converts a legacy 1.x {domain}.paskiadb database to paskia.kantadb; see below.
  • paskia — serves all configured domains; takes no domain options, only --listen as a per-run override.

Upgrading from 1.x

2.0 intentionally changes the on-disk layout and the domain configuration model:

  • The database is the single file paskia.kantadb in the working directory; user files (avatars) live in paskia.data/users/. paskia migrate performs the conversion and renames the old database 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); 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 development-server (vite) setting.
  • OIDC becomes instance-global: one signing key and one client set, reachable through every domain's discovery URL (previously each rp-id had its own). Existing clients keep working through any domain.