🔗 Related Origin Requests (ROR)
With Related Origin Requests, a passkey created on foo.domain can be used to authenticate on
bar.domain (and any other related domain) — without creating a new passkey for each domain.
This is a WebAuthn standard supported by Chrome 129+, Safari, and Edge.
How It Works
bar.domain calls the API normally — browser sends
Origin: https://bar.domain automatically.
bar.domain from Origin, finds primaryRpId =
"foo.domain" in the Domain table.
https://foo.domain/.well-known/webauthn and verifies
bar.domain is listed.
No client-side changes needed. The backend and browser handle ROR transparently. The client code remains identical whether authenticating from the primary or a related domain.
rpId Resolution
The backend resolves the rpId using two mechanisms:
- Explicit (recommended): The client sends
rpIdas a query parameter (?rpId=foo.domain) or header (X-RpId: foo.domain). - Automatic fallback: The backend extracts the origin from the browser's
OriginorRefererheader.
The resolved rpId is then looked up in the Domain table. If the domain has a primaryRpId, the
system uses it (ROR mode). Otherwise, the domain's own rpId is used directly.
Domain Configuration
Primary domain (foo.domain):
rpId:"foo.domain"primaryRpId:null
Related domains (bar.domain, baz.domain):
rpId:"bar.domain"primaryRpId:"foo.domain"
.well-known/webauthn Endpoint
The IBEx API automatically serves the .well-known/webauthn endpoint on the primary domain.
Browsers use it to verify that a related origin is authorized.
GET https://foo.domain/.well-known/webauthn
{
"origins": [
"https://bar.domain",
"https://baz.domain"
]
}
Client Example
Using @simplewebauthn/browser — the code is identical for primary and related domains:
// Client on https://bar.domain
// Browser automatically sends Origin: https://bar.domain
// 1. Get authentication options
const response = await fetch('https://api.ibex.fi/v1.2/auth/sign-in');
const { credentialRequestOptions } = await response.json();
// credentialRequestOptions.rpId → "foo.domain" (primary)
// 2. Call WebAuthn (browser handles ROR automatically)
const assertion = await startAuthentication(credentialRequestOptions);
// 3. Send the response
const result = await fetch('https://api.ibex.fi/v1.2/auth/sign-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: assertion })
});
// ✅ Authenticated with the passkey from foo.domain!
For server-to-server calls (no browser Origin header), pass the rpId explicitly:
// Query parameter
fetch('https://api.ibex.fi/v1.2/auth/sign-in?rpId=bar.domain');
// Or header
fetch('https://api.ibex.fi/v1.2/auth/sign-in', {
headers: { 'X-RpId': 'bar.domain' }
});
Security
| Attack Vector | Prevention |
|---|---|
| Origin forgery | Only non-forgeable headers accepted (Origin, Referer) |
| Invalid primaryRpId | Foreign key constraint at database level |
| Circular references | Cycle detection in resolvePrimaryRpId() |
| Unauthorized domain queries | .well-known/webauthn validates domain existence |
| Database error bypass | Requests with origin rejected if DB validation fails (503) |
| Domain chaining | primaryRpId must point to a primary domain only |
Browser Support
- Chrome 129+
- Safari (recent versions)
- Edge (recent versions)
For older browsers, authentication will fail if the origin doesn't exactly match the rpId.
Troubleshooting
- Passkey not found — verify
primaryRpIdis set in the Domain table, check.well-known/webauthnlists the origin, and confirm browser ROR support. - "rpId unknown" error — the primary domain (
primaryRpId) must exist in the Domain table with a validapiKey. - No rpId resolved — ensure the browser sends
Origin(automatic for CORS), or passrpIdvia query parameter /X-RpIdheader.