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.
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 likeOktaProvider
are now just calledOkta
.access_token
is hidden or omitted by default, which is a huge part of the problem. We're going to add it to theSession
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/typescriptCreate
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
}
}
- Now that you've added the type extension file you have to tell Typescript to look at it, in your
tsconfig.json
in thecompilerOptions.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"
],
...
- 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 - obviouslyconsole.log
s you can remove after testing. I've added asession.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)
- Pages are using
auth()
e.g. inserver-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:
And that's what took me the best part of a day to figure out! Hopefully it's faster for you
Testing: You can see the
access_token
now in theconsole.log
s that are printing, if you want to see it on a page editserver-example/page.tsx
andclient-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