Skip to content

Instantly share code, notes, and snippets.

@o-az
Created March 28, 2026 22:46
Show Gist options
  • Select an option

  • Save o-az/8186cc726c1182776a1b039d408ad2b1 to your computer and use it in GitHub Desktop.

Select an option

Save o-az/8186cc726c1182776a1b039d408ad2b1 to your computer and use it in GitHub Desktop.
WebAuthn rejects .local RP IDs in Chrome — evidence and references

WebAuthn rejects .local RP IDs in Chrome

Problem

Passkey creation fails with SecurityError when the origin is app.tempo.local and the RP ID is tempo.local.

Root cause

Chrome validates WebAuthn RP IDs against the Public Suffix List (PSL). Both the origin host and the claimed RP ID must have a registry-controlled domain per the PSL. .local is not in the PSL — it's an IANA special-use domain reserved for mDNS (RFC 6762).

The rejection happens in OriginIsAllowedToClaimRelyingPartyId:

if (!net::registry_controlled_domains::HostHasRegistryControlledDomain(
        caller_origin.host(),
        net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
        net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES) ||
    !net::registry_controlled_domains::HostHasRegistryControlledDomain(
        claimed_relying_party_id,
        net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
        net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
  // This prevents "https://login.awesomecompany" from claiming
  // "awesomecompany", which is allowed by the spec but disallowed by
  // chromium.
  return false;
}

HostHasRegistryControlledDomain("tempo.local", ...) returns false because .local has no PSL entry. The function doesn't explicitly name .local — the rejection is implicit via the PSL lookup returning no registry.

The unit tests confirm the pattern for non-PSL domains:

// Internal labels (disallowed by Chromium)
{"https://login.awesomecompany", "awesomecompany", false},

tempo.local falls into the same category — a domain under a TLD with no PSL entry.

Verified behavior

Origin RP ID Result
https://localhost:3001 localhost ✅ Passkey created (localhost has a special-case bypass in Chrome)
https://app.tempo.local:3001 tempo.local SecurityError.local not in PSL
https://wallet.tempo.xyz tempo.xyz .xyz is in the PSL

References

  1. Chromium RP ID validation: webauthn_security_utils.ccOriginIsAllowedToClaimRelyingPartyId()
  2. Chromium test cases: webauthn_security_utils_unittest.cc — internal label rejection pattern
  3. IANA special-use domains: https://www.iana.org/assignments/special-use-domain-names/special-use-domain-names.xhtmllocal. is reserved
  4. Public Suffix List: https://publicsuffix.org/list/public_suffix_list.dat.local absent
  5. RFC 6762 (mDNS): https://www.rfc-editor.org/rfc/rfc6762 — defines .local as reserved for multicast DNS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment