goauthentik / goauthentik/authentik
Support WebAuthn Related Origin Requests: one RP ID shared across brands
- Dominant language
- Python
- Stars
- 25.6k
- Forks
- 2k
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 651
Description
### Is your feature request related to a problem?
A single authentik instance with brands on different registrable domains (`auth.example.com`, `auth.example.net`) cannot offer passkeys that work across brands: authentik derives the WebAuthn RP ID unconditionally from the request host (`get_rp_id()` in `authentik/stages/authenticator_webauthn/utils.py`), so every brand is its own credential silo and there is no configuration to change that.
WebAuthn Level 3 solves this with Related Origin Requests: one RP ID serves several origins, authorized via `https:///.well-known/webauthn`. Supported in Chrome/Edge 128+, Safari 18+, Firefox 152+. authentik implements no part of it.
Overlaps with #24791 (configurable expected origins for native apps — the origins list below would give those a home).
### Describe the solution you'd like
A `WebAuthnRPConfig` object, referenced by brands via dropdown. Brand-level because the RP ID is a domain property and brands already own the domain; explicit origins because `Brand.domain` is a suffix matcher, not an origin, and the default brand matches every host.
```text
WebAuthnRPConfig
name = "shared-passkeys"
rp_id = "auth.example.com" # unique across configs, immutable after creation
origins = ["https://auth.example.com", "https://auth.example.net", "android:apk-key-hash:..."]
Brand A/B -> rp_config = shared-passkeys
Brand C -> rp_config = null # today's behavior, feature is opt-in
```
- Both stages send the config's `rp_id` instead of the request host; verification passes it as `expected_rp_id` and the origins list as `expected_origin` (py_webauthn already accepts a list).
- authentik serves `GET /.well-known/webauthn` when the request host equals a config's `rp_id`, returning its HTTPS origins. This works only when the `rp_id` host routes to authentik; an RP ID on a domain authentik does not serve requires hosting the document externally (the format is public).
- Origin entries are format-validated on save; every listed origin can run ceremonies for the shared RP ID, so the list is exact — no wildcards.
- Brands without an RP config keep today's behavior exactly (RP ID and expected origin from the request) — the feature is fully opt-in, no change for existing deployments.
**The one sharp edge: attaching an existing brand to a shared config strands that brand's existing passkeys** (they are bound to the old request-host RP ID — WebAuthn, not implementation). Passkey-only users are locked out with no self-service recovery. Adoption therefore needs a pre-flight that blocks while users exist whose only factor would become unusable, and docs that require a fallback factor — also because non-ROR clients fail hard (`SecurityError`) and some passkey providers (e.g. 1Password) don't support ROR yet.
### Describe alternatives that you've considered
- Consolidating logins onto one domain: defeats the purpose of brands.
- Serving the well-known externally via ingress: insufficient — authentik still sends the request-host RP ID, and verification rejects foreign origins (the authenticator signs over the RP ID hash).
- Deriving origins from brand domains: unsound — suffix matchers, no scheme/port, default brand matches everything.
### Additional context
Spec: https://www.w3.org/TR/webauthn-3/#sctn-related-origins · Guide: https://passkeys.dev/docs/advanced/related-origins/
Happy to implement this and open a PR if the direction is acceptable.
Supersedes #25395 (opened via API, which drops the template labels; resubmitted through the form so it lands in triage).
Contributor guide
Research direction
Start with get_rp_id() in authentik/stages/authenticator_webauthn/utils.py, then read the WebAuthn Related Origin Requests specification and the passkeys.dev guide. The requested result includes opt-in brand-linked RP configuration, RP ID and origin validation, the /.well-known/webauthn response, ceremony verification changes, and safeguards for existing passkeys and unsupported clients.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- authentication, backend-api-design, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100