How hard is it to implement WebAuthn myself?
Short answer
The core is not hard: with a maintained library such as SimpleWebAuthn, registering and checking a passkey takes a day or two. The hard part is everything around it: choosing the RP ID, storing challenges and credentials, several passkeys per person, autofill, account recovery, fallbacks and testing across browsers and password managers. Plan for weeks, not days, or use a provider that has passkeys built in, like loginos.
WebAuthn (Web Authentication) is the browser API behind passkeys: the browser creates a key pair for your site, keeps the private key on the device or in a password manager, and signs a challenge from your server when someone signs in. The cryptography is solved for you by the browser and a server library. What decides the effort is the product work around it, and that part is easy to underestimate.
This answer breaks the job into pieces with an honest effort for each. For the step-by-step build with code, see How do I add passkeys to my website?
What does a WebAuthn implementation involve?
A production passkey login has more parts than the two ceremonies most tutorials show:
| Part | What you build | Rough effort |
|---|---|---|
| Registration and sign-in ceremonies | Two API routes each: options, then verify | 1 to 2 days with a library |
| Challenges | Random, single-use, short-lived, tied to the session | Hours |
| Credential storage | A table for several passkeys per person | Hours |
| Autofill sign-in | Conditional mediation on your login form | 1 to 2 days, mostly testing |
| Passkey management | List, name and remove passkeys in settings | 1 to 3 days |
| Fallbacks | Password, email code or link for people without a passkey | Days, if you don’t have them yet |
| Account recovery | Getting back in after losing every device | Days to weeks, and ongoing support |
| Testing | Browsers, operating systems, password managers, security keys | Ongoing |
The first three rows are the part people call “implementing WebAuthn”. The rest is what makes passkeys usable for real people, and it is where most of the time goes.
What does the library do for you?
A server library does the parts you should never write yourself. It parses the CBOR-encoded data the browser returns, reads the COSE public key, checks the signature, and confirms that the origin, the RP ID hash and the challenge match what you expect. It also reads the flags the authenticator sets: user present, user verified, backup eligible and backed up.
Maintained libraries exist for most stacks, such as SimpleWebAuthn for Node.js, py_webauthn for Python, go-webauthn for Go, WebAuthn4J for Java and webauthn-rs for Rust. With one of them, the verify step of sign-in is a single call:
import { verifyAuthenticationResponse } from "@simplewebauthn/server";
const challenge = await takeChallenge(session); // yours: read once, then delete
const passkey = await db.passkeys.find(body.id); // yours: look up by credential id
if (!challenge || !passkey) throw new Error("Sign-in expired, try again.");
const { verified, authenticationInfo } = await verifyAuthenticationResponse({
response: body,
expectedChallenge: challenge,
expectedOrigin: "https://example.com",
expectedRPID: "example.com",
credential: { id: passkey.id, publicKey: passkey.publicKey, counter: passkey.counter, transports: passkey.transports },
requireUserVerification: true,
});
if (!verified) throw new Error("Sign-in failed.");
await db.passkeys.update(passkey.id, { counter: authenticationInfo.newCounter, lastUsedAt: new Date() });Everything outside that call is yours: where the challenge lives, how passkeys are stored, what happens when the lookup fails, and which account the passkey opens.
Which parts are easy to get wrong?
Choosing the RP ID
The RP ID (relying party ID) is the domain a passkey belongs to, like example.com. A passkey works only on that domain and its subdomains, and you can’t change the RP ID later without every existing passkey stopping to work. Choose the broadest domain you control and will keep. If your product runs on several unrelated domains, a passkey for one doesn’t work on the others. Related Origin Requests, a list of allowed origins served at /.well-known/webauthn, can extend one RP ID to a few other domains, but browser support varies, so check it before you rely on it.
Challenges
Each ceremony needs a fresh random challenge from your server, at least 16 bytes. Store it on the server or in a signed, HttpOnly session, accept it once, and expire it after a few minutes. A challenge that can be reused, or that the client chooses, defeats the point of signing it.
Storing credentials
One person can have many passkeys: one per password manager, plus security keys. Store each as its own row: the credential id (unique), the public key, the signature counter, the transports, the backup flags, the AAGUID (which tells you the provider, like “iCloud Keychain”, so you can name the passkey), and when it was created and last used. Use excludeCredentials at registration so the same password manager doesn’t create a second passkey for the same account.
Counters and synced passkeys
The signature counter was meant to detect cloned keys: each sign-in should report a higher number. Passkeys synced by Apple, Google or Microsoft usually report 0 every time. Treat 0 as normal, and flag only a counter that goes down after it was above zero.
User verification
User verification means the device checked the person with Face ID, a fingerprint or a PIN. If you treat a passkey as a full sign-in, not just a second step after a password, require it (userVerification: "required") and check the flag on the server.
Autofill (conditional UI)
Passkey autofill shows passkeys in the browser’s suggestions on your username field. It needs autocomplete="username webauthn" on the field and a WebAuthn request with mediation: "conditional" started when the page loads. That request stays pending, so you have to abort it before starting another one, for example when the person clicks a “Sign in with a passkey” button.
Account recovery
Recovery decides how secure your accounts really are. A person who loses every device needs a way back, and that way is only as strong as its weakest step. Synced passkeys make loss rarer, because a new phone restores them from the person’s Apple, Google or password manager account. You still need a path for the rest: an email link, a password, recovery codes or a support process. Each of those is a feature to build and an attack surface to defend.
Testing
Passkeys behave differently across Chrome, Safari, Firefox and Edge, on macOS, Windows, iOS and Android, and with iCloud Keychain, Google Password Manager, Windows Hello, 1Password, Bitwarden and hardware security keys. Chrome DevTools has a WebAuthn panel with virtual authenticators for automated tests, but only real devices show what people see. Budget time for a test matrix, and again after major browser releases.
When should you build it, and when should you use a service?
Build it yourself when:
- Sign-in is part of your product, and you want full control over every screen.
- Your app runs on one domain, so one RP ID covers everything.
- You already run accounts, email delivery and support, so recovery has somewhere to live.
Use a service or an identity provider when:
- You want passkeys this month, not next quarter.
- Your product runs on several domains, and one RP ID can’t cover them.
- Nobody on your team wants to own recovery, testing and browser changes.
Both paths use the same standard, so moving later is possible. Passkeys already created for your RP ID keep working with any server that has their public keys.
How loginos does it
loginos runs the passkey side for you. People create and use their passkeys on loginos.io, and your app receives a standard OpenID Connect sign-in: a code, exchanged on your server for the person’s identity.
- No WebAuthn code in your app. loginos handles both ceremonies, challenges, credential storage, counters, user verification and passkey autofill on its sign-in page.
- Every domain. The passkey belongs to loginos.io, so it works on every domain of your platform, and on every other platform that uses loginos. No RP ID decision, no Related Origin Requests.
- Management. People add, name and remove their passkeys in their loginos account. Passkeys are synced by Apple, Google, Microsoft or a password manager.
- Fallback. Passwords still work for people who prefer them. The ID token’s
amrclaim says how someone signed in:["hwk"]after a passkey and["pwd"]after a password, so you can ask for a passkey before sensitive actions.
Your integration is the quickstart: two routes on your server, and the claims reference lists what the token carries. Be aware of one limit: loginos doesn’t send email yet, so there are no recovery emails. People keep a second passkey or a password as their way back in.
Frequently asked questions
How long does it take to add passkeys to an existing login?
With a maintained library, a working prototype takes a day or two. A production version with autofill, passkey management, fallbacks, recovery and a test matrix usually takes a few weeks of focused work. A hosted provider shortens that to the time it takes to add an OpenID Connect sign-in.
Do I need to check attestation?
Usually not. Attestation proves which authenticator model created a passkey. Consumer apps rarely need it, and most passkey providers don’t send it. Request attestation: "none" unless you must allow only certain security keys, for example for staff accounts.
Is WebAuthn the same as passkeys?
Passkeys are WebAuthn credentials that can be discovered by the browser without a username, and that are usually synced between a person’s devices. WebAuthn is the API and the standard. Every passkey uses WebAuthn, but WebAuthn also covers security keys used as a second factor.
Can I use WebAuthn without a library?
You can, but you shouldn’t. You would parse CBOR, decode COSE keys for several algorithms and verify signatures yourself, and a mistake there lets attackers in. The browser API calls are simple; the server-side verification is where a maintained library earns its place.
One account for every domain you run
loginos is an OpenID Connect provider with passkeys, a user pool shared by all of your domains, roles in every token, and account linking for the users you already have. Add your platform and its domains in a few minutes.
Keep reading
- PasskeysHow do I add passkeys to my website?To add passkeys, you use the WebAuthn browser API with a server library. Your server sends a random challenge, the browser calls navigator.credentials.create() to make a key pair, and you store the public key. To sign in, the browser signs a new challenge with navigator.credentials.get() and your server checks the signature. With loginos, passkeys live on loginos.io and need no WebAuthn code on your domains.7 min read
- PasskeysIs there a service that handles passkey login for me?Yes. Passkey add-ons such as Corbado Connect, the Hanko Passkey API and Bitwarden Passwordless.dev add passkeys to the login you already have, while full auth providers such as Auth0, Clerk, Stytch, Descope and Amazon Cognito run all of sign-in, passkeys included. Compare fallbacks, recovery, domains and data export first. With loginos, your app handles one OpenID Connect callback, and the ID token says whether a passkey was used.6 min read
- PasskeysHow do I implement passwordless login in a web app?You implement passwordless login by replacing the password with something the person has: a passkey, a one-time link or code sent by email or SMS, or an existing Google or Apple account. Passkeys are the only common method that resists phishing, so offer them first and keep an email link or code as the fallback. With loginos you get passkeys with a password fallback, but no email or social login.7 min read