Refresh token rotation is the practice of updating an access_token
on behalf of the user, without requiring interaction (eg.: re-sign in). access_token
s are usually issued for a limited time. After they expire, the service verifying them will ignore the value. Instead of asking the user to sign in again to obtain a new access_token
, certain providers support exchanging a refresh_token
for a new access_token
, renewing the expiry time. Refreshing your access_token
with other providers will look very similar, you will just need to adjust the endpoint and potentially the contents of the body being sent to them in the request.
Our goal is to add zero-config support for built-in providers eventually. Let us know if you would like to help.
Implementation
First, make sure that the provider you want to use supports refresh_token
’s. Check out The OAuth 2.0 Authorization Framework spec for more details. Depending on the session strategy, the refresh_token
can be persisted either in a database, in a cookie, or in an encrypted JWT.
While using a JWT to store the refresh_token
is very common, it is less
secure than saving it in a database as it is easier for a potential attacker
to retrieve from a JWT compared to your applications database. You need to
evaluate based on your requirements which strategy you choose.
JWT strategy
Using the jwt and session callbacks, we can persist OAuth tokens and refresh them when they expire.
Below is a sample implementation of refreshing the access_token
with Google. Please note that the OAuth 2.0 request to get the refresh_token
will vary between different providers, but the rest of logic should remain similar.
import NextAuth, { type User } from "next-auth"
import Google from "next-auth/providers/google"
export const { handlers, auth } = NextAuth({
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
// Google requires "offline" access_type to provide a `refresh_token`
authorization: { params: { access_type: "offline", prompt: "consent" } },
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
// First login, save the `access_token`, `refresh_token`, and other
// details into the JWT
const userProfile: User = {
id: token.sub,
name: profile?.name,
email: profile?.email,
image: token?.picture,
}
return {
access_token: account.access_token,
expires_at: account.expires_at,
refresh_token: account.refresh_token,
user: userProfile,
}
} else if (Date.now() < token.expires_at * 1000) {
// Subsequent logins, if the `access_token` is still valid, return the JWT
return token
} else {
// Subsequent logins, if the `access_token` has expired, try to refresh it
if (!token.refresh_token) throw new Error("Missing refresh token")
try {
// The `token_endpoint` can be found in the provider's documentation. Or if they support OIDC,
// at their `/.well-known/openid-configuration` endpoint.
// i.e. https://accounts.google.com/.well-known/openid-configuration
const response = await fetch("https://oauth2.googleapis.com/token", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.AUTH_GOOGLE_ID!,
client_secret: process.env.AUTH_GOOGLE_SECRET!,
grant_type: "refresh_token",
refresh_token: token.refresh_token!,
}),
method: "POST",
})
const responseTokens = await response.json()
if (!response.ok) throw responseTokens
return {
// Keep the previous token properties
...token,
access_token: responseTokens.access_token,
expires_at: Math.floor(Date.now() / 1000 + (responseTokens.expires_in as number)),
// Fall back to old refresh token, but note that
// many providers may only allow using a refresh token once.
refresh_token: responseTokens.refresh_token ?? token.refresh_token,
}
} catch (error) {
console.error("Error refreshing access token", error)
// The error property can be used client-side to handle the refresh token error
return { ...token, error: "RefreshAccessTokenError" as const }
}
}
},
async session({ session, token }) {
if (token.user) {
session.user = token.user as User
}
return session
},
},
},
})
declare module "next-auth" {
interface Session {
error?: "RefreshAccessTokenError"
}
}
declare module "next-auth/jwt" {
interface JWT {
access_token: string
expires_at: number
refresh_token: string
error?: "RefreshAccessTokenError"
}
}
Database strategy
Using the database session strategy is very similar, but instead of preserving the access_token
and refresh_token
in the JWT, we will save it in the database by updating the account
value.
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
authorization: { params: { access_type: "offline", prompt: "consent" } },
}),
],
callbacks: {
async session({ session, user }) {
const [googleAccount] = await prisma.account.findMany({
where: { userId: user.id, provider: "google" },
})
if (googleAccount.expires_at * 1000 < Date.now()) {
// If the access token has expired, try to refresh it
try {
// https://accounts.google.com/.well-known/openid-configuration
// We need the `token_endpoint`.
const response = await fetch("https://oauth2.googleapis.com/token", {
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.AUTH_GOOGLE_ID!,
client_secret: process.env.AUTH_GOOGLE_SECRET!,
grant_type: "refresh_token",
refresh_token: googleAccount.refresh_token,
}),
method: "POST",
})
const responseTokens = await response.json()
if (!response.ok) throw responseTokens
await prisma.account.update({
data: {
access_token: responseTokens.access_token,
expires_at: Math.floor(
Date.now() / 1000 + responseTokens.expires_in
),
refresh_token:
responseTokens.refresh_token ?? googleAccount.refresh_token,
},
where: {
provider_providerAccountId: {
provider: "google",
providerAccountId: googleAccount.providerAccountId,
},
},
})
} catch (error) {
console.error("Error refreshing access token", error)
// The error property can be used client-side to handle the refresh token error
session.error = "RefreshAccessTokenError"
}
}
return session
},
},
})
declare module "next-auth" {
interface Session {
error?: "RefreshAccessTokenError"
}
}
declare module "next-auth/jwt" {
interface JWT {
access_token: string
expires_at: number
refresh_token: string
error?: "RefreshAccessTokenError"
}
}
Client Side
The RefreshAccessTokenError
error that is caught in the session
callback is passed to the client. This means that you can direct the user to the sign-in flow if we cannot refresh their token. Don’t forget, calling useSession
client-side, for example, requires your component is wrapped with the <SessionProvider />
.
We can handle this functionality as a side effect:
"use client";
import { useEffect } from "react";
import { signIn, useSession } from "next-auth/react";
const HomePage() {
const { data: session } = useSession();
useEffect(() => {
if (session?.error === "RefreshAccessTokenError") {
signIn(); // Force sign in to hopefully resolve error
}
}, [session]);
return <div>Home Page</div>;
}