The example above shows what signing in feels like. Here is what happens behind those moments, from creating the passkey to using it on a later visit. This is a written explanation; it does not create a credential or run an authentication request.
First, separate the three jobs.
- The website’s server
- Also called the relying party. It manages your account, asks for proof, and decides whether a response is valid.
- Your browser
- Connects the website to the passkey system through WebAuthn, the browser’s authentication API. It knows the page’s actual origin and enforces which website can request which credential.
- The authenticator
- The part that creates and uses the keys. It may be provided by your device, a password manager, or a physical security key. Your page’s JavaScript does not get access to its private key.
There are two related keys. The private key makes a signature; the public key verifies it. A signature proves that the matching private key was used for particular data. This is not “encrypt with one key and decrypt with the other.” HTTPS separately protects the connection.
Background: MDN’s WebAuthn overview ↗
1. Save a passkey for your account.
For a new account, the website can register a passkey as its first sign-in method; a password is not a WebAuthn requirement. For an existing account, enrollment must be authorized using an existing sign-in or recovery process. Email verification, identity checks, and account recovery are separate decisions made by the website. Approving a local device prompt alone does not establish ownership of an existing account.
The server prepares a registration request.
For an existing account, it first checks that you are allowed to add a sign-in method. It creates an unpredictable challenge and associates it with this registration attempt. It sends the browser the challenge, website identity, user identifier, supported algorithms, and credential preferences.
The browser asks to create a credential.
The page calls
navigator.credentials.create()withpublicKeyoptions. The browser checks the website’s eligibility and lets you choose an available passkey provider.You approve; the authenticator creates the key pair.
Your face, fingerprint, or device PIN can authorize the operation locally. The credential is scoped to the website’s relying party ID. The private key stays protected by the authenticator or provider.
The server validates and saves the result.
The browser returns a credential ID, client data, and an attestation object containing registration information, including the public key. The server verifies the expected challenge, origin, RP ID and required flags, and processes attestation according to policy. It then links the verified credential to the account.
The public key goes to the website here.
credential ID: kY7p…Q2w
attestationObject → authData
→ credentialPublicKey:
key type: EC2
algorithm: ES256
curve: P-256
x (hex): 9f2c7a81…
y (hex): 4b18d6e3…Illustrative decoded view, with values shortened by …; this is not a complete response or a usable key. The public key is inside the attestation object’s authenticator data, encoded as a COSE key. These labels translate its numeric fields into words. The server extracts and stores it after validating registration.
The two coordinates, x and y, describe this example’s public key. They are safe for the website to receive. The private key is not part of the registration response.
Registration details: Google’s server registration guide ↗
2. Use the saved passkey to sign in.
“Next visit”: request a fresh challenge.
The server generates a new random challenge for this attempt. It keeps enough state to check the response later. A previously recorded response will not match a fresh challenge. The server must enforce expiration and single use; the browser’s prompt timeout alone does not do that.
The page calls
navigator.credentials.get()with the server’s options. A website can identify an account first and list permitted credential IDs, or let a discoverable passkey identify the account.“Your device asks for your OK”: select and authorize the key.
The browser and authenticator locate a credential for the requested relying party. If user verification is required, an appropriate local check must succeed. A touch showing that someone is present and a PIN or biometric check verifying the user are distinct signals.
The authenticator makes the cryptographic proof.
It signs the authenticator data together with a hash of the browser’s client data. The client data includes the challenge and page origin; authenticator data includes an RP ID hash and flags. That binds the proof to this attempt and website context.
signature = Sign(privateKey, authenticatorData || SHA256(clientDataJSON))Here,
||means joining bytes. The private key is used to calculate the signature; it is not included in the response.“You’re in”: the server verifies before creating a session.
The browser returns the credential ID, signature, authenticator data, client data and, when provided, user handle. The server looks up the saved public key and checks the signature plus the expected challenge, origin, RP ID and required presence/verification flags. It checks account ownership and processes counters and backup flags according to policy.
Only after successful verification does the application establish its normal signed-in session. An unresolved, cancelled, expired or invalid response must not sign anyone in.
A challenge looks like random text.
{
"challenge": "qGtaOP6qT7LOKxtrsd-koChtLA69XJ9ltWH-sw9r7U8",
"rpId": "mysticcoders.com",
"userVerification": "required"
}A fictional request-options fragment. This full example challenge represents 32 random bytes written as base64url text. Base64url is a way of writing bytes, not encryption. The challenge is not a password or secret; it must be unpredictable and fresh for each attempt.
The response ties the proof to that challenge.
credential ID: kY7p…Q2w
clientDataJSON (decoded):
{
"type": "webauthn.get",
"challenge": "qGtaOP6qT7LOKxtrsd-koChtLA69XJ9ltWH-sw9r7U8",
"origin": "https://mysticcoders.com",
"crossOrigin": false
}
authenticatorData (base64url):
JUCtq-qS…
signature (base64url):
MEUCIQDx…Illustrative response fragments, unpacked for reading; … marks shortened values. In JSON transport, binary fields such as clientDataJSON, authenticatorData and signature are commonly base64url strings. Here clientDataJSON is decoded so you can see the same challenge coming back. These fragments are not a valid signed response.
The credential ID tells the server which saved public key to use. Matching the challenge alone is not enough: the server also checks the signature and website context. The public key was saved during setup; it does not need to be sent again for this sign-in. The private key is never sent to the website.
Data formats: Registration public key ↗ · Client data fields ↗.
Protocol checks: WebAuthn assertion verification ↗. Server flow: Google’s authentication guide ↗.
3. Try the same passkey on a fake website.
Our real example uses mysticcoders.com. The unrelated mysticcoders-login.example cannot simply claim that RP ID. The browser validates the request against the actual origin, regardless of the page’s logo or wording.
If the fake page requests a credential for its own domain, it does not get the real site’s credential. If it registers a new key, the real server has not registered that public key for your account. A copied response is also tied to its original challenge and context.
An RP ID is a domain identifier; an origin also includes the scheme and port. Legitimate subdomains can share an eligible parent RP ID, and WebAuthn defines explicit related-origin arrangements. A lookalike spelling alone grants no access.
Passkeys protect this sign-in exchange. They do not fix a compromised device, a stolen session cookie, or weak account recovery.
Four server operations, plus your account system.
These are illustrative endpoint names, not endpoints implemented by this demo. A WebAuthn library can generate options and verify responses; your application still owns account authorization, challenge lifetime, credential storage, sessions, and recovery.
POST /passkeys/register/options- Authorize enrollment, create a registration challenge, and return creation options.
POST /passkeys/register/verify- Verify the browser’s registration response and save the credential against the authorized account.
POST /passkeys/signin/options- Create an authentication challenge and return request options.
POST /passkeys/signin/verify- Verify the assertion, consume the challenge, update credential metadata, and establish the session.
Browser APIs work with binary data. JSON transport needs suitable encoding and decoding; a browser library can handle that conversion and the WebAuthn calls. Handle cancellation and unsupported environments in the UI. Credential management and recovery need their own application flows.
Implementation references: SimpleWebAuthn server ↗ · SimpleWebAuthn browser ↗ · Complete registration requirements ↗