From Passwords to Passkeys - Lessons from Building a Passwordless Login Flow
Passwords have been the backbone of online authentication for decades, but they've always been a pain point. They'e easy to forget, hard to manage securely, and vulnerable to phishing. Developers have tried to patch the problem with password managers, one-time codes, and two-factor prompts. Each step improved things, but none solved the root problem: the password itself.
Recently I built and deployed a passwordless login flow using passkeys, powered by the WebAuthn standard. My implementation was in Go, but the lessons apply across stacks. In this post I’ll explain what passkeys are, how the flow works, and what I learned along the way.
What are Passkeys and Webauthn?
WebAuthn (short for Web Authentication) is a W3C standard that allows browsers and devices to handle secure, public-key based authentication.
Passkeys are the user-friendly version of WebAuthn credentials. They’re stored by the operating system or browser, synced across devices, and can be unlocked with biometrics, device PINs, or hardware keys.
Instead of storing and comparing passwords, the server stores a public key. The user's device keeps the private key secure. Authentication is a challenge–response exchange, not a secret the user types in.
For developers, this means less responsibility for password handling and a much stronger defense against phishing. For users, it feels seamless: "use Face ID or Touch ID to sign in."
Why I tried it?
I wanted to add a login option that was both secure and simple. With libraries available in most major languages, I was curious to see how practical it would be to build a real passkey authentication flow.
My goals were straightforward:
- Let users register with a passkey.
- Let them authenticate without needing a password.
- Keep the flow minimal but production-ready.
Building the Flow
At a high level, the passkey flow looks like this:
Registration
- The server generates a challenge and sends it to the browser.
- The browser calls navigator.credentials.create() and prompts the user to create a new passkey.
- The resulting credential (public key + metadata) is sent back to the server and stored.
Authentication
- The server generates a new challenge.
- The browser calls navigator.credentials.get() and the user confirms with Face ID, Touch ID, or PIN.
- The signed response is verified against the stored public key. If it checks out, the login succeeds.
In Go, I used the go-webauthn/webauthn library, which handles much of the specification complexity. But the flow is the same no matter what stack you’re on.
Simplified Example (Go)
Registration
// Begin registration
options, sessionData, _ := webAuthn.BeginRegistration(user)
storeSession("registration", sessionData)
// Send `options` to the browser for navigator.credentials.create()
when the browser responds:
// Finish registration
credential, _ := webAuthn.FinishRegistration(user, sessionData, r)
saveCredentialToDB(user, credential)
Authentication
// Begin authentication
options, sessionData, _ := webAuthn.BeginLogin(user)
storeSession("login", sessionData)
// Send `options` to the browser for navigator.credentials.get()
on response:
// Finish authentication
_, err := webAuthn.FinishLogin(user, sessionData, r)
if err == nil {
log.Println("Login successful")
}
What I Learned
- The spec is heavy, but libraries help. The raw WebAuthn spec is detailed and intimidating. Using a well-tested library takes care of most of that.
- Browser UX is surprisingly smooth. Chrome, Safari, and Firefox all handle passkeys well. Prompts feel native and fast.
- Testing is tricky. Simulating cross-device scenarios (e.g. registering on iPhone, logging in on laptop) can be awkward. Platform sync (iCloud Keychain, Google Password Manager) helps.
- Multiple credentials are essential. Supporting more than one passkey per user (for different devices) turned out to be a must-have feature, and it’s already part of my implementation.
- Fallbacks are important. Passkeys are powerful, but not every user is ready. I kept email + password as a backup for now.
Resources
- WebAuthn Guide - a friendly introduction
- go-webauthn/webauthn Library
- FIDO Alliance
Passkeys aren't science fiction anymore. I've deployed a working passwordless login flow, and I suspect this will become the default way we sign in sooner than expected.