After successful authentication, the Auth0 Next.js SDK creates and manages a user session. The SDK supports two session storage strategies: stateless (cookie-based) and stateful (database-backed).
Session Data Structure
The session contains all authentication state for the current user:
interface SessionData {
user: User; // User profile from ID token
tokenSet: TokenSet; // OAuth tokens
internal: { // SDK metadata
sid: string; // Session ID from ID token
createdAt: number; // Unix timestamp
};
accessTokens?: AccessTokenSet[]; // MRRT tokens (optional)
connectionTokenSets?: ConnectionTokenSet[]; // Federated tokens (optional)
mfaContext?: MfaContext; // MFA state (optional)
}
interface TokenSet {
accessToken: string; // API access token
idToken: string; // User identity token (JWT)
refreshToken?: string; // Token for refresh
expiresAt?: number; // Unix timestamp
scope?: string; // Granted scopes
audience?: string; // API audience
requestedScope?: string; // Originally requested scopes
token_type?: string; // Usually "Bearer"
}
interface User {
sub: string; // Unique user ID
name?: string; // Full name
email?: string; // Email address
picture?: string; // Profile picture URL
[key: string]: any; // Custom claims
}
Storage Strategies
Stateless Sessions (Default)
Session data is encrypted and stored entirely in cookies. This is the default mode and requires no external dependencies.
Advantages:
- Zero external dependencies
- Simple deployment
- Automatic scaling (no shared state)
- Fast session retrieval
Limitations:
- Cookie size limits (~4KB per cookie)
- Cannot revoke sessions server-side
- All session data travels with every request
Configuration:
import { Auth0Client } from "@auth0/nextjs-auth0/server";
export const auth0 = new Auth0Client({
// Stateless is the default - no sessionStore needed
session: {
rolling: true,
absoluteDuration: 60 * 60 * 24 * 7, // 7 days
inactivityDuration: 60 * 60 * 24, // 1 day
cookie: {
name: '__session',
sameSite: 'lax',
secure: true,
path: '/'
}
}
});
Storage mechanism:
The SDK automatically chunks large sessions across multiple cookies if needed.
If your session exceeds browser cookie limits (~20 cookies or ~4KB per cookie), consider using stateful sessions or reducing session size by removing unnecessary custom claims.
Stateful Sessions
Session data is stored in an external database, with only a session ID stored in the cookie.
Advantages:
- No cookie size limitations
- Server-side session revocation
- Reduced bandwidth (small cookie)
- Centralized session management
Disadvantages:
- Requires external data store
- Additional latency for database queries
- More complex infrastructure
Configuration:
import { Auth0Client } from "@auth0/nextjs-auth0/server";
import { SessionDataStore } from "@auth0/nextjs-auth0/types";
// Implement the SessionDataStore interface
class RedisSessionStore implements SessionDataStore {
async get(id: string): Promise<SessionData | null> {
const data = await redis.get(`session:${id}`);
return data ? JSON.parse(data) : null;
}
async set(id: string, session: SessionData): Promise<void> {
await redis.set(
`session:${id}`,
JSON.stringify(session),
'EX',
60 * 60 * 24 * 7 // 7 days TTL
);
}
async delete(id: string): Promise<void> {
await redis.del(`session:${id}`);
}
// Optional: Required for back-channel logout
async deleteByLogoutToken(claims: LogoutToken): Promise<void> {
// Find and delete sessions matching the logout token
const sessionIds = await redis.keys('session:*');
for (const key of sessionIds) {
const session = await this.get(key.replace('session:', ''));
if (session?.user.sub === claims.sub) {
await redis.del(key);
}
}
}
}
export const auth0 = new Auth0Client({
sessionStore: new RedisSessionStore()
});
Storage mechanism:
Rolling Sessions
Rolling sessions extend the session lifetime with each request, up to an absolute maximum.
export const auth0 = new Auth0Client({
session: {
rolling: true, // Enable rolling sessions
absoluteDuration: 60 * 60 * 24 * 7, // Max 7 days from creation
inactivityDuration: 60 * 60 * 24 // Expires after 1 day of inactivity
}
});
How it works:
With rolling sessions:
- Active users remain authenticated as long as they use the app within the inactivity window
- Inactive users are automatically logged out after the inactivity duration
- Maximum lifetime prevents indefinite sessions
Without rolling sessions (rolling: false):
- Session expires after
absoluteDuration regardless of activity
- Users must re-login after the fixed duration
- More predictable but less user-friendly
The SDK’s middleware automatically touches sessions on every request when rolling sessions are enabled, extending the session lifetime. This is why the middleware matcher should include all protected routes.
Session Cookie Configuration
Cookie Attributes
export const auth0 = new Auth0Client({
session: {
cookie: {
name: '__session', // Cookie name
sameSite: 'lax', // CSRF protection
secure: true, // HTTPS only (recommended)
path: '/', // Cookie path
domain: undefined, // Cookie domain (optional)
transient: false // Session vs persistent cookie
}
}
});
Or via environment variables:
AUTH0_COOKIE_DOMAIN=example.com
AUTH0_COOKIE_PATH=/
AUTH0_COOKIE_TRANSIENT=false
AUTH0_COOKIE_SECURE=true
AUTH0_COOKIE_SAME_SITE=lax
Cookie Attributes Explained
| Attribute | Values | Purpose |
|---|
sameSite | strict, lax, none | CSRF protection level |
secure | true, false | Require HTTPS |
httpOnly | Always true | Prevent JavaScript access |
path | Any path | Cookie scope |
domain | Domain string | Share across subdomains |
transient | true, false | Session vs persistent cookie |
Security Best Practices:
- Always use
secure: true in production (HTTPS)
- Keep
httpOnly: true (hardcoded by SDK) to prevent XSS attacks
- Use
sameSite: 'lax' or 'strict' for CSRF protection
- Only set
sameSite: 'none' if you need cross-site cookies (requires secure: true)
Accessing Sessions
Server Components (App Router)
import { auth0 } from '@/lib/auth0';
export default async function Page() {
const session = await auth0.getSession();
if (!session) {
return <div>Not authenticated</div>;
}
return <div>Hello {session.user.name}</div>;
}
API Routes (App Router)
import { auth0 } from '@/lib/auth0';
import { NextResponse } from 'next/server';
export async function GET() {
const session = await auth0.getSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({ user: session.user });
}
Pages Router
import { auth0 } from '@/lib/auth0';
import type { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const session = await auth0.getSession(req);
if (!session) {
return { props: { user: null } };
}
return { props: { user: session.user } };
};
Client Components
'use client';
import { useUser } from '@auth0/nextjs-auth0';
export default function Profile() {
const { user, isLoading, error } = useUser();
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>Not authenticated</div>;
return <div>Hello {user.name}</div>;
}
The useUser() hook fetches user data from the /auth/profile route, which reads from the server-side session.
Updating Sessions
You can modify session data after authentication:
import { auth0 } from '@/lib/auth0';
import { NextResponse } from 'next/server';
export async function POST() {
const session = await auth0.getSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Update session with custom data
await auth0.updateSession({
...session,
user: {
...session.user,
lastVisit: new Date().toISOString()
}
});
return NextResponse.json({ success: true });
}
Session updates are overwritten when the user re-authenticates or when tokens are refreshed. For persistent user metadata, store it in your database or use Auth0’s user metadata feature.
Token Refresh
When a refresh token is available, the SDK automatically refreshes expired access tokens:
// Automatic refresh when token is expired
const accessToken = await auth0.getAccessToken();
// Force refresh even if not expired
const accessToken = await auth0.getAccessToken({ refresh: true });
Refresh flow:
Refresh Token Rotation: If your Auth0 application uses Refresh Token Rotation, configure an overlap period in your Auth0 Dashboard to prevent race conditions when multiple requests attempt to refresh tokens simultaneously.
Session Security
Encryption
All session data is encrypted using JWE (JSON Web Encryption) with the AUTH0_SECRET:
# Generate a secure secret (32+ bytes)
openssl rand -hex 32
Session Lifecycle
Best Practices
- Use strong secrets: Generate with
openssl rand -hex 32
- Enable HTTPS: Set
secure: true in production
- Configure SameSite: Use
'lax' or 'strict' for CSRF protection
- Set reasonable durations: Balance security and user experience
- Monitor session size: Keep under 4KB for cookie-based sessions
- Implement logout: Clear sessions when users log out
- Use refresh tokens: Enable offline_access scope for long-lived sessions
- Handle token expiration: Refresh tokens before they expire
Multi-Resource Refresh Tokens (MRRT)
When calling multiple APIs, the SDK stores separate access tokens per audience:
// Default audience token
const defaultToken = await auth0.getAccessToken();
// Different audience token
const apiToken = await auth0.getAccessToken({
audience: 'https://api.example.com'
});
The session structure with MRRT:
{
tokenSet: {
accessToken: 'token-for-default-audience',
refreshToken: 'refresh-token',
// ...
},
accessTokens: [
{
accessToken: 'token-for-api1',
audience: 'https://api1.example.com',
scope: 'read:data',
expiresAt: 1234567890
},
{
accessToken: 'token-for-api2',
audience: 'https://api2.example.com',
scope: 'read:data write:data',
expiresAt: 1234567890
}
]
}
Session Debugging
To inspect session contents during development:
import { auth0 } from '@/lib/auth0';
export async function GET() {
const session = await auth0.getSession();
console.log('Session data:', {
user: session?.user,
tokenSet: {
hasAccessToken: !!session?.tokenSet.accessToken,
hasRefreshToken: !!session?.tokenSet.refreshToken,
expiresAt: session?.tokenSet.expiresAt,
scope: session?.tokenSet.scope
},
createdAt: session?.internal.createdAt,
age: session ? Date.now() / 1000 - session.internal.createdAt : 0
});
return Response.json({ ok: true });
}
Next Steps