diff --git a/frontend/auth/admin/AdminApp.vue b/frontend/auth/admin/AdminApp.vue index e3472f8..f1aca61 100644 --- a/frontend/auth/admin/AdminApp.vue +++ b/frontend/auth/admin/AdminApp.vue @@ -484,14 +484,15 @@ function createRealm() { origins: [], originValidation: [], wellKnownCheck: null, - authHostValidation: null, }) } function openRealm(realm) { // One combined list for editing: in-domain sites and related origins, - // classified by hostname. Strip https:// scheme for editing. - const origins = [...(realm.origins || []), ...(realm.related_origins || [])] + // classified by hostname. The default is always shown explicitly as the + // '*.rp_id' wildcard entry. Strip https:// scheme for editing. + const stored = realm.origins || [] + const origins = [...(stored.length ? stored : ['*.' + realm.rp_id]), ...(realm.related_origins || [])] .map(o => o.replace(/^https:\/\//, '')) openDialog('realm-edit', { isNew: false, @@ -501,7 +502,6 @@ function openRealm(realm) { origins, originValidation: origins.map(() => null), wellKnownCheck: null, - authHostValidation: null, }) } @@ -945,13 +945,19 @@ async function submitDialog() { const auth_host = d.auth_host?.trim() || '' // The combined origins list is split by hostname: entries on the // rp-id domain form the in-domain allow-list, entries elsewhere are - // related origins (ROR). Bare hostnames are sent as-is; the backend + // related origins (ROR). Wildcards ('*.app.example.com') classify by + // their base domain. Bare hostnames are sent as-is; the backend // normalizes them with https://. const origins = [] const related_origins = [] for (const o of (d.origins || []).map(o => o.trim()).filter(o => o)) { let hn = null - try { hn = new URL(o.startsWith('http') ? o : 'https://' + o).hostname } catch { continue } + if (o.startsWith('*.')) { + hn = o.slice(2).replace(/\.+$/, '') + } else { + try { hn = new URL(o.startsWith('http') ? o : 'https://' + o).hostname } catch { continue } + } + if (!hn) continue if (hn === rp_id || hn.endsWith('.' + rp_id)) origins.push(o) else related_origins.push(o) } diff --git a/frontend/src/admin/AdminDialogs.vue b/frontend/src/admin/AdminDialogs.vue index cbfd47a..833a4dc 100644 --- a/frontend/src/admin/AdminDialogs.vue +++ b/frontend/src/admin/AdminDialogs.vue @@ -1,5 +1,5 @@ @@ -321,15 +359,6 @@ function validateAuthHost() { - -

Validating...

-

Valid

-

Must be {{ dialog.data.rp_id }} or a subdomain of it.

-

Well-formed but unreachable — make sure it is routed to this instance.

-

Reachable, but does not serve this domain.

-

Optional. Moves the account and admin interface to this one hostname. Sign-in works on every site regardless.

Allowed Origins @@ -344,14 +373,24 @@ function validateAuthHost() { class="origin-input" :class="{ 'input-error': dialog.data.originValidation[i] === 'invalid' }" /> - related - + 🔑 + related +
+ +
+ + + +
+

Some sites are unreachable — make sure they are routed to this instance.

Some sites are reachable but do not serve this domain.

-

All of {{ dialog.data.rp_id }} and its subdomains may sign in (default). Add entries to restrict sign-in to specific sites, or to share this domain's passkeys with another domain name.

-

Only the listed sites may sign in with this domain's passkeys. Entries on other domain names become related origins (WebAuthn ROR).

+

+ Only the listed sites may sign in with this domain's passkeys — *.{{ dialog.data.rp_id }} means the domain and all its subdomains. + Entries on other domain names become related origins (WebAuthn ROR). The 🔑 site hosts the account and admin interface (set via ⋮). +