{
  "id": 20127744,
  "title": "Testing Passkeys Through a Tunnel: RP ID, Origins, and Why Credentials Don’t Follow You From localhost",
  "url": "https://urgent.news/2026/11/15/testing-passkeys-through-a-tunnel-rp-id-origins-and-why-credentials",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-11-15T06:34:13.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/instatunnel/testing-passkeys-through-a-tunnel-rp-id-origins-and-why-credentials-dont-follow-you-from-25ml"
  },
  "original_language": "en",
  "account": "When you build a passkey flow on a local server like http://localhost:3000, the registration and login processes work smoothly. However, when you try to test the login with a real phone over a tunnel, the passkey login screen shows no passkeys at all. This isn't due to any error in the system; instead, passkeys are tied to a specific domain, not your app or laptop. When testing through a tunnel, the tunnel's hostname becomes part of the credential, causing the credentials to be separate from those registered on the local domain.\n\nA passkey is associated with a single Relying Party ID (RP ID), which is essentially a domain name. The RP ID must match the page's hostname or be a registrable parent domain of it. It cannot be a public suffix like \"com\". Your server verifies both the RP ID and the full origin (scheme, host, and port) separately. Therefore, a random tunnel URL, which changes both the RP ID and the origin each time it's used, will orphan any passkeys previously registered.\n\nTo resolve this issue, using a stable, named tunnel subdomain ensures a consistent RP ID. This way, passkeys registered on the local domain can be authenticated on the tunnel-hosted page. An RP ID is simply the effective domain of the page, without any additional scheme, port, or path. For example, for a page on www.example.com, a valid RP ID would be \"www.example.com\" or \"example.com\". However, public suffixes such as \"com\" are not accepted.\n\nIt's crucial to remember that localhost credentials and tunnel-hostname credentials are distinct environments. Credentials registered for localhost won't authenticate under a tunnel hostname. Hence, it's essential to treat these environments as separate when testing. While the authenticator (like iCloud Keychain, Google Password Manager, or Windows Hello) only offers credentials matching the RP ID, your database might still hold public keys attached to an RP ID you no longer serve. Therefore, two checks are needed: one for the RP ID and another for the origin. These checks ensure that passkeys work seamlessly across different environments.",
  "summary": "You build a passkey flow on http://localhost:3000 . Registration works, login works, and you feel done. Then you open a tunnel so a real phone can try it, and the login screen offers no passkeys at all. Nothing is broken in the usual sense. The tunnel is up, the certificate is valid, the browser is happy. Your credentials simply live somewhere else. Passkeys are bound to a domain, not to your app…",
  "key_points": [
    "Passkeys are tied to a specific domain (RP ID), not the app or laptop.",
    "Testing through a tunnel changes the RP ID and origin, causing passkeys to be orphaned.",
    "Use a stable, named tunnel subdomain to maintain consistent RP ID for passkey authentication."
  ],
  "editors_take": "The strict coupling of passkeys to specific domain names forces developers to use stable tunnel subdomains to prevent authentication failures during local testing environments.",
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}