Skip to main content
This recipe uses the Dynamic JavaScript SDK (@dynamic-labs-sdk/client + @dynamic-labs-sdk/react-hooks). The SDK is headless — render your own auth UI and call the SDK’s auth functions on submit. See the React Quickstart for the full setup.

Pre-requisites

Steps

Add the right env variables

You’ll need to define two environment variables in your .env.local file:
You’ll be able to find both in the SDK & API Keys page of the Dynamic dashboard. The first will already be generated for you but for the API key, you’ll need to generate your own via the UI on that page. Make sure you add the values of each variable to the .env.local file, and you’re good to go.

Install the Dynamic JavaScript SDK

Create the Dynamic client module

The client is a module-level singleton — create it once and import it from anywhere. Import this file at app root so extensions register before any component renders.
app/lib/dynamicClient.ts

Add the DynamicProvider

In Next.js App Router, providers need to live in a client component so they can hold reactive state. Create a wrapper and import the client module so extensions register on mount:
app/components/dynamic-provider-wrapper.tsx
Now mount the wrapper in app/layout.tsx:
app/layout.tsx

Build a login component

The JS SDK is headless — there’s no built-in widget. Build a minimal login form using sendEmailOTP and verifyOTP:
app/components/login.tsx
Mount it in your header:
app/components/header.tsx
Now we’re almost done with the client side. We’ll need to somehow send the JWT that Dynamic returns on login to our server functions so that we can validate it and create a session. To do this we’ll use the tokenChanged event Dynamic provides, but let’s come back to that and first add the server side code.

Define the JWT decoding

NextAuth needs to know how to decode and validate the JWT which Dynamic sends back. To do this we’ll create a custom JWT decoder inside a new helper file:
app/lib/authHelpers.ts
You’ll see that the above function depends on a few things, one of which is the external jsonwebtoken library. We’ll need to install this:
Next we’ll need to define the getKey function which is used to fetch the public key which you can use to decode the JWT. This function will make an API call to Dynamic. We’ll add this to the same file:
app/lib/authHelpers.ts
With this in place, there are just two steps left. Firstly we’ll need to adapt the NextAuth configuration to use the CredentialsProvider so the JWT works, and then we’ll need to trigger everything correctly when a user logs in.

Update the NextAuth configuration

You can copy the below code and paste it over the full existing auth.ts file in the demo repo:
./auth.ts

Trigger the JWT validation on login

In the old React Core SDK you could pass an onAuthSuccess callback into DynamicContextProvider settings. The JS SDK is event-driven instead — listen for tokenChanged and forward the new JWT to NextAuth. Add a NextAuthBridge component inside DynamicProvider:
app/components/nextauth-bridge.tsx
Mount it once inside the provider wrapper:
app/components/dynamic-provider-wrapper.tsx
useEvent cleans up the subscription on unmount, so it’s safe under React Strict Mode. We’re using the getCsrfToken function which NextAuth provides — this is important because NextAuth uses CSRF tokens to prevent CSRF attacks.

Token Scopes

A JWT token includes a scope claim (a space-separated list of scopes) that indicates the user’s authentication state. You must verify that the scope list includes user:basic to confirm the user has fully completed authentication.
Critical: Verify that user:basic is present in the scope list — do not check for strict equality. A fully authenticated user may have additional scopes (e.g. from Access Lists or Gates), so the scope string can be e.g. user:basic beta-access. If user:basic is not among the scopes, the user has NOT completed the authentication flow and the JWT should not be trusted for protected operations. Common non-final scopes include:
  • requiresAdditionalAuth — User must complete MFA
  • Other intermediate scopes — User is still completing verification steps
Our SDK handles this for the frontend, but for the backend you will need to check this scope and handle it accordingly. To do this, add a scope verification check in the authorize function in the auth.ts file:
./auth.ts
Important: Always reject tokens whose scope list does not include user:basic. These tokens represent incomplete authentication and should never be trusted for protected operations.

Run the example

You should now see a login form in the header which you can use to sign in with email. Once you’ve logged in you should see “LOGGED IN” in the browser console.

Going further

You’ll see in auth.ts that we are assigning certain JWT fields to a user object. You can add any fields you want to this object, and then access them in your pages via the useSession hook.
Last modified on June 24, 2026