Auth.js | Refresh Token Rotation (2024)

Guides

Refresh Token Rotation

šŸ’”

As of today, there is no built-in solution for automatic Refresh Tokenrotation. This guide will help you to achieve this in your application. Ourgoal is to add zero-config support for built-in providers eventually. Let usknow if you would like to help.

What is refresh token rotation?

Refresh token rotation is the practice of updating an access_token on behalf of the user, without requiring interaction (ie.: re-authenticating).access_tokens are usually issued for a limited time. After they expire, the service verifying them will ignore the value, rendering the access_token useless.Instead of asking the user to sign in again to obtain a new access_token, many providers also issue a refresh_token during initial signin, that has a longer expiry date.Auth.js libraries can be configured to use this refresh_token to obtain a new access_token without requiring the user to sign in again.

Implementation

There is an inherent limitation of the following guides that comes from thefact, that - for security reasons - refresh_tokens are usually only usableonce. Meaning that after a successful refresh, the refresh_token will beinvalidated and cannot be used again. Therefore, in some cases, arace-condition might occur if multiple requests will try to refresh the tokenat the same time. The Auth.js team is aware of this and would like to providea solution in the future. This might include some ā€œlockā€ mechanism to preventmultiple requests from trying to refresh the token at the same time, but thatcomes with the drawback of potentially creating a bottleneck in theapplication. Another possible solution is background token refresh, to preventthe token from expiring during an authenticated request.

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 an encrypted JWT inside a cookie or in a database.

JWT strategy

šŸ’”

While using a cookie to store the refresh_token is simpler, it is lesssecure. To mitigate risks with the strategy: "jwt", Auth.js libraries storethe refresh_token in an encrypted JWT, in an HttpOnly cookie. Still, youneed to evaluate based on your requirements which strategy you choose.

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.

./auth.ts

import NextAuth, { type User } from "next-auth"import Google from "next-auth/providers/google" export const { handlers, auth } = NextAuth({ providers: [ Google({ // 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-time login, save the `access_token`, its expiry and the `refresh_token` return { ...token, access_token: account.access_token, expires_at: account.expires_at, refresh_token: account.refresh_token, } } else if (Date.now() < token.expires_at * 1000) { // Subsequent logins, but the `access_token` is still valid return token } else { // Subsequent logins, but the `access_token` has expired, try to refresh it if (!token.refresh_token) throw new TypeError("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", { method: "POST", 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!, }), })  const tokensOrError = await response.json()  if (!response.ok) throw tokensOrError  const newTokens = tokensOrError as { access_token: string expires_in: number refresh_token?: string }  token.access_token = newTokens.access_token token.expires_at = Math.floor( Date.now() / 1000 + newTokens.expires_in ) // Some providers only issue refresh tokens once, so preserve if we did not get a new one if (newTokens.refresh_token) token.refresh_token = newTokens.refresh_token return token } catch (error) { console.error("Error refreshing access_token", error) // If we fail to refresh the token, return an error so we can handle it on the page token.error = "RefreshTokenError" return token } } }, async session ({ session, token }) { session.error = token.error return session }, },}) declare module "next-auth" { interface Session { error?: "RefreshTokenError" }} declare module "next-auth/jwt" { interface JWT { access_token: string expires_at: number refresh_token?: string error?: "RefreshTokenError" }}

Database strategy

Using the database session strategy is similar, but instead we will save the access_token, expires_at and refresh_token on the account for the given provider.

./auth.ts

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({ 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", { method: "POST", 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, }), })  const tokensOrError = await response.json()  if (!response.ok) throw tokensOrError  const newTokens = tokensOrError as { access_token: string expires_in: number refresh_token?: string }  await prisma.account.update({ data: { access_token: newTokens.access_token, expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in), refresh_token: newTokens.refresh_token ?? googleAccount.refresh_token, }, where: { provider_providerAccountId: { provider: "google", providerAccountId: googleAccount.providerAccountId, }, }, }) } catch (error) { console.error("Error refreshing access_token", error) // If we fail to refresh the token, return an error so we can handle it on the page session.error = "RefreshTokenError" } } return session }, },}) declare module "next-auth" { interface Session { error?: "RefreshTokenError" }}

Error handling

If the token refresh was unsuccesful, we can force a re-authentication.

app/dashboard/page.tsx

import { useEffect } from "react"import { auth, signIn } from "@/auth" export default async function Page() { const session = await useSession() if (session?.error === "RefreshTokenError") { await signIn("google") // Force sign in to obtain a new set of access and refresh tokens }}
Creating a Framework IntegrationEdge Compatibility
Auth.js | Refresh Token Rotation (2024)
Top Articles
Budget and Actuals | OpenBudget.NY.Gov
What Does Skill Issue Mean in the Workplace? - HiPeople
Bj ģ‚¬ģŠ“ģ“ ė¶„ģˆ˜
Libiyi Sawsharpener
Restored Republic January 20 2023
Best Restaurants In Seaside Heights Nj
2021 Tesla Model 3 Standard Range Pl electric for sale - Portland, OR - craigslist
Pollen Count Central Islip
Nexus Crossword Puzzle Solver
Things To Do In Atlanta Tomorrow Night
Valentina Gonzalez Leak
2024 Non-Homestead Millage - Clarkston Community Schools
065106619
NHS England Ā» Winter and H2 priorities
Gia_Divine
Recap: Noah Syndergaard earns his first L.A. win as Dodgers sweep Cardinals
Epguides Strange New Worlds
Finalize Teams Yahoo Fantasy Football
All Obituaries | Gateway-Forest Lawn Funeral Home | Lake City FL funeral home and cremation Lake City FL funeral home and cremation
All Obituaries | Verkuilen-Van Deurzen Family Funeral Home | Little Chute WI funeral home and cremation
25 Best Things to Do in Palermo, Sicily (Italy)
Foolproof Module 6 Test Answers
Hctc Speed Test
Poochies Liquor Store
January 8 Jesus Calling
NV Energy issues outage watch for South Carson City, Genoa and Glenbrook
HP PARTSURFER - spare part search portal
Dairy Queen Lobby Hours
La Qua Brothers Funeral Home
Capital Hall 6 Base Layout
Lil Durk's Brother DThang Killed in Harvey, Illinois, ME Confirms
AP Microeconomics Score Calculator for 2023
Unity Webgl Player Drift Hunters
Hingham Police Scanner Wicked Local
Craigslist Mexicali Cars And Trucks - By Owner
Sam's Club Gas Prices Deptford Nj
Torrid Rn Number Lookup
Isabella Duan Ahn Stanford
Mcalister's Deli Warrington Reviews
Mbfs Com Login
Caphras Calculator
John Wick: Kapitel 4 (2023)
Cvs Coit And Alpha
Aurora Southeast Recreation Center And Fieldhouse Reviews
Tito Jackson, member of beloved pop group the Jackson 5, dies at 70
antelope valley for sale "lancaster ca" - craigslist
Runelite Ground Markers
Edict Of Force Poe
Suzanne Olsen Swift River
Latest Posts
Article information

Author: Rev. Porsche Oberbrunner

Last Updated:

Views: 6173

Rating: 4.2 / 5 (53 voted)

Reviews: 84% of readers found this page helpful

Author information

Name: Rev. Porsche Oberbrunner

Birthday: 1994-06-25

Address: Suite 153 582 Lubowitz Walks, Port Alfredoborough, IN 72879-2838

Phone: +128413562823324

Job: IT Strategist

Hobby: Video gaming, Basketball, Web surfing, Book restoration, Jogging, Shooting, Fishing

Introduction: My name is Rev. Porsche Oberbrunner, I am a zany, graceful, talented, witty, determined, shiny, enchanting person who loves writing and wants to share my knowledge and understanding with you.