NextAuth with App Router

This guide will help you integrate Descope with your NextAuth application using App Router. Follow the steps below to get started.

If you're interested in using our native SDK instead of NextAuth, you can read about the pros and cons of each in our guide.

Install NextAuth.js

To use Descope with Next.js, the simplest method is to use NextAuth.js. You can install it with this command:

Terminal
npm i --save next-auth

Import NextAuth Packages

Import all necessary NextAuth packages in a route.ts file. The location of route.ts will exist in app/api/auth/[...nextauth].

app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth/next";
import type { NextAuthOptions } from "next-auth"
 
export const authOptions: NextAuthOptions = {
  providers: [],
}
 
const handler = NextAuth(authOptions)
 
export { handler as GET, handler as POST }

Initialize Descope as a Provider

Once you've imported the necessary packages, you'll need to initialize NextAuth and add Descope as a provider.

app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth/next";
import type { NextAuthOptions } from "next-auth"
 
export const authOptions: NextAuthOptions = {
	providers: [
		{
			id: "descope",
			name: "Descope",
			type: "oauth",
			wellKnown: `https://api.descope.com/__ProjectID__/.well-known/openid-configuration`,
			authorization: { params: { scope: "openid email profile" } },
			idToken: true,
			clientId: "__ProjectID__",
			clientSecret: "<Descope Access Key>",
			checks: ["pkce", "state"],
			profile(profile) {
				return {
					id: profile.sub,
					name: profile.name,
					email: profile.email,
					image: profile.picture,
				}
			},
		}
	],
}
 
const handler = NextAuth(authOptions)
 
export { handler as GET, handler as POST }

Add JWT Callback for Token Refresh

To make sure that your NextAuth session is refreshed according to the settings defined in the Descope Console, you'll want to add a JWT callback function to refresh the Descope Access Token that NextAuth uses for its session/profile management.

app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth/next";
import type { NextAuthOptions } from "next-auth"
 
export const authOptions: NextAuthOptions = {
	providers: [
		...
	],
	callbacks: {
        async jwt({token, account, profile}) {
            if (account) {
                return {
                    ...token,
                    access_token: account.access_token,
                    expires_at: Math.floor(Date.now() / 1000 + account.expires_in),
                    refresh_token: account.refresh_token,
                    profile: {
                      name: profile?.name,
                      email: profile?.email,
                      image: profile?.picture,
                  },
                }
            } else if (Date.now() < token.expires_at * 1000) {
                return token
            } else {
                try {
                    const response = await fetch("https://api.descope.com/oauth2/v1/token", {
                        headers: {"Content-Type": "application/x-www-form-urlencoded"},
                        body: new URLSearchParams({
                            client_id: "__ProjectID__",
                            client_secret: "<Descope Access Key>",
                            grant_type: "refresh_token",
                            refresh_token: token.refresh_token,
                        }),
                        method: "POST",
                    })
    
                    const tokens = await response.json()
    
                    if (!response.ok) throw tokens
    
                    return {
                        ...token,
                        access_token: tokens.access_token,
                        expires_at: Math.floor(Date.now() / 1000 + tokens.expires_in),
                        refresh_token: tokens.refresh_token ?? token.refresh_token,
                    }
                } catch (error) {
                    console.error("Error refreshing access token", error)
                    return {...token, error: "RefreshAccessTokenError"}
                }
            }
        },
    
        async session({session, token}) {
            if (token.profile) {
              session.user = token.profile;
            }
    
            session.error = token.error
            session.accessToken = token.access_token
            return session
        },
    }
}
 
const handler = NextAuth(authOptions)
 
export { handler as GET, handler as POST }

Setup NextAuth's SessionProvider

In your /app directory, create a provider.tsx file and wrap the client components in SessionProvider to allow for session management and authentication throughout your Next application.

app/provider.tsx
'use client';
 
import { SessionProvider } from 'next-auth/react';
 
export default function NextAuthSessionProvider({ children }: Readonly<{ children: React.ReactNode }>) {
  return <SessionProvider>{children}</SessionProvider>;
}

Allow Access to Session Data

In your layout.tsx file, import the NextAuthSessionProvider and wrap the nodes in the provider.

app/layout.tsx
import NextAuthSessionProvider from './provider'
 
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body>
        <NextAuthSessionProvider>
          <div>
            {children}
          </div>
        </NextAuthSessionProvider>
      </body>
    </html>
  );
}

Accessing the Authentication Flow

Add a sign-in button in the client to access your sign-in authentication flow. The signIn method has 'descope' as the provider id, and the callback URL set to /dashboard as an example to redirect back to.

components/navbar.tsx
'use client'
 
import { signIn } from "next-auth/react"
 
export default function Navbar() {
    return (
        <button 
            onClick={() => signIn(
                "descope", 
                { callbackUrl: "/dashboard" }
            )}
        >
			Apply
        </button>
    )
}

Customizing the Authentication Flow URL

The URL of the hosted authentication flow that is redirected to after calling signIn can be modified in your Descope console. Navigate to your Descope console -> Applications -> OIDC default application -> Flow Hosting URL. Or go directly here.

Session Management

To learn more about session management with NextAuth & Descope, check out the Web Client Session Validation docs.

Congratulations

Now that you've got the authentication down, go focus on building out the rest of your app!


Checkpoint

Your application is now integrated with Descope. Please test with sign-up or sign-in use case.

Need help?

Using NextAuth and Customization

Once you've configured NextAuth to work with Descope as an OIDC provider, the next step is to utilize all of the various NextAuth functions in your application.

You can visit our guide with detailed docs on how all of the Sign In, Logout, etc. functions work with NextAuth, in your Next.js application.

Otherwise, you can visit our Flow Customization section to configure and personalize many different areas of Descope, including your brand, style, custom user authentication journeys, etc.

We recommend starting with customizing your user-facing screens, such as signup and login.

NextAuth and Widgets

If you're using NextAuth with Descope, you will have to incorporate some elements of our Next.js SDK in your application to be able to utilize Descope Widgets.

Using Widgets will require the use of our Next.js SDK in your application requires a reference to your Descope Project ID. This works from wrapping your layout.tsx with our <AuthProvider> wrapper from our Next.js SDK.

layout.tsx
export default function App({ Component, pageProps,}: AppProps<{ session: Session }>) {
  return (
      <AuthProvider projectId={process.env.DESCOPE_PROJECT_ID || ''}>
        <>
          <SessionManager />
          <Navbar Logo={LogoBlack.src} />
          <div>
            <Component {...pageProps} />
          </div>
          <Bottom Logo={LogoWhite.src} SocialList={SocialList}  />
        </>
      </AuthProvider>
  )
}

As long as the Widget components have access to your refresh tokens, you should be able to use them, even if using NextAuth. If you're managing your refresh tokens with localStorage instead of cookies, you'll need to do some additional steps, documented below.

Managing Refresh Tokens

If you are managing your refresh tokens with cookies, and using Manage in Response Body for your Token Response Method instead, then you'll need to persist the refresh token in your application manually.

If you are using Manage in Cookies instead, you can skip this part.

_app.tsx
const SessionManager = () => {
 
  const {data: session, status} = useSession();
 
  useEffect(() => {
      if(status === "loading") return;
 
      if(!session){
        console.log()
        localStorage.removeItem("DSR")
 
        return;
      }
 
      const sessionData = session as Session & {refreshToken: string};
 
      if(sessionData.refreshToken){
        localStorage.setItem("DSR", sessionData.refreshToken)
      }
  }, [session, status])
 
  return null;
}

You'll also need to modify the NextAuth callback functions to include the refresh token as well.

utils/options.ts
 ],
 callbacks: {
    async jwt({ token, account }) {
      if (account?.id_token) {
        token.idToken = account.id_token;
      }
 
      if (account?.refresh_token) {
        token.refreshToken = account.refresh_token;
      }
      return token;
    },
    async session({ session, token }) {
 
      (session as any).idToken = token.idToken;
      (session as any).refreshToken = token.refreshToken;
      return session;
    },  
  },

Once you've done that, the widgets should work with the same session created with every login with Descope.

Was this helpful?

On this page