IBEx Wallet

API

🔗 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

1 Client on bar.domain calls the API normally — browser sends Origin: https://bar.domain automatically.
2 Backend extracts bar.domain from Origin, finds primaryRpId = "foo.domain" in the Domain table.
3 Backend returns WebAuthn options with rpId: "foo.domain" (the primary domain).
4 Browser fetches https://foo.domain/.well-known/webauthn and verifies bar.domain is listed.
5 Browser finds and uses the passkey created for foo.domain — authentication succeeds!
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:

  1. Explicit (recommended): The client sends rpId as a query parameter (?rpId=foo.domain) or header (X-RpId: foo.domain).
  2. Automatic fallback: The backend extracts the origin from the browser's Origin or Referer header.

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):

Related domains (bar.domain, baz.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

For older browsers, authentication will fail if the origin doesn't exactly match the rpId.


Troubleshooting

← Back to Documentation