Separate related domains (ROR) from the in-domain sign-in allow-list

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.
This commit is contained in:
2026-09-06 22:29:12 +00:00
parent fefd54f02a
commit b9e6f4bc27
16 changed files with 376 additions and 160 deletions
+6 -4
View File
@@ -430,7 +430,7 @@ defineExpose({ focusFirstElement })
<div class="section-header">
<h2>Realms</h2>
<p class="section-description">
Each realm is one passkey rp-id with its own display name, optional dedicated auth host, and allowed origins (including Related Origin Requests origins on unrelated domains). Changes apply immediately.
Realms are the domain names this instance serves. Each realm has its own passkeys: users sign in per domain, and a passkey registered on one realm never works on another. Add a realm for every domain you operate. To let several <em>different</em> domain names share the same passkeys, open the realm and configure related domains (WebAuthn Related Origins). Changes apply immediately.
</p>
</div>
<div class="section-actions">
@@ -441,13 +441,14 @@ defineExpose({ focusFirstElement })
<tr>
<th>Realm</th>
<th>Auth Host</th>
<th class="center">Origins</th>
<th class="center">Sign-in Sites</th>
<th class="center">Related Domains</th>
<th class="center"></th>
</tr>
</thead>
<tbody>
<tr v-if="!realms || realms.length === 0">
<td colspan="4" class="center muted">No realms configured</td>
<td colspan="5" class="center muted">No realms configured</td>
</tr>
<tr v-for="realm in realms" :key="realm.rp_id">
<td class="perm-name-cell">
@@ -463,7 +464,8 @@ defineExpose({ focusFirstElement })
<span v-if="realm.effective_auth_host">{{ realm.effective_auth_host }}<span v-if="!realm.auth_host" class="muted"> (shared)</span></span>
<span v-else class="muted"></span>
</td>
<td class="center">{{ realm.origins?.length || 0 }}</td>
<td class="center">{{ realm.origins?.length || 'All' }}</td>
<td class="center">{{ realm.related_origins?.length || '—' }}</td>
<td class="center">
<button v-if="!realm.is_default" @click="$emit('deleteRealm', realm)" class="icon-btn delete-icon" aria-label="Delete realm" title="Delete realm"></button>
</td>