How I got the access_token from the provider into my Session on Next Auth V5 in a Next.js app with Typescript and appDir

How I got the access_token from the provider into my Session on Next Auth V5 in a Next.js app with Typescript and appDir

Next Auth (now known as Auth.js) is a bit of a hot mess.

It absolutely takes away lots of work for you if you're using multiple authentication providers (e.g. login with Facebook, Google, Apple etc.) but makes it very hard to get access to those Provider's access tokens if, like me, you have a backend that requires that token for authentication. Additionally support for automatically refreshing tokens is lacking even in V5 which means it's still really only suitable for that very initial 'login and get the user's name' step rather than any on-going checks to ensure that a user is authenticated.

If you have a backend that requires the access token from the authentication provider to be passed in requests (e.g. a Bearer token) then here's a quick guide I wrote recently after spending too much time trying to figure out how to get to it with a Next.js app using App Dir with Typescript.


This works for both client side and server side pages with appDir. I have an API that expects the provider's JWT to authenticate requests, so having access to this field on all pages was essential for me to be able to craft XHR requests properly.

  1. Examples are based on next-auth-example at commit 4e02b3441. If you're working with this project you have to configure your own provider, this is well covered in the existing docs. If it's your first time to V5 from V4 the biggest difference seems to be that the .env variables now all have different prefixes because of the move to Auth.js and things like OktaProvider are now just called Okta.

  2. access_token is hidden or omitted by default, which is a huge part of the problem. We're going to add it to the Session type in a callback and to make this work without Typescript throwing up we have to extend the type. This is similar to what's too briefly described here in the official docs: https://next-auth.js.org/getting-started/typescript

  3. Create types/next-auth.d.ts in the base of your project to house our type extensions. It should look like:

import NextAuth, { DefaultSession } from "next-auth"
import {DefaultJWT} from "@auth/core/jwt";

declare module "next-auth" {

    // Extend session to hold the access_token
    interface Session {
        access_token: string & DefaultSession
    }

    // Extend token to hold the access_token before it gets put into session
    interface JWT {
        access_token: string & DefaultJWT
    }
}
  1. Now that you've added the type extension file you have to tell Typescript to look at it, in your tsconfig.json in the compilerOptions.include key add the path to the file you just created. In the example project that means it now looks like (last item in the array is the change):
...
  "include": [
    "process.d.ts",
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "types/next-auth.d.ts"
  ],
...
  1. The place where we're going to grab the access_token is during the login process. This means using a callback. Amend the config at auth.ts for this. I'd already amended this to use my own provider, and set the JWT strategy, here's what the whole file looked like - obviously console.logs you can remove after testing. I've added a session.strategy = JWT because that suits the persistence model I'm looking for for tokens, it's not directly related to the solution here:
import NextAuth from "next-auth"

import type {NextAuthConfig} from "next-auth"
import Okta from "@auth/core/providers/okta";

export const config = {
    theme: {
        logo: "https://next-auth.js.org/img/logo/logo-sm.png",
    },
    providers: [
        Okta({
            clientId: "myClientIdhere",
            clientSecret: "mySecretHere",
            issuer: "https://path.to.your.okta.instance.com/oauth2/default",
            // Put in a an empty `state` because Next Auth doesn't appear to be specifying this
            // properly, and Okta doesn't like a missing state param
            authorization: "https://path.to.your.okta.instance.com/oauth2/default/v1/authorize?response_type=code&state=e30="
        })
    ],
    session: {
        strategy: "jwt",
    },
    debug: true,
    callbacks: {
        session({session, token}) {
            console.log(`Auth Sess = ${JSON.stringify(session)}`)
            console.log(`Auth Tok = ${JSON.stringify(token)}`)
            if (token.access_token) {
                session.access_token = token.access_token // Put the provider's access token in the session so that we can access it client-side and server-side with `auth()`
            }
            return session
        },
        jwt({token, account, profile}) {
            console.log(`Auth JWT Tok = ${JSON.stringify(token)}`)
            console.log(`Router Auth JWT account = ${JSON.stringify(account)}`)

            if (account) {
                token.access_token = account.access_token // Store the provider's access token in the token so that we can put it in the session in the session callback above
            }

            return token
        },
        authorized({ request, auth }) {
            const { pathname } = request.nextUrl
            if (pathname === "/middleware-example") return !!auth
            return true
        },
    }
} satisfies NextAuthConfig

export const {handlers, auth, signIn, signOut} = NextAuth(config)
  1. Pages are using auth() e.g. in server-example/page.tsx: const session = await auth(). Accessing the token is now as simple as doing {session?.access_token} on the page. Here's a screenshot of that working:

screenshot

  1. And that's what took me the best part of a day to figure out! Hopefully it's faster for you

  2. Testing: You can see the access_token now in the console.logs that are printing, if you want to see it on a page edit server-example/page.tsx and client-example/page.tsx - these both use the same code (auth()) to get the session/token in Next Auth v5, which is nice.

Other weaknesses in current day Next Auth you might want to deal with next if you're on the same journey as me. Refreshing tokens: https://authjs.dev/guides/basics/refresh-token-rotation?frameworks=core