Monday, December 22, 2025

Implementing True Idle Timeout with Refresh Token Rotation in Next.js + NextAuth

authorImage

Parashar

8 min read
postMainImage

Problem statement:


NextAuth keeps users signed in as long as the tab is open — even if the user hasn’t interacted for hours.
At the same time, enterprise apps often require refresh token rotation, cross-tab safety, and hard idle timeouts.

After going through alot of research on Next Auth discussiuon threat, actually nothing worked for me, And I had to develop my own way around for Next auth refresh token rotation. Issues like race condition, stale token rotation, cross tab synchronization issues were very commen in the approaches proposed on the discussion.

Why NextAuth Alone Is Not Enough

As of today, NextAuth does not provide:

  • A built-in idle timeout
  • A refresh token rotation lifecycle on the client (No official approach/Built in support)
  • Cross-tab coordination for token refresh
  • Protection against reload-during-rotation edge cases

Out of the box, NextAuth:

  • Refreshes sessions when the tab is open
  • Assumes activity ≠ interaction
  • Has no concept of user idleness

So we need to build this on top of NextAuth, not inside it.

High-Level Architecture

We split responsibilities into three isolated systems:

javascript
1┌──────────────────────┐
2Idle Time Guard      │  ← Signs out on real inactivity
3 (react-idle-timer)4└──────────┬───────────┘
56┌──────────▼───────────┐
7Token Refresh Layer  │  ← Keeps access token fresh
8 (Provider)9└──────────┬───────────┘
1011┌──────────▼───────────┐
12TokenRotationManager │  ← Cross-tab + retry logic
13 (Singleton)14└──────────────────────┘
15

Each layer has one job only.

Key Design Rules

Before touching code, these rules shaped everything:

1. Idle timeout ≠ token expiry

A user can:

  • Be active but token expires → refresh
  • Be inactive but token valid → logout

These must be independent systems.

2. No localStorage for auth state

  • Tokens live in NextAuth session
  • Rotation state is in-memory
  • Only one small recovery flag uses sessionStorage (explained later)

3. Cross-tab safety is mandatory

If the user has:

  • 3 tabs open
  • One tab refreshes tokens
  • Another tab must not refresh again

------------------------------------------------------------------------------------------------

Part 1: True Idle Timeout (User Interaction Based)

Why “Idle” Is Hard

Idle does not mean:

  • Token expired
  • Tab inactive
  • Browser minimized

Idle means:

No keyboard, mouse, or touch activity for X minutes

We use react-idle-timer because it:

  • Tracks real user events
  • Supports cross-tab synchronization
  • Works in App Router

Idle Guard Responsibilities

The IdleTimeGuard does only this:

  • Starts a timer when the user is authenticated
  • Resets on interaction
  • When timeout is reached:
    • Ensures token rotation is not running
    • Marks the system as signing out
    • Calls signOut()

Critically:

Idle guard NEVER refreshes tokens

This prevents the classic bug:

“Idle timeout never triggers because token keeps refreshing.”

Cross-Tab Idle Handling

With crossTab: true:

  • Activity in any tab resets idle state
  • Inactivity across all tabs triggers logout

This mirrors real enterprise expectations.

------------------------------------------------------------------------------------------------

Part 2: Token Refresh Provider (Session Watcher)

NextAuth sessions expose:

  • accessTokenExpires
  • refreshToken

But NextAuth will not rotate tokens automatically.

So we build a TokenRefreshProvider that:

  • Watches expiry time
  • Decides when rotation should happen
  • Delegates the how to a manager

What This Provider Does

Every few seconds (or on tab focus):

Check session validity

Ask the manager:

  • Should we rotate?
  • Is rotation already happening?
  • Are retries exhausted?

Trigger rotation when appropriate

Update the NextAuth session after success

Why Not Rotate Immediately on Expiry?

Because:

  • Network failures happen
  • Refresh tokens may still be valid
  • Immediate logout = bad UX

Instead:

  • Rotate before expiry
  • Retry with backoff
  • Only sign out when:
    • Token is expired
    • Retries are exhausted

Part 3: TokenRotationManager (The Brain)

This is the most important piece.

Why a Manager?

Without a manager:

  • Multiple tabs refresh at once
  • Tokens get invalidated
  • Users get logged out randomly

The manager is a singleton, meaning:

  • One instance per tab
  • Shared coordination via BroadcastChannel

Manager Responsibilities

The manager handles:

  • Rotation state (isRotating)
  • Retry tracking
  • Cross-tab communication
  • Grace periods
  • Reload-during-rotation recovery
  • Emergency rotation when expired

Cross-Tab Coordination

Using BroadcastChannel:

  • Tab A starts rotation → broadcasts ROTATION_START
  • Tab B hears it → pauses rotation
  • Tab A succeeds → broadcasts ROTATION_SUCCESS
  • Tab B updates session immediately

This prevents:

  • Duplicate refresh calls
  • Token invalidation races

Handling Page Reload During Rotation

This is a nasty edge case:

Refresh token request is sent

Backend invalidates old refresh token

User reloads page

Old token is now invalid

Next refresh attempt fails

Solution:

  • Before rotation → mark “pending rotation” in sessionStorage
  • On load:
    • If pending marker exists
    • And it’s recent
    • Assume refresh token might be invalid
    • Attempt immediate recovery rotation

This avoids silent auth corruption.

Retry & Backoff Strategy

Rotation failures are expected.

The manager:

  • Retries up to MAX_RETRY_ATTEMPTS
  • Uses exponential backoff
  • Allows emergency rotation if token is already expired
  • Forces logout only when:
    • Token is expired
    • All retries failed

This gives resilience without infinite loops.

Why Idle Timeout Does NOT Care About Tokens

One of the biggest mistakes is tying idle timeout to token expiry.

We explicitly prevent that by:

  • Idle guard checking:
    • Is user authenticated?
    • Is rotation currently running?
  • Never refreshing tokens from idle logic

So:

  • Active user → tokens rotate
  • Inactive user → logout, even if token valid

Code Preview: Idle Timeout + Token Rotation (Conceptual)

⚠️ Note:
These snippets are intentionally incomplete and simplified.
They illustrate how the system is structured, not the full production implementation.

1️⃣ Idle Time Guard (User Interaction Based Logout)

javascript
1"use client"
2
3import { useIdleTimer } from "react-idle-timer"
4import { signOut, useSession } from "next-auth/react"
5
6export function IdleTimeGuard({ children }: { children: React.ReactNode }) {
7  const { status } = useSession()
8
9  const handleIdle = async () => {
10    // Prevent duplicate sign-outs
11    // Skip public routes
12    // Skip if token rotation is in progress
13
14    await signOut({
15      callbackUrl: "/session-expired?reason=idle",
16    })
17  }
18
19  useIdleTimer({
20    timeout: /* idle timeout in ms */,
21    onIdle: handleIdle,
22    crossTab: true,
23    disabled: status !== "authenticated",
24  })
25
26  return <>{children}</>
27}
28

What this shows

  • True inactivity detection
  • Cross-tab idle sync
  • Idle timeout completely decoupled from token expiry

2️⃣ Token Refresh Provider (Session Watcher)

javascript
1"use client"
2
3import { useSession } from "next-auth/react"
4import { useEffect } from "react"
5import { getTokenRotationManager } from "@/lib/token-rotation-manager"
6
7export function TokenRefreshProvider({ children }: { children: React.ReactNode }) {
8  const { data: session, status, update } = useSession()
9  const manager = getTokenRotationManager()
10
11  useEffect(() => {
12    if (status !== "authenticated") return
13
14    const checkAndRotate = async () => {
15      // Skip public routes
16      // Skip if signing out
17      // Skip if another tab is rotating
18
19      if (manager.shouldRotate(session.accessTokenExpires)) {
20        await manager.rotate(
21          session.refreshToken,
22          session.user.roleId
23        )
24      }
25    }
26
27    checkAndRotate()
28    const interval = setInterval(checkAndRotate, /* interval */)
29
30    return () => clearInterval(interval)
31  }, [status, session])
32
33  return <>{children}</>
34}
35

What this shows

  • Token expiry monitoring
  • Delegation to a rotation manager
  • Periodic + on-focus checks

3️⃣ Token Rotation Manager (Singleton, Cross-Tab Safe)

javascript
1class TokenRotationManager {
2  private isRotating = false
3  private failedAttempts = 0
4
5  rotate(refreshToken: string, roleId: number) {
6    if (this.isRotating) return
7
8    this.isRotating = true
9    this.broadcast("ROTATION_START")
10
11    try {
12      // Call refresh token API
13      // Compute new expiry
14      // Update internal state
15
16      this.broadcast("ROTATION_SUCCESS")
17    } catch {
18      this.failedAttempts++
19      this.broadcast("ROTATION_FAIL")
20    } finally {
21      this.isRotating = false
22    }
23  }
24
25  shouldRotate(expiresAt: number) {
26    // Check expiry window
27    // Check grace period
28    // Check backoff
29    return true
30  }
31
32  broadcast(type: string) {
33    // BroadcastChannel communication
34  }
35}
36
37export const getTokenRotationManager = () => {
38  // Return singleton instance
39}
40

What this shows

  • Centralized rotation control
  • Cross-tab awareness
  • Retry-aware design
  • Singleton pattern

4️⃣ Handling Reload During Token Rotation (Edge Case)

javascript
1// Before refresh API call
2sessionStorage.setItem("pending_rotation", Date.now().toString())
3
4// On app load
5const pending = sessionStorage.getItem("pending_rotation")
6
7if (pending) {
8  // Page likely reloaded during rotation
9  // Attempt immediate recovery refresh
10  sessionStorage.removeItem("pending_rotation")
11}
12

Why this matters

  • Prevents silent logout
  • Handles backend invalidation safely
  • One of the most commonly missed cases

How These Pieces Work Together

javascript
1User Activity ───▶ Idle Guard ───▶ Sign Out
234Token Expiry ───▶ Refresh Provider ───▶ Rotation Manager
567                        Cross-Tab Coordination
8
Leave a Comment
Share your thoughts and join the conversation

Comments (1)

Parashar neupane

Hey, Share your thoughts👋

You may also like

mainImage

How I created a Free Stack AI Support System

authorImage

Parashar

Saturday, January 24, 2026