---
url: 'https://www.corbado.com/blog/passkeys-nextauth'
title: 'Add Passkeys to your NextAuth.js Application'
description: 'This tutorial shows how to build a sample app with passkey authentication using Next.js as a web framework and NextAuth.js as authentication library.'
lang: 'en'
author: 'Nicolai'
date: '2023-09-06T00:00:00.000Z'
lastModified: '2026-04-15T06:00:22.290Z'
keywords: 'NextAuth, NextAuth.js, Auth.js'
category: 'Passkeys Implementation'
---

# Add Passkeys to your NextAuth.js Application

> ⚠️ **Archived tutorial - outdated integration**
>
> This article uses Corbado Complete, a discontinued product. Corbado now offers
> **[passkey observability and authentication for enterprise CIAM](https://www.corbado.com/features)**
>
> _The tutorial below is preserved for reference only and is no longer maintained._

## Key Facts

- **Passkey Association** is the core integration pattern: existing users authenticated
  via OAuth or password add a passkey to their account, then use it for future logins.
- The **corbado-passkey-associate-login web component** handles all WebAuthn complexity
  including challenge generation, challenge verification and Conditional UI automatically.
- NextAuth.js requires a **CredentialsProvider** configured with a custom authorize method
  to bridge Corbado passkey authentication into a standard NextAuth session.
- After passkey login, Corbado issues a **cbo_short_session cookie** containing a JWT that
  must be verified against Corbado's JWKS endpoint before NextAuth creates its own
  session.
- Corbado supports **passkey sign-up** with passkey login and email links as fallback,
  beyond the associate-only flow shown in this tutorial.

## 1. Introduction

In this blog post, we'll be walking through the process of building a sample application
with passkey authentication using [Next.js](https://www.corbado.com/blog/nextjs-passkeys) as a web framework and
NextAuth.js as authentication library. To make passkeys work, we plug
[Corbados Passkey Associate web component](https://www.corbado.com/enterprise) into the NextAuth.js
authentication process. It automatically connects to a passkeys backend and handles all
passkeys related aspects of authentication (e.g. generating and verifying the WebAuthn
challenge, handling [Conditional UI](https://www.corbado.com/glossary/conditional-ui), etc).

If you want to see the finished code, please have a look at our
[sample application GitHub repository](https://github.com/corbado/example-passkeys-nextauth).

Our final login page looks as follows (of course you can improve the styling but our focus
is on demonstrating the feasibility):

![Passkey sign in page](https://www.corbado.com/website-assets/652532835766cdfb4390a42b_nextauth_example_sign_in_1817885604.jpg)

## 2. NextAuth.js passkey project prerequisites

This tutorial assumes basic familiarity with [Next.js](https://www.corbado.com/blog/nextjs-passkeys),
NextAuth.js, TypeScript and HTML. Lets dive in!

## 3. Repository structure for NextAuth.js passkey project

Our [Next.js](https://www.corbado.com/blog/nextjs-passkeys) project with integrated NextAuth.js authentication is
structured like this.

Note that these are only the most important files.

```
├── pages
|   ├── api
|   |   ...
|   |   └── auth
|   |       ├── [...nextauth].ts   # Configuration of the authentication providers
|   |       └── associate.ts       # Endpoint which requests an association token from Corbado
|   |
|   ├── auth
|   |   ├── redirect.tsx            # Page where the user gets redirected to by Corbado after authentication
|   |   └── signin.tsx              # Sign in page which also contains the Corbado web component
|   |
|   └── index.tsx                   # Main page which is shown when no path is given
|
├── .env.local                      # Contains the environment variables
```

We use a concept called Passkey Association where an already authenticated user (e.g. via
OAuth, password or any other conventional method) can add a passkey to their account and
subsequently login with this passkey. This means that a user cannot directly register
using a passkey, but the user can add a passkey once they have created an account.

Creating a passkey account and using it to log into your app will work as shown here:

![Passkey creation and usage](https://www.corbado.com/website-assets/652532b2eeb1365b7d2fa08d_diagram_fc7caebed1.jpg)

The diagram show the following processes:

**Creating a passkey (1-4):**

1. The application requests an association token for a certain authenticated user from our
   backend

2. Our backend requests this association token from Corbado's backend

3. Corbado's backend returns the token to our backend

4. Our backend returns the token to our frontend and the frontend hands it to the
   <corbado-passkey-associate/> web component which will then display a Create passkey
   button

This enables the user to [create a passkey](https://www.corbado.com/blog/passkey-creation-best-practices) which
they can then use to login as follows:

**Login with a passkey (5-9):**

5. After the user has entered his email into the Corbado-passkey-associate- login
   webcomponent, the webcomponent gets a challenge from Corbado's backend

6. The webcomponent sends back the challenge response to the Corbado backend

7. If the authentication was successful the user is redirected to the redirectURL (You can
   configure it in the developer panel), if the authentication was unsuccessful you get a
   notification so you can display an error

8. In the redirectPage we tell NextAuth that someone has logged in with a passkey

9. We obtain the current user from Corbado's session and hand it to NextAuth which will
   initiate a session itself for that user and proceed as normal

## 4. Set up your Corbado account and project

Visit the [Corbado developer panel](https://app.corbado.com) to sign up and create your
account (youll see passkey sign-up in action here!).

![Corbado developer panel](https://www.corbado.com/website-assets/65253300342fc81383ef5f2b_dev_panel_login_63f42b8387.jpg)

In the appearing project wizard, select Web app as type of app and afterward select
whether you have existing users or not (which does not matter to us in this case because
only existing, authenticated users can add a passkey). Moreover, providing some details
regarding your frontend and backend tech stack as well as the main goal you want to
achieve with Corbado helps us to customize and smoothen your developer experience.

Next, we navigate to [Settings > General > URLs](#) and set the Application URL, Redirect
URL and [Relying Party](https://www.corbado.com/glossary/relying-party) ID to the following values (We will host
our app on port 3000):

![Corbado developer panel](https://www.corbado.com/website-assets/652533a7ddc87d3f91dc4e9b_dev_panel_url_settings_c1dc2299fe.jpg)

1. **Application URL:** Provide the URL where you embedded the web component,
   [http://localhost:3000/auth/signin](http://localhost:3000/auth/signin)
2. **Redirect URL:** Provide the URL your app should redirect to after successful
   authentication and which gets sent a short-term session cookie, here:
   [http://localhost:3000/auth/redirect](http://localhost:3000/auth/redirect)
3. **Relying Party ID:** Provide the domain (no protocol, no port and no path) where
   passkeys should be bound to, here: localhost

## 5. Set up Next.js with NextAuth.js

We download the NextAuth.js example from
[here](https://github.com/nextauthjs/next-auth-example) and run

```bash
npm install && npm run dev
```

The default page should appear:

![NextAuth.js example](https://www.corbado.com/website-assets/652533e77d5e0b055ebe9e62_nextauth_example_index_e7a74cc2ad.jpg)

When we click on Sign in, we are redirected to the NextAuth.js default authentication
page.

![NextAuth.js sign in page](https://www.corbado.com/website-assets/6525348ae0bdf155010898f3_nextauth_signin_page_ce89c7f159.jpg)

For demo purposes, we went to the
[Google developer console](https://console.cloud.google.com/) and generated credentials to
be able to use
[Google as an OAuth provider](https://www.balbooa.com/gridbox-documentation/how-to-get-google-client-id-and-client-secret).
The NextAuth.js example reads the OAuth credentials for all providers from the
environment, so we create a .env.local file and place our Google credentials there.

```dotenv
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=

// ...

GOOGLE_ID=
GOOGLE_SECRET=
```

Afterwards, we can sign in using our Google account!

![Index page](https://www.corbado.com/website-assets/652534bf3b18f3ca1eff3f82_index_already_registered_passkey_8dec3115f5.jpg)

Our initial project is now set up. Lets add passkey authentication!

## 6. Create passkey login page

### 6.1 Create custom login page for NextAuth.js providers

Apart from email magic links and OAuth providers, the UI of the NextAuth.js default
authentication page only supports custom inputs in the form of text fields as a separate
way of authentication. We, however, want to use passkeys, which cannot be used via text
fields. Therefore, we create our own login page under /pages/auth/signin.tsx with some
bootstrapping:

```tsx
"use client";
import type {
    GetServerSidePropsContext,
    InferGetServerSidePropsType,
    NextApiRequest,
    NextApiResponse,
} from "next";
import { getProviders, signIn } from "next-auth/react";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../api/auth/[...nextauth]";
import { useCallback, useEffect, useState } from "react";

export default function SignIn(
    { providers }: InferGetServerSidePropsType<typeof getServerSideProps>,
    req: NextApiRequest,
    res: NextApiResponse,
) {
    const [session, setSession] = useState<any>(null);

    var providersNew = Object.values(providers);

    useEffect(() => {
        // Refresh the session whenever it changes
        if (session) {
            session.refresh(() => {});
        }
    }, [session]);

    return (
        <>
            <div className="parent">
                <div className="buttons">
                    {providersNew.map((provider) => (
                        <div key={provider.name}>
                            <button
                                className="btn btn-primary button"
                                onClick={() => signIn(provider.id)}
                            >
                                Sign in with {provider.name}
                            </button>
                        </div>
                    ))}
                </div>
            </div>
            <style jsx>{`
                .parent {
                    width: 100%;
                    margin-left: auto;
                    margin-right: auto;
                    align-items: center;
                }
                .button {
                    margin-left: auto;
                    margin-right: auto;
                    margin-top: 10px;
                    margin-bottom: 10px;
                    display: block;
                    border-radius: 30px;
                    background-color: #1853fe;
                }
            `}</style>
        </>
    );
}

export async function getServerSideProps(context: GetServerSidePropsContext) {
    const session = await getServerSession(context.req, context.res, authOptions);

    // If the user is already logged in, redirect.
    // Note: Make sure not to redirect to the same page
    // To avoid an infinite loop!
    if (session) {
        return { redirect: { destination: "/" } };
    }

    const providers = await getProviders();

    return {
        props: { providers: providers ?? [] },
    };
}
```

To clarify: We only create the UI ourselves. The authentication is still handled by
NextAuth.js and gets initiated when we call the signIn method that NextAuth.js provides.

### 6.2 Embed the passkey authentication webcomponent

We now add the <corbado-passkey-associate-login/> to our signIn page.

```tsx
"use client";
import type {
    GetServerSidePropsContext,
    InferGetServerSidePropsType,
    NextApiRequest,
    NextApiResponse,
} from "next";
import { getProviders, signIn } from "next-auth/react";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../api/auth/[...nextauth]";
import { useCallback, useEffect, useState } from "react";

import "@corbado/webcomponent/pkg/auth_cui.css";

const projectID = process.env.CORBADO_PROJECT_ID;

export default function SignIn(
    { providers }: InferGetServerSidePropsType<typeof getServerSideProps>,
    req: NextApiRequest,
    res: NextApiResponse,
) {
    const [session, setSession] = useState<any>(null);

    var providersNew = Object.values(providers);
    providersNew = providersNew.filter(function (el) {
        return el.name != "webauthn";
    });

    useEffect(() => {
        // This will run only on client-side

        import("@corbado/webcomponent")
            .then((module) => {
                const Corbado = module.default || module;

                ("Initializing Corbado session");
                setSession(new Corbado.Session(projectID));
            })
            .catch((err) => {});
    }, []);

    useEffect(() => {
        // Refresh the session whenever it changes
        if (session) {
            session.refresh(() => {});
        }
    }, [session]);

    return (
        <>
            <div className="parent">
                <div className="buttons">
                    {providersNew.map((provider) => (
                        <div key={provider.name}>
                            <button
                                className="btn btn-primary button"
                                onClick={() => signIn(provider.id)}
                            >
                                Sign in with {provider.name}
                            </button>
                        </div>
                    ))}
                </div>
                <div className="associate-container">
                    <corbado-passkey-associate-login project-id={projectID} />
                </div>
            </div>
            <style jsx>{`
                .parent {
                    width: 100%;
                    margin-left: auto;
                    margin-right: auto;
                    align-items: center;
                }
                .button {
                    margin-left: auto;
                    margin-right: auto;
                    margin-top: 10px;
                    margin-bottom: 10px;
                    display: block;
                    border-radius: 30px;
                    background-color: #1853fe;
                }
                .associate-container {
                    width: 200px;
                    margin-left: auto;
                    margin-right: auto;
                    align-items: center;
                }
            `}</style>
        </>
    );
}

export async function getServerSideProps(context: GetServerSidePropsContext) {
    const session = await getServerSession(context.req, context.res, authOptions);

    // If the user is already logged in, redirect.
    // Note: Make sure not to redirect to the same page
    // To avoid an infinite loop!
    if (session) {
        return { redirect: { destination: "/" } };
    }

    const providers = await getProviders();

    return {
        props: { providers: providers ?? [] },
    };
}
```

Notice how we also filter out the webauthn authentication provider (our
CredentialsProvider).

### 6.3 Create redirect page

After successful authentication, the Corbado web component redirects the user to the
Redirect URL we configured in [step 4](#set-up-your-corbado-account-and-project)
(/auth/redirect).

We create a file under /pages/auth/redirect.tsx and add the following code. This just
calls the signIn method of NextAuth.js, but with the parameters credentials instead of an
OAuth-provider-id.

```tsx
import type {
    GetServerSidePropsContext,
    InferGetServerSidePropsType,
    NextApiRequest,
    NextApiResponse,
} from "next";
import { getProviders, signIn } from "next-auth/react";
import { getServerSession } from "next-auth/next";
import { authOptions } from "../api/auth/[...nextauth]";

export default function Redirect(
    { providers }: InferGetServerSidePropsType<typeof getServerSideProps>,
    req: NextApiRequest,
    res: NextApiResponse,
) {
    signIn("credentials", { provider: "corbado" });

    return (
        <>
            <p>Authenticating...</p>
        </>
    );
}

export async function getServerSideProps(context: GetServerSidePropsContext) {
    const session = await getServerSession(context.req, context.res, authOptions);

    // If the user is already logged in, redirect.
    // Note: Make sure not to redirect to the same page
    // To avoid an infinite loop!
    if (session) {
        return { redirect: { destination: "/" } };
    }

    const providers = await getProviders();

    return {
        props: { providers: providers ?? [] },
    };
}
```

### 6.4 Add credentials provider for the custom passkey login

To be able to call NextAuth.jss signIn method with credentials as parameter, we need to
configure a custom authentication provider, so NextAuth.js knows what to do and when to
generate a session for which user.

The authentication providers for NextAuth.js are configured in
/pages/api/auth/\[¦nextauth].ts.

Here, we add a CredentialsProvider to incorporate our custom authentication option. The
CredentialsProvider contains an authorize method which gets called by NextAuth.js after we
use the signIn method ([step 6.3](#create-redirect-page)).

We obtain the current user from the session that Corbado has initiated. By returning an
object containing email and name, we tell NextAuth.js the current user and NextAuth.js
will therefore initialize a session itself, which will then be used in all of your
application pages.

```ts
import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import GithubProvider from "next-auth/providers/github";
import TwitterProvider from "next-auth/providers/twitter";
import Auth0Provider from "next-auth/providers/auth0";
import CredentialsProvider from "next-auth/providers/credentials";
import * as jose from "jose";

const projectID = process.env.CORBADO_PROJECT_ID;

export const authOptions: NextAuthOptions = {
    // https://next-auth.js.org/configuration/providers/oauth

    providers: [
        FacebookProvider({
            clientId: process.env.FACEBOOK_ID,
            clientSecret: process.env.FACEBOOK_SECRET,
        }),
        GithubProvider({
            clientId: process.env.GITHUB_ID,
            clientSecret: process.env.GITHUB_SECRET,
        }),
        GoogleProvider({
            clientId: process.env.GOOGLE_ID,
            clientSecret: process.env.GOOGLE_SECRET,
        }),
        TwitterProvider({
            clientId: process.env.TWITTER_ID,
            clientSecret: process.env.TWITTER_SECRET,
        }),
        Auth0Provider({
            clientId: process.env.AUTH0_ID,
            clientSecret: process.env.AUTH0_SECRET,
            issuer: process.env.AUTH0_ISSUER,
        }),
        CredentialsProvider({
            name: "webauthn",
            credentials: {},
            async authorize(cred, req) {
                if (cred.provider !== "corbado") return null;

                // Get the token from the cookie
                var cbo_short_session = req.headers.cookie
                    .split("; ")
                    .find((row) => row.startsWith("cbo_short_session"));
                var token = cbo_short_session.split("=")[1];

                // Get the JWKS URL from the project ID
                var issuer = "https://" + projectID + ".frontendapi.corbado.io";
                var jwksUrl = issuer + "/.well-known/jwks";

                // Initialize the JWKS client
                const JWKS = jose.createRemoteJWKSet(new URL(jwksUrl), {
                    cacheMaxAge: 10 * 60 * 1000,
                });
                const options = {
                    issuer: issuer,
                };
                try {
                    // Verify the token
                    const { payload } = await jose.jwtVerify(token, JWKS, options);
                    if (payload.iss === issuer) {
                        //
                        //Next steps: Load data from database here to always have all the data available in the session
                        return { email: payload.email, name: payload.name, image: null };
                    } else {
                        console.log("issuer not valid");
                    }
                } catch (e) {
                    console.log("Error: ", e);
                }
            },
        }),
    ],
    theme: {
        colorScheme: "light",
    },
    callbacks: {
        async jwt({ token }) {
            token.userRole = "admin";
            return token;
        },
    },
    pages: {
        signIn: "/auth/signin",
    },
};

export default NextAuth(authOptions);
```

## 7. Add passkey associate component

The <corbado-passkey-associate/> web component will allow an authenticated user to add a
passkey to their account and afterwards login with it using the web component we already
integrated.

To get our association token, we create our own endpoint under
/pages/api/auth/association.ts which requests a token for a specific user from the Corbado
backend and returns it to us.

```ts
const Corbado = require("@corbado/node-sdk");
import type { NextApiRequest, NextApiResponse } from "next";

// sind nicht im passenden .env Example
const projectID = process.env.CORBADO_PROJECT_ID;
const apiSecret = process.env.CORBADO_API_SECRET;

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
    const config = new Corbado.Configuration(projectID, apiSecret);
    const corbado = new Corbado.SDK(config);

    const { loginIdentifier, loginIdentifierType } = req.body;

    try {
        // use the Corbado SDK to create the association token
        const associationToken = await corbado.associationTokens.create(
            loginIdentifier,
            loginIdentifierType,
            {
                remoteAddress: req.headers["x-forwarded-for"] || req.socket.remoteAddress,
                userAgent: req.headers["user-agent"],
            },
        );

        if (associationToken?.data?.token) {
            return res.status(200).send(associationToken.data.token);
        } else {
            return res.status(200).send({ error: "error_creating_association_token" });
        }
    } catch (err) {
        console.log(err);
        res.status(200).send({ error: "error_creating_association_token" });
    }
}
```

In our frontend, we use this endpoint to get the association token for our current user
and have the <corbado-passkey-associate/> web component display the Create
[Passkey button](https://www.corbado.com/blog/passkey-login-best-practices):

```tsx
import Layout from "../components/layout";
import { signIn, signOut, useSession } from "next-auth/react";
import React, { useCallback, useEffect, useState } from "react";

import axios from "axios";
import("@corbado/webcomponent");

interface AssociationToken {
    associationToken: string;
}

const PASSKEY_CREATION_SUCCESSFUL = "PASSKEY_CREATION_SUCCESSFUL";
const PASSKEY_CREATION_FAILED = "PASSKEY_CREATION_FAILED";
const DEVICE_NOT_PASSKEY_READY = "DEVICE_NOT_PASSKEY_READY";

export default function IndexPage() {
    const { data: session, status } = useSession();
    const [associationToken, setAssociationToken] = useState<AssociationToken | null>(
        null,
    );
    const [ref, setRef] = useState<any | null>(null);
    const [hasPasskey, setHasPasskey] = useState<boolean>(false);
    const [hasCheckedPasskey, setHasCheckedPasskey] = useState<boolean>(false);
    const [passkeyReady, setPasskeyReady] = useState<boolean>(true);

    // The following event handlers can be used to react to different events from the web component
    const onPasskeyCreationSuccessful = useCallback((_event: CustomEvent) => {
        console.log("Passkey creation successful");
        setAssociationToken(null);
        setPasskeyReady(true);
        setHasPasskey(true);
    }, []);

    const onPasskeyCreationFailed = useCallback((_event: CustomEvent) => {
        console.log("Passkey creation failed");
        setAssociationToken(null);
        setHasCheckedPasskey(false);
    }, []);

    const onDeviceNotPasskeyReady = useCallback((_event: CustomEvent) => {
        console.log("Device not passkey ready");
        setAssociationToken(null);
        setPasskeyReady(false);
        setHasPasskey(false);
    }, []);

    // Create and remove the event listeners
    useEffect(() => {
        if (ref) {
            ref.addEventListener(
                PASSKEY_CREATION_SUCCESSFUL,
                onPasskeyCreationSuccessful,
            );
            ref.addEventListener(PASSKEY_CREATION_FAILED, onPasskeyCreationFailed);
            ref.addEventListener(DEVICE_NOT_PASSKEY_READY, onDeviceNotPasskeyReady);
        }

        // Cleanup function
        return () => {
            if (ref) {
                ref.removeEventListener(
                    PASSKEY_CREATION_SUCCESSFUL,
                    onPasskeyCreationSuccessful,
                );
                ref.removeEventListener(PASSKEY_CREATION_FAILED, onPasskeyCreationFailed);
                ref.removeEventListener(
                    DEVICE_NOT_PASSKEY_READY,
                    onDeviceNotPasskeyReady,
                );
            }
        };
    }, [
        ref,
        onPasskeyCreationSuccessful,
        onPasskeyCreationFailed,
        onDeviceNotPasskeyReady,
    ]);

    const handleButtonClick = async () => {
        try {
            // loginIdentifier needs to be obtained via a backend call or your current state / session management
            // it should be a dynamic value depending on the current logged-in user
            const response = await axios.post<AssociationToken>("/api/auth/associate", {
                loginIdentifier: session.user.email,
                loginIdentifierType: "email",
            });
            setHasCheckedPasskey(true);
            setHasPasskey(response.data.error != undefined);
            console.log("AssociationToken response: ", response.data);
            if (response.data.error == undefined) {
                setAssociationToken(response.data);
            }
        } catch (err) {
            console.log(err);
        }
    };

    if (session?.user != undefined && !hasCheckedPasskey) {
        handleButtonClick();
    }

    return (
        <Layout>
            <h1>NextAuth.js Example</h1>
            <p>
                This is an example site to demonstrate how to use{" "}
                <a href="https://next-auth.js.org">NextAuth.js</a> together with{" "}
                <a href="https://corbado.com">Corbado</a> for passkey authentication.
            </p>
            {!session?.user && (
                <>
                    <p>
                        When you are logged in, you can add a passkey to your account
                        here!
                    </p>
                </>
            )}
            {session?.user && !hasPasskey && (
                <>
                    {!associationToken && (
                        <button onClick={handleButtonClick}>
                            Add passkey to my account
                        </button>
                    )}
                    {associationToken && !hasPasskey && (
                        <div className="associate-container">
                            <corbado-passkey-associate
                                project-id="pro-2808756695548043260"
                                association-token={associationToken}
                                ref={setRef}
                            />
                        </div>
                    )}
                    <style jsx>{`
                        .associate-container {
                            width: 200px;
                            margin-left: auto;
                            margin-right: auto;
                            align-items: center;
                        }
                    `}</style>
                </>
            )}
            {session?.user && hasPasskey && (
                <>
                    <p>
                        <strong>
                            You have already registered a passkey on this device!
                        </strong>
                    </p>
                </>
            )}

            {!passkeyReady && (
                <>
                    <p>
                        <strong>Your device is not passkey ready!</strong>
                    </p>
                </>
            )}
        </Layout>
    );
}
```

Thats it our app is fully set up and configured!

## 8. Start using passkeys with our NextAuth.js implementation

To start our application we execute

```bash
npm install
```

and afterwards

```bash
npm run dev
```

When visiting [http://localhost:3000](http://localhost:3000) you should see the following
screen:

![Index page](https://www.corbado.com/website-assets/65253518cde66887c8e324ae_index_79f3f7dbb7.jpg)

Clicking on "Sign in" should redirect you to our custom signIn page:

![Passkey sign in page](https://www.corbado.com/website-assets/652532835766cdfb4390a42b_nextauth_example_sign_in_679ff01ff2.jpg)

Now login with Google or any other provider. After authentication, you will be sent to the
index page where you have the possibility to add a passkey to your account:

![Index page](https://www.corbado.com/website-assets/652535659f293a260c45bd04_index_logged_in_f977430546.jpg)

Click on the Create [passkey button](https://www.corbado.com/blog/passkey-login-best-practices) and
[create a passkey](https://www.corbado.com/blog/passkey-creation-best-practices).

![Creating passkey](https://www.corbado.com/website-assets/652535b0662a5d2fad47aac6_create_passkey_791bb43eac.jpg)

![Index page](https://www.corbado.com/website-assets/652534bf3b18f3ca1eff3f82_index_already_registered_passkey_0e28dd0506.jpg)

Sign out and go to the "Sign in" page again. If you now enter the email of the OAuth
account into the Corbado web component, you should be able to log in using the passkey you
just created.

![Passkey sign in page](https://www.corbado.com/website-assets/652545ec72227cb0190013ca_login_with_passkey_63f1960665.jpg)

## 9. Conclusion

This tutorial showed how easy it is to add
[passwordless authentication](https://www.corbado.com/glossary/passwordless-authentication) with passkeys to
NextAuth.js using Corbado. We only used Corbado for
[passkey login](https://www.corbado.com/blog/passkey-login-best-practices), but Corbado can also provide passkey
sign-up together with [passkey login](https://www.corbado.com/blog/passkey-login-best-practices) and email links
as fallback.

## Frequently Asked Questions

### How does the passkey login flow connect to NextAuth.js session management?

After the Corbado web component authenticates the user, it redirects to a configured
redirect page that calls NextAuth's signIn method with credentials parameters. NextAuth
then verifies the Corbado short-session JWT via JWKS and, if valid, creates its own
session for that user.

### Can users register a new account directly with a passkey in a NextAuth.js integration?

No, not with the Passkey Association approach described here. Users must first create an
account and authenticate via an existing provider such as Google OAuth, then add a passkey
to their account for future logins.

### What environment variables and Corbado developer panel settings do I need to configure?

You need to set CORBADO_PROJECT_ID and CORBADO_API_SECRET in your environment. In the
Corbado developer panel you must configure the Application URL, Redirect URL and Relying
Party ID, which for local development are your localhost signin page, your redirect page
and 'localhost' respectively.

### How does NextAuth.js verify that a passkey authentication from Corbado is legitimate?

The CredentialsProvider's authorize method reads the cbo_short_session cookie, then uses
the jose library to verify the JWT against Corbado's remote JWKS endpoint at
projectID.frontendapi.corbado.io/.well-known/jwks, checking the issuer claim before
returning user data to NextAuth.
