<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu" -->

---
title: How to Build Passkey Authentication with WebAuthn,...
description: This tutorial builds a passwordless login system using WebAuthn passkeys with a NestJS backend and Next.js frontend. The backend stores users and passkeys in...
canonical: https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: How to Build Passkey Authentication with WebAuthn, Next.js and NestJS | daily.dev
og:description: This tutorial builds a passwordless login system using WebAuthn passkeys with a NestJS backend and Next.js frontend. The backend stores users and passkeys in...
og:url: https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu
og:image: https://api.daily.dev/og/posts/ECvXPQbBU.png
og:image:alt: How to Build Passkey Authentication with WebAuthn, Next.js and NestJS
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# How to Build Passkey Authentication with WebAuthn, Next.js and NestJS

**[freeCodeCamp](https://daily.dev/sources/freecodecamp)** · 32 min read · 3 upvotes · 1 comments

## Summary

This tutorial builds a passwordless login system using WebAuthn passkeys with a NestJS backend and Next.js frontend. The backend stores users and passkeys in PostgreSQL using TypeORM, while SimpleWebAuthn (version 14) handles the WebAuthn cryptography. It covers generating and verifying registration and authentication options, issuing JWTs on successful sign-in, and building sign-up/sign-in pages with the SimpleWebAuthn browser library. It also lists four security mistakes: double-encoding the credential ID, clearing challenges with 'undefined' instead of 'null', looking up credentials without checking the owner, and letting public registration add passkeys to existing accounts. Full source code is on GitHub.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://www.freecodecamp.org/news/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs>

## Questions this post answers

### Why does sign-in fail to find a passkey I just registered with SimpleWebAuthn?

It likely happens because the credential ID got encoded twice. In SimpleWebAuthn version 14, credential.id is already a base64url string, so running it through Buffer.from(credential.id).toString('base64url') again produces a double-encoded value. Registration succeeds but sign-in can never match it, since the browser sends back the original, singly-encoded ID. Store credential.id as-is.

_Debugging WebAuthn credential mismatches gets easier with implementation details like this surfaced on daily.dev._

### How do I prevent an attacker from registering their own passkey on someone else's account in WebAuthn?

Reject passkey registration for any email that already has an authenticator registered, since the registration endpoint is public and unauthenticated. Without that check, anyone who knows a victim's email can attach their own passkey and sign in as that user. Adding further passkeys to an existing account should be a separate, authenticated feature instead.

_Developers hardening passwordless auth flows can track security patterns like this via daily.dev._

### Should I use userVerification 'required' or 'preferred' with SimpleWebAuthn?

Use 'required' on both the registration/authentication options and the server-side verification call. If options request 'preferred', devices can skip the fingerprint or PIN check, but SimpleWebAuthn's verifyRegistrationResponse and verifyAuthenticationResponse default to requireUserVerification: true, causing confusing failures for users whose devices skipped verification. Keeping both settings aligned to 'required' avoids this mismatch.

_Teams choosing WebAuthn verification settings can compare these trade-offs through daily.dev._

## Community discussion

Top comments from developers on daily.dev.

**@pdfopsdev** · 0 upvotes

> the counter check bites people in a sneaky way too - if you're testing on an emulator the sign count often just stays at 0, so you won't see clone-detection false positives until real hardware hits prod. worth adding a note about [rp.id](http://rp.id) mismatches between localhost and your deployed domain, that one cost me a whole afternoon.

## Similar posts on daily.dev

- [Killing the Password: How to Add Passkeys to Your Rails 8 App](https://daily.dev/posts/killing-the-password-how-to-add-passkeys-to-your-rails-8-app-prvzndxhp) · Ruby Flow · 0 upvotes · 0 comments
- [Passwordless Authentication and Identity Theft: Can Passkeys Make the Web Safer?](https://daily.dev/posts/passwordless-authentication-and-identity-theft-can-passkeys-make-the-web-safer--ctnhfcv2s) · SitePoint · 0 upvotes · 0 comments
- [Simplified Authentication with Better Auth](https://daily.dev/posts/simplified-authentication-with-better-auth-weaj84lid) · Telerik · 0 upvotes · 0 comments
- [Passkeys in Production: A 2026 Developer Guide](https://daily.dev/posts/passkeys-in-production-a-2026-developer-guide-uprvdh0dz) · Alex CloudStar · 0 upvotes · 0 comments

---

Tags: [#authentication](https://daily.dev/tags/authentication), [#nextjs](https://daily.dev/tags/nextjs), [#nestjs](https://daily.dev/tags/nestjs), [#passkeys](https://daily.dev/tags/passkeys)

[View this post on daily.dev](https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"How to Build Passkey Authentication with WebAuthn, Next.js and NestJS","url":"https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu"},"datePublished":"2026-10-27T19:05:03.828Z","dateModified":"2026-10-27T19:21:03.315Z","description":"This tutorial builds a passwordless login system using WebAuthn passkeys with a NestJS backend and Next.js frontend. The backend stores users and passkeys in...","image":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/c99377d0ba84195e8e5c8192f8bb7765?_a=AQAEuop","thumbnailUrl":"https://media.daily.dev/image/upload/f_auto,q_auto/v1/posts/c99377d0ba84195e8e5c8192f8bb7765?_a=AQAEuop","isAccessibleForFree":true,"articleSection":"freeCodeCamp","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"freeCodeCamp","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1628412854/logos/freecodecamp","url":"https://daily.dev/sources/freecodecamp"},"commentCount":1,"discussionUrl":"https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":3},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":1}],"keywords":"authentication,nextjs,nestjs,passkeys","timeRequired":"PT32M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"freeCodeCamp","item":"https://daily.dev/sources/freecodecamp"},{"@type":"ListItem","position":3,"name":"How to Build Passkey Authentication with WebAuthn, Next.js and NestJS"}]}
{"@context":"https://schema.org","@type":"WebPage","@id":"https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu","comment":[{"@type":"Comment","text":"the counter check bites people in a sneaky way too - if you’re testing on an emulator the sign count often just stays at 0, so you won’t see clone-detection false positives until real hardware hits prod. worth adding a note about rp.id mismatches between localhost and your deployed domain, that one cost me a whole afternoon.","datePublished":"2026-10-28T18:01:08.289Z","url":"https://daily.dev/posts/ECvXPQbBU#c-kgdetSEJo","author":{"@type":"Person","name":"PDFops","url":"https://daily.dev/pdfopsdev","image":"https://media.daily.dev/image/upload/s---8isRBKc--/f_auto/v1782922291/avatars/avatar_orjMeK8QKaaVZwGq7ScPz?_a=BAMAMicg0"}}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/how-to-build-passkey-authentication-with-webauthn-next-js-and-nestjs-ecvxpqbbu#faq","mainEntity":[{"@type":"Question","name":"Why does sign-in fail to find a passkey I just registered with SimpleWebAuthn?","acceptedAnswer":{"@type":"Answer","text":"It likely happens because the credential ID got encoded twice. In SimpleWebAuthn version 14, credential.id is already a base64url string, so running it through Buffer.from(credential.id).toString('base64url') again produces a double-encoded value. Registration succeeds but sign-in can never match it, since the browser sends back the original, singly-encoded ID. Store credential.id as-is. Debugging WebAuthn credential mismatches gets easier with implementation details like this surfaced on daily.dev."}},{"@type":"Question","name":"How do I prevent an attacker from registering their own passkey on someone else's account in WebAuthn?","acceptedAnswer":{"@type":"Answer","text":"Reject passkey registration for any email that already has an authenticator registered, since the registration endpoint is public and unauthenticated. Without that check, anyone who knows a victim's email can attach their own passkey and sign in as that user. Adding further passkeys to an existing account should be a separate, authenticated feature instead. Developers hardening passwordless auth flows can track security patterns like this via daily.dev."}},{"@type":"Question","name":"Should I use userVerification 'required' or 'preferred' with SimpleWebAuthn?","acceptedAnswer":{"@type":"Answer","text":"Use 'required' on both the registration/authentication options and the server-side verification call. If options request 'preferred', devices can skip the fingerprint or PIN check, but SimpleWebAuthn's verifyRegistrationResponse and verifyAuthenticationResponse default to requireUserVerification: true, causing confusing failures for users whose devices skipped verification. Keeping both settings aligned to 'required' avoids this mismatch. Teams choosing WebAuthn verification settings can compare these trade-offs through daily.dev."}}]}
```

