<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Andrii</title>
    <description>The latest articles on Forem by Andrii (@serafimsanvol).</description>
    <link>https://forem.com/serafimsanvol</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F518005%2F4b4e9d20-7e5f-4ee7-9841-e76a3df5ea59.jpeg</url>
      <title>Forem: Andrii</title>
      <link>https://forem.com/serafimsanvol</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/serafimsanvol"/>
    <language>en</language>
    <item>
      <title>Everyone should see this article!</title>
      <dc:creator>Andrii</dc:creator>
      <pubDate>Thu, 30 Oct 2025 13:34:12 +0000</pubDate>
      <link>https://forem.com/serafimsanvol/everyone-should-see-this-1bc6</link>
      <guid>https://forem.com/serafimsanvol/everyone-should-see-this-1bc6</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/copyleftdev/dear-scammers-you-picked-the-wrong-developer-46m6" class="crayons-story__hidden-navigation-link"&gt;🎯 Dear Scammers: You Picked the Wrong Developer&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/copyleftdev" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F965504%2F11378d5e-5ab1-4d72-9f9e-f4db74d77a54.png" alt="copyleftdev profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/copyleftdev" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Mr. 0x1
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Mr. 0x1
                
              
              &lt;div id="story-author-preview-content-2973517" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/copyleftdev" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F965504%2F11378d5e-5ab1-4d72-9f9e-f4db74d77a54.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Mr. 0x1&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/copyleftdev/dear-scammers-you-picked-the-wrong-developer-46m6" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 29 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/copyleftdev/dear-scammers-you-picked-the-wrong-developer-46m6" id="article-link-2973517"&gt;
          🎯 Dear Scammers: You Picked the Wrong Developer
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/copyleftdev/dear-scammers-you-picked-the-wrong-developer-46m6" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;45&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/copyleftdev/dear-scammers-you-picked-the-wrong-developer-46m6#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              10&lt;span class="hidden s:inline"&gt; comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>webdev</category>
      <category>cybersecurity</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Webauthn authentication with React and Nest.js</title>
      <dc:creator>Andrii</dc:creator>
      <pubDate>Sun, 05 Oct 2025 11:06:17 +0000</pubDate>
      <link>https://forem.com/serafimsanvol/webauthn-authentication-with-react-and-nestjs-49bp</link>
      <guid>https://forem.com/serafimsanvol/webauthn-authentication-with-react-and-nestjs-49bp</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;WebAuthn is a relatively new way of authentication for websites that offers better security than traditional authentication methods like password-based authentication. &lt;/p&gt;

&lt;p&gt;Passwords have many weaknesses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They can be guessable (birth date, dog name, etc.)&lt;/li&gt;
&lt;li&gt;Most people use weak passwords, which are vulnerable to dictionary attacks&lt;/li&gt;
&lt;li&gt;It’s possible to get them via phishing, darknet, or even in some cases if passwords are not hashed in the database as raw text.&lt;/li&gt;
&lt;li&gt;And many more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When using WebAuthn, all of the enumerated problems above are gone. Hackers can’t:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guess the password, because there’s no password&lt;/li&gt;
&lt;li&gt;Use phishing because each passkey is connected strictly to a single website&lt;/li&gt;
&lt;li&gt;Use database leaks for authorization purposes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This level of security exists because of the WebAuthn standard design. Shortly, your device creates a passkey, which is stored on your device, and you confirm creating that passkey for the website. The server verifies user identity, and here it is, the passkey created, and the user can be authenticated. Next time the user needs to log in to your website, he can just select the previously created passkey from the prompt, the server validates that the passkey exists and belongs to the current user, and if so user is authenticated. That’s a super brief explanation.&lt;/p&gt;

&lt;p&gt;To better understand it, you can check these interactive demos:&lt;br&gt;
&lt;a href="https://www.webauthn.me/" rel="noopener noreferrer"&gt;https://www.webauthn.me/&lt;/a&gt;&lt;br&gt;
&lt;a href="https://webauthn.io/" rel="noopener noreferrer"&gt;https://webauthn.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A more detailed explanation of the exact steps will be provided in the next sections.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pre-requisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 (or higher) installed&lt;/li&gt;
&lt;li&gt;Git installed&lt;/li&gt;
&lt;li&gt;familiarity with React, Nest.js, and Prisma&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  FE Setup and overview
&lt;/h2&gt;

&lt;p&gt;I’ve prepared some boilerplate code so that you can focus specifically on passkey implementation.&lt;/p&gt;

&lt;p&gt;To clone the FE part of the boilerplate, use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/serafimsanvol/webauthn-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then don’t forget to install dependencies with your preferred package manager, like&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, it’s just a basic React app with React Router.&lt;/p&gt;

&lt;p&gt;We have a few routes here&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sign up page (/) - where we fill user email to confirm&lt;/li&gt;
&lt;li&gt;sign in (/sign-in) - sign in with email + passkey combination&lt;/li&gt;
&lt;li&gt;protected (/protected) - route that will be hidden behind login&lt;/li&gt;
&lt;li&gt;passkey sign up (/passkey) - here we verify the token from email, create a session for the user, and request to create a passkey.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It has already implemented session-based authentication and email verification routes. All API call functions are located at app/api/auth/index.ts. Take a minute to take a look.&lt;/p&gt;

&lt;p&gt;To start in dev mode, use the command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server started on &lt;a href="http://localhost:5173/" rel="noopener noreferrer"&gt;http://localhost:5173/&lt;/a&gt;, where you can click through existing pages (links to all of them you can find in the header). Some functionality won’t work, like logging out before we start our backend, so let’s do it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsya4dj73mogzio2ka8im.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsya4dj73mogzio2ka8im.png" alt="Screenshot of the home page" width="512" height="244"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  BE Setup and overview
&lt;/h2&gt;

&lt;p&gt;To clone the BE part of the boilerplate, use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/serafimsanvol/webauthn-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don’t forget to install dependencies with your preferred package manager&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create .env file in the root folder and paste data from .env.sample for now. We’re using SQLite in this sample for simplicity.&lt;/p&gt;

&lt;p&gt;Let’s take a quick look at the existing code. Starting with the schema.prisma file.&lt;/p&gt;

&lt;p&gt;Here you can notice 3 models: &lt;br&gt;
User - basic user data, including id, email, and whether the user's email is verified. &lt;br&gt;
AuthTokens - table for short-lived tokens. In our case, we would use them to confirm that the user is actually the owner of the email.&lt;br&gt;
Session - our table for long-lived tokens that the user has for authentication, an alternative to JWT (JSON Web Tokens).&lt;/p&gt;

&lt;p&gt;prisma module - basic setup of Prisma and Prisma service. Global module to use in all our modules.&lt;/p&gt;

&lt;p&gt;users - module for operations with users, right now we only have UsersService with 2 methods - findUserByEmail and createUser.&lt;/p&gt;

&lt;p&gt;emails - module for sending emails. EmailsService contains a single method, sendVerificationEmail.&lt;br&gt;
For this method to work, you need to sign up for an account on resend - &lt;a href="https://resend.com/" rel="noopener noreferrer"&gt;https://resend.com/&lt;/a&gt; &lt;br&gt;
and create API_KEY.&lt;br&gt;
Paste the key into the .env file as RESEND_API_KEY&lt;br&gt;
and your email with which you signed up on resend as RECIPIENT_EMAIL in the .env file. &lt;/p&gt;

&lt;p&gt;and finally the auth module.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note about the authentication system:&lt;/strong&gt; it’s not a fully-featured, and is just bare-bones. If you want to use it as a template, you need to implement at least Sliding Session / Sliding Expiration and Rotating Refresh Tokens.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before starting, you need to apply migrations to your database. This can be done using&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx prisma migrate dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To start the dev server, run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run dev 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Passkey authentication overview
&lt;/h2&gt;

&lt;p&gt;On &lt;a href="https://www.webauthn.me/" rel="noopener noreferrer"&gt;https://www.webauthn.me/&lt;/a&gt;, you can find a great, interactive overview of the steps required for passkey sign-up. Let’s break them into steps here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user has to provide a username or email. In our case, that would be the email of the user after it is verified via an authentication token&lt;/li&gt;
&lt;li&gt;The browser sends a request to the server to generate the signup options&lt;/li&gt;
&lt;li&gt;The server responds with generated options&lt;/li&gt;
&lt;li&gt;In the browser, we’re asking the user to create a new passkey&lt;/li&gt;
&lt;li&gt;After that, we’re sending data about the user's passkey to the server to verify it and confirm successful sign-up.&lt;/li&gt;
&lt;li&gt;We’ve done it!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For sign-in steps would be similar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;user providing username/email&lt;/li&gt;
&lt;li&gt;We’re sending a request to the server to generate sign-in options&lt;/li&gt;
&lt;li&gt;With generated data from the server, we’re initiating user interaction to provide a relevant passkey via browser/native UI&lt;/li&gt;
&lt;li&gt;Sending data to the server to confirm&lt;/li&gt;
&lt;li&gt;Now we’re authenticated!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Additional data structures
&lt;/h2&gt;

&lt;p&gt;Let’s start with DB changes. We need to create db model for Passkeys. It would look like this:&lt;br&gt;
This is based on recommended code from &lt;a href="https://simplewebauthn.dev/docs/packages/server#additional-data-structures" rel="noopener noreferrer"&gt;simplewebauthn&lt;/a&gt;, a library we would use for both FE and BE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum CredentialDeviceType {
 singleDevice
 multiDevice
}

model Passkey {
 id             String               @id @default(cuid())
 credentialId   String               @unique
 userId         String
 publicKey      Bytes
 webauthnUserID String
 counter        Int
 deviceType     CredentialDeviceType
 backedUp       Boolean @default(false)
 transports     String
 deleted        Boolean @default(false)


 createdAt DateTime @default(now())
 user      User     @relation(fields: [userId], references: [id])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, we would need a model for storing challenges&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;enum WebauthChallengeType {
 REGISTRATION
 AUTHENTICATION
}


model WebauthChallenge {
 id        String               @id @default(cuid())
 userId    String
 challenge String               @unique
 createdAt DateTime             @default(now())
 expiresAt DateTime
 isUsed    Boolean              @default(false)
 type      WebauthChallengeType


 user User @relation(fields: [userId], references: [id])
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And don’t forget to update the User model&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model User {
 id        String   @id @default(cuid())
 email     String   @unique
 verified  Boolean  @default(false)
 createdAt DateTime @default(now())
 updatedAt DateTime @updatedAt


 authTokens AuthToken[]
 sessions   Session[]
 webauthChallenges WebauthChallenge[]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;let’s run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx prisma migrate dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to name, run, and apply migration&lt;br&gt;
And we are ready to start implementing some routes&lt;/p&gt;
&lt;h2&gt;
  
  
  Generate registration options
&lt;/h2&gt;

&lt;p&gt;Use nest commands to setup module&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nest g module passkeys --no-spec
nest g service passkeys --no-spec   
nest g controller passkeys --no-spec 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s create file constants.ts in passkeys folder with some necessary config data&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const RP_NAME = "Sample project name";

export const RP_ID = "localhost"; // Change this to your actual RP ID
/**
* The origin URL for the RP, used in passkey registration and authentication.
*/
export const ORIGIN = `http://${RP_ID}:5173`; // Adjust the port if necessary

export const WEBAUTHN_TIMEOUT = 60000; // 60 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This setup is valid only for local development; don’t forget to update values before deployment.&lt;/p&gt;

&lt;p&gt;Now we need to add a dependency that would help us with authentication&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm install @simplewebauthn/server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Signup options BE
&lt;/h2&gt;

&lt;p&gt;Let’s add to the passkey controller a route to generate signup options. We’re finding based on the token if a session for the user exists, retrieving the user, and calling generateSignupOptions, which we’re going to implement right now&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Controller, Get, UnauthorizedException } from "@nestjs/common";
import { Token } from "src/common/decorators/token/token.decorator";
import { PrismaService } from "src/prisma/prisma.service";
import { PasskeysService } from "./passkeys.service";


@Controller("passkeys")
export class PasskeysController {
 constructor(
   private readonly prisma: PrismaService,
   private readonly passkeysService: PasskeysService,
 ) {}


 @Get("signup-options")
 async generateSignupOptions(@Token() token: string) {
   const session = await this.prisma.session.findUnique({
     where: {
       token: token.split(".")[0],
     },
     include: {
       user: {
         select: {
           id: true,
           email: true,
           verified: true,
           createdAt: true,
           updatedAt: true,
         },
       },
     },
   });


   if (!session?.user) throw new UnauthorizedException("Session not found");


   return this.passkeysService.generateSignupOptions({ user: session.user });
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the passkey service create method, generateSignupOptions. Inside, we’re generating a sign-up option based on user info and default params, then creating a challenge in the database so we can confirm that this is the actual challenge that was created by our backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Injectable } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { User, WebauthChallengeType } from "@prisma/client";
import { generateRegistrationOptions } from "@simplewebauthn/server";
import { RP_ID, RP_NAME, WEBAUTHN_TIMEOUT } from "./constants";


@Injectable()
export class PasskeysService {
 constructor(private readonly prisma: PrismaService) {}


 async generateSignupOptions({ user }: { user: User }) {
   const options: PublicKeyCredentialCreationOptionsJSON =
     await generateRegistrationOptions({
       rpName: RP_NAME,
       rpID: RP_ID,
       userName: user.email,
       attestationType: "none",
       timeout: WEBAUTHN_TIMEOUT,
       excludeCredentials: [],
       authenticatorSelection: {
         residentKey: "preferred",
         userVerification: "preferred",
         authenticatorAttachment: "platform",
       },
       supportedAlgorithmIDs: [-7, -257], // ES256 and RS256
     });


   await this.prisma.webauthChallenge.create({
     data: {
       userId: user.id,
       challenge: options.challenge,
       expiresAt: new Date(Date.now() + WEBAUTHN_TIMEOUT),
       isUsed: false,
       type: WebauthChallengeType.REGISTRATION,
     },
   });


   return options;
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s implement verify signup on BE&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify signup on BE
&lt;/h2&gt;

&lt;p&gt;Let's update a passkey controller&lt;/p&gt;

&lt;p&gt;We should receive a response in JSON format from the user on the client, and a challenge to verify it.&lt;/p&gt;

&lt;p&gt;Update imports&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {
 Body,
 Controller,
 Get,
 Post,
 UnauthorizedException,
} from "@nestjs/common";
import { Token } from "src/common/decorators/token/token.decorator";
import { PrismaService } from "src/prisma/prisma.service";
import { PasskeysService } from "./passkeys.service";
import { RegistrationResponseJSON } from "@simplewebauthn/server";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;add a new route handler to controller&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; @Post("verify-signup")
 async verifySignup(
   @Body("response") clientResponse: RegistrationResponseJSON,
   @Body("challenge") challenge: string,
   @Token() token: string,
 ) {
   return this.passkeysService.verifySignup(
     clientResponse,
     challenge,
     token.split(".")[1],
   );
 }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;verifySignup in the passkeys service should look like this. Checking if the sessionId is valid. Then check if the challenges match. Verifying registration response, and if verified, creating in db passkey.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; async verifySignup(
   clientResponse: RegistrationResponseJSON,
   challenge: string,
   sessionId: string,
 ) {
   try {
     const session = await this.prisma.session.findFirst({
       where: { id: sessionId },
     });


     if (!session) throw new UnauthorizedException("Session not found");


     const currentChallenge = await this.prisma.webauthChallenge.findFirst({
       where: {
         challenge,
         type: WebauthChallengeType.REGISTRATION,
         expiresAt: {
           gte: new Date(),
         },
         userId: session?.userId,
         isUsed: false,
       },
     });


     if (!currentChallenge) throw new Error("Invalid or expired challenge");


     const verification = await verifyRegistrationResponse({
       expectedOrigin: ORIGIN,
       expectedRPID: RP_ID,
       response: clientResponse,
       expectedChallenge: challenge,
       requireUserVerification: false,
     });


     const { verified, registrationInfo } = verification;


     if (!registrationInfo || !verified)
       throw new UnauthorizedException("Verification failed");


     const { credential } = registrationInfo;


     const { response } = clientResponse;


     const transports = response.transports
       ? response.transports.join(", ")
       : "";


     await this.prisma.passkey.create({
       data: {
         webauthnUserID: credential.id,
         publicKey: credential.publicKey,
         counter: credential.counter,
         transports,
         userId: currentChallenge.userId,
         credentialId: Buffer.from(credential.id).toString("base64"),
         deviceType: registrationInfo.credentialDeviceType,
       },
     });


     return {
       verified: verification.verified,
     };
   } catch (error) {
     console.error("Verification error:", error);
     throw error;
   }
 }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’ve finished the signup flow on BE. Let’s implement FE and check it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  FE-part of the sign-up
&lt;/h2&gt;

&lt;p&gt;Firstly, let’s add a new dependency&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pnpm add @simplewebauthn/browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s add api call for our created routes in api/auth/index.ts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { ORIGIN } from "../constants";
import type { RegistrationResponseJSON } from "@simplewebauthn/browser";


export const getSignupOptions = async () =&amp;gt; {
 return fetch(`${ORIGIN}/passkeys/signup-options`, {
   credentials: "include",
 }).then((res) =&amp;gt; res.json());
};


export const verifySignup = async (
 challenge: string,
 response: RegistrationResponseJSON
) =&amp;gt; {
 return fetch(`${ORIGIN}/passkeys/verify-signup`, {
   method: "POST",
   credentials: "include",
   headers: {
     "Content-Type": "application/json",
   },
   body: JSON.stringify({
     challenge,
     response,
   }),
 });
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add on click, generate sign-up options, then verify them (add this to app/routes/passkey.tsx)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useLoaderData, useNavigate } from "react-router";
import { startRegistration } from "@simplewebauthn/browser";
import type { PublicKeyCredentialCreationOptionsJSON } from "@simplewebauthn/browser";
import { getSignupOptions, verifyEmail, verifySignup } from "~/api/auth";


export async function clientLoader() {
 const urlParams = new URLSearchParams(window.location.search);
 const token = urlParams.get("token");


 const response = await verifyEmail(token || "");


 if (response.status &amp;gt;= 400) {
   return { message: "" };
 }


 return { message: "Email verified successfully" };
}


const Passkey = () =&amp;gt; {
 const { message } = useLoaderData&amp;lt;typeof clientLoader&amp;gt;();
 const navigate = useNavigate();
 return (
   &amp;lt;div className="flex flex-col items-center justify-center min-h-[100%]"&amp;gt;
     {message &amp;amp;&amp;amp; &amp;lt;p className="text-green-500"&amp;gt;{message}&amp;lt;/p&amp;gt;}
     &amp;lt;p className="mb-4"&amp;gt;Sign up with Passkey&amp;lt;/p&amp;gt;
     &amp;lt;button
       onClick={async () =&amp;gt; {
         const optionsJSON: PublicKeyCredentialCreationOptionsJSON =
           await getSignupOptions();


         const challenge = optionsJSON.challenge;


         const result = await startRegistration({
           optionsJSON,
         });


         await verifySignup(challenge, result);


         navigate("/protected");
       }}
       className="bg-blue-500 py-2 px-4 text-white p-2 rounded-md cursor-pointer"
     &amp;gt;
       Signup
     &amp;lt;/button&amp;gt;
   &amp;lt;/div&amp;gt;
 );
};


export default Passkey;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, let’s finish the users/me path (on BE).&lt;/p&gt;

&lt;p&gt;It should look like this now. We should just add a passkey check, so it’s finished&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {
 Controller,
 ForbiddenException,
 Get,
 UnauthorizedException,
} from "@nestjs/common";
import { Token } from "src/common/decorators/token/token.decorator";
import { PrismaService } from "src/prisma/prisma.service";
import { User } from "@prisma/client";
@Controller("users")
export class UsersController {
 constructor(private readonly prisma: PrismaService) {}
 @Get("me")
 async getCurrentUser(@Token() token: string): Promise&amp;lt;User&amp;gt; {
   // move to guards all of the checks
   if (!token) throw new UnauthorizedException("No session token found");


   const session = await this.prisma.session.findUnique({
     where: {
       token: token.split(".")[0],
     },
     include: {
       user: true,
     },
   });


   if (!session) throw new UnauthorizedException("Session not found");


   const passKey = await this.prisma.passkey.findFirst({
     where: {
       userId: session.user.id,
       deleted: false,
     },
   });


   if (!passKey) throw new ForbiddenException("No passkey found for the user");


   return session.user;
 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s start our FE and BE by using in both folders command&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pnpm run dev&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Sign up a user by writing an email and sending a verification email on the home page &lt;a href="http://localhost:5173/" rel="noopener noreferrer"&gt;http://localhost:5173/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4604b44mnuixk9qquz4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4604b44mnuixk9qquz4.png" alt="Home page with send verification email form" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, you should see a success message&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30odnqng38mb6vat605s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F30odnqng38mb6vat605s.png" alt="Verification email sent page" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check your email.If you can’t find an email like this, check your spam folder or logs on the backend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6led3j7nire5mwj7vo82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6led3j7nire5mwj7vo82.png" alt="Screenshot of email with link to verify sign up" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the link from the email, you should be redirected to the  passkey page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcvptf8pzufx3nf2pzdr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcvptf8pzufx3nf2pzdr.png" alt="sign up with passkey page" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Right now user is authenticated using a cookie token, and corresponding records are created in the database.&lt;/p&gt;

&lt;p&gt;Then click on the signup button to proceed. You’d face a browser modal&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4l0na5dovkm0r3qzbo0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff4l0na5dovkm0r3qzbo0.png" alt="Create a passkey prompt" width="450" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If created successfully, you’d be redirected to the protected page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fls3x4iggyapavwh1nqjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fls3x4iggyapavwh1nqjh.png" alt="screnshot of protected page that shows after successfull sign up" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s it, you’ve created an account and a corresponding passkey!&lt;/p&gt;

&lt;h2&gt;
  
  
  Now, let’s implement sign-in
&lt;/h2&gt;

&lt;p&gt;Before doing that, don’t forget to log out&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcepldllsqcj5g0u15xv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzcepldllsqcj5g0u15xv.png" alt="screenshot of protected page with cursor hovered on logout button" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let’s create a route to generate sign-in options. We accept email from the user and fetch all relevant passkeys for this user. Create a new route in the passkeys controller&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Post("signin-options")
 async generateSigninOptions(@Body("email") email: string) {
   const user = await this.prisma.user.findUnique({
     where: { email },
     include: {
       Passkey: true,
     },
   });


   const passkeys = user?.Passkey || [];


   return this.passkeysService.generateSigninOptions(passkeys, user?.id);
 }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let’s implement the generateSigninOptions method in the passkeys service.&lt;/p&gt;

&lt;p&gt;Imports update&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {
 generateRegistrationOptions,
 RegistrationResponseJSON,
 verifyRegistrationResponse,
 PublicKeyCredentialRequestOptionsJSON,
 generateAuthenticationOptions,
} from "@simplewebauthn/server";
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PrismaService } from "src/prisma/prisma.service";
import { User, WebauthChallengeType, Passkey } from "@prisma/client";
import { ORIGIN, RP_ID, RP_NAME, WEBAUTHN_TIMEOUT } from "./constants";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And code update&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; async generateSigninOptions(userPasskeys: Passkey[], userId?: string) {
   const options: PublicKeyCredentialRequestOptionsJSON =
     await generateAuthenticationOptions({
       rpID: RP_ID,
       userVerification: "required",
       // Require users to use a previously-registered authenticator
       allowCredentials: userPasskeys.map((passkey) =&amp;gt; ({
         type: "public-key",
         // toggle here
         id: passkey.webauthnUserID,
         transports: passkey.transports.split(
           ", ",
         ) as AuthenticatorTransport[],
       })),
     });


   if (!userId) return options;


   await this.prisma.webauthChallenge.create({
     data: {
       challenge: options.challenge,
       expiresAt: new Date(Date.now() + (options.timeout || WEBAUTHN_TIMEOUT)), // Default to 60 seconds if timeout is not set
       isUsed: false,
       type: WebauthChallengeType.AUTHENTICATION,
       userId,
     },
   });


   return options;
 }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’re requiring userVerification here and passing already created passkeys as allowed credentials. After that, create in the DB data about the challenge and authentication type.&lt;/p&gt;

&lt;p&gt;Now, let’s implement the sign-in verification route (in the passkeys controller)&lt;/p&gt;

&lt;p&gt;But before that let’s import AuthModule to Passkeys module&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Module } from "@nestjs/common";
import { PasskeysService } from "./passkeys.service";
import { PasskeysController } from "./passkeys.controller";
import { AuthModule } from "src/auth/auth.module";


@Module({
 providers: [PasskeysService],
 controllers: [PasskeysController],
 imports: [AuthModule],
})
export class PasskeysModule {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update passkeys controller constructor&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;constructor(
   private readonly prisma: PrismaService,
   private readonly passkeysService: PasskeysService,
   private readonly authService: AuthService,
 ) {}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then update imports&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {
 Body,
 Controller,
 Get,
 Post,
 Res,
 UnauthorizedException,
} from "@nestjs/common";
import {
 RegistrationResponseJSON,
 AuthenticationResponseJSON,
} from "@simplewebauthn/server";
import { Response } from "express";
import { PrismaService } from "src/prisma/prisma.service";
import { PasskeysService } from "./passkeys.service";
import { Token } from "src/common/decorators/token/token.decorator";
import { AuthService } from "src/auth/auth.service";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a route where we call verifySignin from the service, and on success, set a cookie and send a success message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; @Post("verify-signin")
 async verifySignin(
   @Body("response") clientResponse: AuthenticationResponseJSON,
   @Body("challenge") challenge: string,
   @Res({ passthrough: true }) response: Response,
 ) {
   const { verified, userId } = await this.passkeysService.verifySignin(
     clientResponse,
     challenge,
   );


   if (!verified) throw new UnauthorizedException("Verification failed");


   const sessionToken = await this.authService.generateSessionToken(userId);


   response.cookie("token", `${sessionToken.token}.${sessionToken.id}`, {
     httpOnly: true,
     secure: false,
     // lax for development, none with secure for production
     sameSite: "lax",
     path: "/",
   });


   return {
     message: "Signin successful",
   };
 }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the method in the passkey service should look like this&lt;br&gt;
Imports&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import {
 generateRegistrationOptions,
 RegistrationResponseJSON,
 verifyRegistrationResponse,
 PublicKeyCredentialRequestOptionsJSON,
 generateAuthenticationOptions,
 verifyAuthenticationResponse,
 AuthenticationResponseJSON,
} from "@simplewebauthn/server";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And code where we check if the challenge is valid and belongs to the user. Then, check do we have a passkey with the ID from the user response on the client and verify all data. On success, invalidating the  corresponding webauthChallenge&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async verifySignin(
   clientResponse: AuthenticationResponseJSON,
   challenge: string,
 ): Promise&amp;lt;{ verified: boolean; userId: string }&amp;gt; {
   try {
     const currentChallenge = await this.prisma.webauthChallenge.findFirst({
       where: {
         challenge,
         type: WebauthChallengeType.AUTHENTICATION,
         expiresAt: {
           gte: new Date(),
         },
         isUsed: false,
       },
       include: {
         user: true, // Include user to get userId
       },
     });


     if (!currentChallenge)
       // TODO think about more specific error
       throw new UnauthorizedException("Invalid or expired challenge");


     const passKey = await this.prisma.passkey.findFirst({
       where: { webauthnUserID: clientResponse.id },
     });


     if (!passKey) {
       throw new UnauthorizedException("Passkey not found");
     }


     const verification = await verifyAuthenticationResponse({
       response: clientResponse,
       expectedChallenge: currentChallenge.challenge,
       expectedOrigin: ORIGIN,
       expectedRPID: RP_ID,
       requireUserVerification: true,
       credential: {
         id: passKey.webauthnUserID,
         publicKey: new Uint8Array(passKey.publicKey),
         counter: passKey.counter as unknown as number,
         transports: passKey.transports.split(
           ", ",
         ) as AuthenticatorTransport[],
       },
     });


     if (!verification.verified) {
       throw new UnauthorizedException("Verification failed");
     }


     await this.prisma.webauthChallenge.update({
       where: { id: currentChallenge.id },
       data: { isUsed: true },
     });


     return {
       verified: verification.verified,
       userId: currentChallenge.userId,
     };
   } catch (error) {
     console.error("Verification error:", error);
     throw error;
   }
 }

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it on BE. Let’s move to FE&lt;/p&gt;

&lt;h2&gt;
  
  
  FE sign-in
&lt;/h2&gt;

&lt;p&gt;We need to add fetches to the created routes.&lt;/p&gt;

&lt;p&gt;Imports update (to the api/auth/index.ts)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import type {
 RegistrationResponseJSON,
 AuthenticationResponseJSON,
 PublicKeyCredentialRequestOptionsJSON,
} from "@simplewebauthn/browser";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And code&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

export const getSigninOptions = async (email: string) =&amp;gt; {
 const optionsJSON: PublicKeyCredentialRequestOptionsJSON = await fetch(
   `${ORIGIN}/passkeys/signin-options`,
   {
     method: "POST",
     headers: {
       "Content-Type": "application/json",
     },
     body: JSON.stringify({ email }),
   }
 ).then((res) =&amp;gt; res.json());


 return optionsJSON;
};


export const verifySignin = async (
 result: AuthenticationResponseJSON,
 challenge: string
) =&amp;gt; {
 return fetch(`${ORIGIN}/passkeys/verify-signin`, {
   method: "POST",
   credentials: "include",
   headers: {
     "Content-Type": "application/json",
   },
   body: JSON.stringify({
     response: result,
     challenge: challenge,
   }),
 });
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now move to app/routes/signin.tsx&lt;/p&gt;

&lt;p&gt;Update imports&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { startAuthentication } from "@simplewebauthn/browser";
import { useForm, type SubmitHandler } from "react-hook-form";
import { redirect, useNavigate } from "react-router";
import { getSigninOptions, getUser, verifySignin } from "~/api/auth";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update the onSubmit handler with our fetches&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const navigate = useNavigate();
 const onSubmit: SubmitHandler&amp;lt;SigninFormData&amp;gt; = async (data) =&amp;gt; {
   const email = data.email;
   const optionsJSON = await getSigninOptions(email);


   const result = await startAuthentication({
     optionsJSON: optionsJSON,
   });


   const response = await verifySignin(result, optionsJSON.challenge);


   if (response.status &amp;lt; 400) return navigate("/protected");
 };

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now go to the /signin page, write your email, and click sign in&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhisj4324k966ftm3mqd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhisj4324k966ftm3mqd.png" alt="sign in page with input for email and sign in button" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’d get a verification prompt Touch ID/password, depends on your machine&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59oey61z010m0uaqia6q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F59oey61z010m0uaqia6q.png" alt="Enter your PIN prompt" width="452" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And after success, you’d be redirected to a protected page&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgckizskv05o4caqwocgv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgckizskv05o4caqwocgv.png" alt="protected page" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here it is, not-so-not-so-short sample of the implementation of WebAuthn.&lt;br&gt;
If you want to learn more about it, because there are a lot of tweaks and caveats, check out:&lt;br&gt;
&lt;a href="https://webauthn.guide/" rel="noopener noreferrer"&gt;https://webauthn.guide/&lt;/a&gt;&lt;br&gt;
Also, many thanks to &lt;a href="https://simplewebauthn.dev/" rel="noopener noreferrer"&gt;https://simplewebauthn.dev/&lt;/a&gt; and its author &lt;a href="https://github.com/MasterKale" rel="noopener noreferrer"&gt;Matthew Miller&lt;/a&gt;&lt;br&gt;
 for libraries and documentation that made writing this article and implementing WebAuthn much easier  &lt;/p&gt;

</description>
      <category>react</category>
      <category>security</category>
      <category>fullstack</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Literally read YouTube videos</title>
      <dc:creator>Andrii</dc:creator>
      <pubDate>Sun, 14 Apr 2024 23:23:17 +0000</pubDate>
      <link>https://forem.com/serafimsanvol/literally-read-youtube-videos-55ha</link>
      <guid>https://forem.com/serafimsanvol/literally-read-youtube-videos-55ha</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/devteam/join-us-for-the-cloudflare-ai-challenge-3000-in-prizes-5f99"&gt;Cloudflare AI Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built a simple app that can help you consume your favorite YouTube videos even faster, by reading them or even summary about them! It's better to be something that fulfills you with &lt;a href="https://www.youtube.com/watch?v=bDeHFYBOhUo"&gt;knowledge&lt;/a&gt; or motivation and a direct list of actions to improve you and your coding journey like &lt;a href="https://www.youtube.com/watch?v=cuuXvVfORfk&amp;amp;list=WL&amp;amp;index=3"&gt;this&lt;/a&gt; &lt;br&gt;
But who am I to judge, I also like to procrastinate and watch some &lt;a href="https://www.youtube.com/watch?v=W86cTIoMv2U"&gt;cats&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbhniz9a18j155wwjviwz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbhniz9a18j155wwjviwz.gif" alt="Demo of interaction with site" width="600" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://baed95e9.cloudflare-ai-challenge-front-end.pages.dev/"&gt;Demo on Cloudflare Pages&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Code
&lt;/h2&gt;

&lt;p&gt;Backend: &lt;a href="https://github.com/serafimsanvol/summarizer"&gt;https://github.com/serafimsanvol/summarizer&lt;/a&gt;&lt;br&gt;
Frontend: &lt;a href="https://github.com/serafimsanvol/cloudflare-ai-challenge-front-end"&gt;https://github.com/serafimsanvol/cloudflare-ai-challenge-front-end&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;I wanted to participate in the "dev challenge" for already a long time but never wasn't able to plan my schedule properly and just dare to try. I'm still struggling with schedules (I'm writing this a few hours before the deadline). When I read about workers and what they can do I got the immediate idea to create an app for transcribing YouTube videos, summarizing them, and creating comics so it can be even easier to memorize important topics. I used "whisper" for audio transcribing and "bart-large-cnn" for summarizing. As you can notice I didn't add the comics generation part, but it can be the next version if I get feedback from users that it's quite a necessary feature. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple Models and/or Triple Task Types&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This project utilizes multiple models for creating text summaries about videos&lt;/p&gt;

</description>
      <category>cloudflarechallenge</category>
      <category>devchallenge</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
