Implementing Secure Refresh Token Rotation in Next.js 14 with NextAuth.js v5 (Auth.js) and Prisma

User avatar placeholder
Written by Tamzid Ahmed

August 26, 2026

Refresh token rotation is one of the most effective defenses against stolen access tokens, yet many Next.js applications still rely on static JWTs that never expire. With the release of NextAuth.js v5 (now branded as Auth.js) and the official Prisma adapter, you can implement a fully database-backed rotation flow without hand-rolling crypto or session tables. This guide walks you through a production-grade setup using Next.js 14, including the Prisma schema, Auth.js callbacks, and reuse-detection logic.

What Is Refresh Token Rotation?

Refresh token rotation is a security pattern in which a single-use refresh token is exchanged for a new access token and a new refresh token on every request. If a previously rotated token is reused, the system flags it as compromised and invalidates the entire session family. Unlike a static refresh token, which remains valid until its expiry, rotated tokens offer a narrow attack window and an automatic breach signal.

This pattern is the same one used by major SaaS platforms such as Auth0, Clerk, and Workos. Adopting it in your own stack closes one of the most common OAuth vulnerabilities: long-lived bearer tokens sitting in browser or mobile storage with no accountability.

Why NextAuth.js v5 (Auth.js) Changes the Game

NextAuth.js v5 introduces first-class TypeScript support, a unified auth() helper, and a streamlined configuration model that works across server components, route handlers, and middleware. Combined with the Prisma adapter (@auth/prisma-adapter), you get durable session storage that makes rotation auditable, revocable, and observable from day one.

Most importantly, v5 standardizes the events and callbacks APIs in a way that makes intercepting provider refresh responses straightforward — the exact hook point you need for a clean token rotation flow. Migration friction from v4 is minimal, and the v5 docs explicitly call out database session strategies for security-sensitive workloads.

Prerequisites and Project Setup

Before writing any code, confirm you have the following in place:

  • Node.js 18.17+ and a Next.js 14 project (App Router preferred)
  • A PostgreSQL or MySQL database reachable from your app
  • Prisma CLI installed via npm i prisma -D
  • next-auth@beta (v5) and @auth/prisma-adapter as runtime dependencies
  • An OAuth provider configured in your Auth.js dashboard (Google, GitHub, or Auth0 work well for testing)

Initialize Prisma with npx prisma init and add the standard Auth.js models to your schema. The full base schema is well-documented, but for rotation you will extend the Account model to track token lineage.

Extending the Prisma Schema for Token Tracking

Auth.js requires User, Account, Session, and VerificationToken models. For refresh token rotation, add two optional fields to the Account model so you can track rotation history and detect stale tokens.

model Account {
  id                String   @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String?  @db.Text
  access_token      String?  @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String?  @db.Text
  session_state     String?
  tokenVersion      Int      @default(0)
  lastRotatedAt     DateTime?
  user              User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@unique([provider, providerAccountId])
}

The tokenVersion field is your rotation counter. Every successful refresh increments it, which lets you detect stale or replayed tokens on the next request. The lastRotatedAt timestamp is useful for telemetry and for identifying accounts whose rotation has stalled.

Choosing the Right Session Strategy

Auth.js supports two session strategies, and choosing the right one is critical for token rotation.

  • JWT strategy: stateless, fast, and edge-compatible — but harder to revoke mid-flight because there is no server-side record of active sessions.
  • Database strategy: stateful, queryable, and the only reliable option for refresh token rotation because every refresh updates a row you can audit.

Set session: { strategy: "database" } in your auth.config.ts. This guarantees each refresh lands in a row you can index, monitor, and invalidate at will.

Implementing the Rotation Flow in Auth.js v5

The rotation logic lives in two places: the signIn callback for the initial provider response, and a custom refresh handler for ongoing rotation. The combination is what makes the system auditable end-to-end.

Hooking the signIn callback

The signIn callback runs after a successful OAuth or credentials response. Use it to persist the latest refresh_token and expires_at, replacing the previous values atomically inside a Prisma transaction. Increment tokenVersion here so subsequent requests can detect anomalies.

Building a custom refresh handler

Create a route handler at app/api/auth/refresh/route.ts that reads the current session, calls the provider’s token endpoint with the stored refresh token, and writes the new pair to the database. If the incoming tokenVersion does not match the stored version, treat it as a reuse event and revoke the session family.

Detecting Token Reuse and Revoking Sessions

A robust rotation system treats any replayed refresh token as a breach signal. When a previously rotated token arrives, the handler should execute the following sequence:

  1. Compare the request’s tokenVersion against the stored tokenVersion.
  2. If the incoming version is less than the stored version, mark the account as compromised.
  3. Delete all Session rows for the user and force a re-authentication on the next request.
  4. Emit a structured log event so your SIEM or observability stack can alert on the incident.

This pattern is sometimes called refresh token theft detection, and it is recommended by RFC 6749 and the OAuth 2.0 Security Best Current Practice document for high-value applications.

Security Best Practices and Common Pitfalls

  • Always store refresh tokens encrypted at rest using column-level encryption or a KMS-managed key.
  • Set short access-token TTLs (5–15 minutes) and refresh-token TTLs of 7–30 days.
  • Bind refresh tokens to a client fingerprint — a hash of the user-agent and IP — when feasible.
  • Rotate the refresh token on every use, and never log the raw token value to the console.
  • Set SameSite=Lax and Secure cookie flags in production, and serve the app exclusively over HTTPS.
  • Avoid storing refresh tokens in localStorage; prefer httpOnly cookies that JavaScript cannot read.

Testing the Rotation Flow

Write integration tests with Vitest or Playwright that exercise three scenarios:

  1. Sign in and capture the initial refresh token and tokenVersion.
  2. Trigger a refresh and assert that a new token is returned with an incremented version.
  3. Replay the old token and assert a 401 response accompanied by a session-revocation event in the database.

Wrap the update and the revocation log in a Prisma $transaction call so the two writes commit atomically. A partial write here would leave your system in an inconsistent state and silently disable reuse detection — the exact failure mode attackers exploit.

Conclusion

Refresh token rotation transforms your authentication layer from a soft target into a hardened perimeter, and NextAuth.js v5 with Prisma makes the implementation far simpler than it was in v4. Audit your current token flow today, add reuse detection, and treat any replayed token as an incident worth investigating. Start by extending the Account model, switching to the database session strategy, and writing a single test that proves your rotation works under hostile conditions — security is only as strong as the code that verifies it.

Leave a Comment