Skip to main content
AuthKit Next.js provides robust session management that automatically handles token refresh, session validation, and secure storage. This page explains how sessions work and how to interact with them.

Session architecture

A session in AuthKit consists of encrypted data stored in an HTTP-only cookie containing:
  • Access token - Short-lived JWT with user and authorization data
  • Refresh token - Long-lived token for obtaining new access tokens
  • User object - Basic user information (id, email, name, etc.)
  • Impersonator - Optional impersonation context (email and reason)
interface Session {
  accessToken: string;
  refreshToken: string;
  user: User;
  impersonator?: Impersonator;
}
Sessions are encrypted using iron-session with AES-256-GCM encryption. The cookie itself is unreadable without your WORKOS_COOKIE_PASSWORD.

Token lifecycle

Access tokens

Access tokens are JWTs that contain all the authorization data your application needs:
interface AccessToken {
  sid: string;              // Session ID
  org_id?: string;          // Organization ID
  role?: string;            // User role
  roles?: string[];         // Multiple roles
  permissions?: string[];   // User permissions
  entitlements?: string[];  // Active entitlements
  feature_flags?: string[]; // Enabled feature flags
}
Access tokens have a short lifetime (typically 5-10 minutes) and are automatically refreshed by the middleware.

Token verification

Before trusting an access token, AuthKit verifies it against WorkOS’s public JWKS:
import { jwtVerify, createRemoteJWKSet } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL(workos.userManagement.getJwksUrl(clientId))
);

const hasValidSession = await jwtVerify(accessToken, JWKS);
This ensures:
  • Token was issued by WorkOS
  • Token hasn’t been tampered with
  • Token signature is valid
  • Token hasn’t expired

Refresh tokens

Refresh tokens are opaque strings that can be exchanged for new access tokens:
const { accessToken, refreshToken, user, impersonator } = 
  await workos.userManagement.authenticateWithRefreshToken({
    clientId: WORKOS_CLIENT_ID,
    refreshToken: session.refreshToken,
    organizationId: session.organizationId,
  });
Refresh tokens:
  • Are automatically used by middleware when access tokens expire
  • Remain valid until explicitly revoked
  • Are stored encrypted in the session cookie
  • Cannot be read by client-side JavaScript

Automatic session refresh

The AuthKit middleware automatically handles token refresh without any action from your application.

How refresh works

1

Access token expiry detected

Middleware attempts to verify the access token and detects it’s expired
2

Refresh token exchange

Middleware calls WorkOS API to exchange refresh token for new tokens
3

Session update

New tokens are encrypted and saved to the session cookie
4

Request continues

Original request continues with fresh session data
Here’s the implementation from session.ts:262-325:
try {
  // Access token expired, refresh it
  const { accessToken, refreshToken, user, impersonator } =
    await getWorkOS().userManagement.authenticateWithRefreshToken({
      clientId: WORKOS_CLIENT_ID,
      refreshToken: session.refreshToken,
      organizationId: organizationIdFromAccessToken,
    });

  // Encrypt session with new tokens
  const encryptedSession = await encryptSession({
    accessToken,
    refreshToken,
    user,
    impersonator,
  });

  // Update cookie
  headers.append(
    'Set-Cookie',
    `${cookieName}=${encryptedSession}; ${getCookieOptions(request.url, true)}`
  );
  
  // Call success hook if provided
  options.onSessionRefreshSuccess?.({
    accessToken,
    user,
    impersonator,
    organizationId,
  });

  return { session: userInfo, headers };
} catch (e) {
  // Refresh failed, delete session and redirect to sign in
  options.onSessionRefreshError?.({ error: e, request });
  // ... cookie deletion logic
}
If refresh token validation fails (token expired, revoked, or invalid), the session is deleted and the user must sign in again.

Refresh hooks

You can hook into the refresh process to track when sessions are refreshed or when refresh fails:
middleware.ts
import { authkitMiddleware } from '@workos-inc/authkit-nextjs';

export default authkitMiddleware({
  onSessionRefreshSuccess: ({ accessToken, user, organizationId }) => {
    console.log(`Session refreshed for user ${user.email}`);
  },
  onSessionRefreshError: ({ error, request }) => {
    console.error('Failed to refresh session:', error);
    // Send to monitoring service
  },
});

Accessing session data

Using withAuth()

The primary way to access session data is through the withAuth() helper:
app/dashboard/page.tsx
import { withAuth } from '@workos-inc/authkit-nextjs';

export default async function DashboardPage() {
  const { user, organizationId, permissions } = await withAuth();
  
  return (
    <div>
      <h1>Welcome, {user.firstName}!</h1>
      <p>Organization: {organizationId}</p>
    </div>
  );
}
The withAuth() function returns:
interface UserInfo {
  user: User;                  // Full user object
  sessionId: string;           // Unique session identifier
  organizationId?: string;     // Current organization
  role?: string;               // Deprecated single role
  roles?: string[];            // Array of roles
  permissions?: string[];      // User permissions
  entitlements?: string[];     // Active entitlements
  featureFlags?: string[];     // Enabled feature flags
  impersonator?: Impersonator; // Impersonation context
  accessToken: string;         // Raw access token
}
withAuth() requires the middleware to be running on the route. If middleware isn’t configured, you’ll get an error.

Handling signed-out users

By default, withAuth() returns { user: null } for signed-out users:
const session = await withAuth();

if (!session.user) {
  // User is signed out
  return <SignInPrompt />;
}

// User is signed in
return <Dashboard user={session.user} />;
To automatically redirect signed-out users:
const { user } = await withAuth({ ensureSignedIn: true });
// User is guaranteed to be signed in here

Reading session from cookies

In advanced scenarios, you can read the session directly from cookies without middleware:
import { getSessionFromCookie } from '@workos-inc/authkit-nextjs';

const session = await getSessionFromCookie();

if (session) {
  const { user, accessToken } = session;
  // Use session data
}
Direct cookie access bypasses automatic refresh. Use withAuth() instead unless you have a specific reason not to.

Manual session refresh

You can manually refresh a session to update user data or switch organizations.

Refreshing current session

import { refreshSession } from '@workos-inc/authkit-nextjs';

const updatedSession = await refreshSession();
This exchanges the refresh token for new tokens, which updates:
  • User profile information
  • Organization membership
  • Permissions and roles
  • Entitlements and feature flags

Switching organizations

To switch the user’s active organization:
import { switchToOrganization } from '@workos-inc/authkit-nextjs';

const session = await switchToOrganization('org_456', {
  returnTo: '/dashboard',
  revalidationStrategy: 'path',
});
This refreshes the session with the new organization context and optionally revalidates the page cache.
If the organization requires SSO enrollment or MFA, the user will be redirected to AuthKit to complete those requirements.

Session termination

Signing out

To terminate a session and sign out the user:
import { signOut } from '@workos-inc/authkit-nextjs';

await signOut({ returnTo: '/goodbye' });
This performs several actions:
1

Extract session ID

Retrieves the session ID from the access token
2

Delete cookie

Removes the session cookie from the browser
3

Revoke session

Calls WorkOS to revoke the session server-side
4

Redirect

Redirects to the WorkOS logout URL, then back to your app
Implementation from auth.ts:80-108:
export async function signOut({ returnTo }: { returnTo?: string } = {}) {
  let sessionId: string | undefined;

  try {
    const { sessionId: sid } = await withAuth();
    sessionId = sid;
  } catch (error) {
    // Fallback: read session directly from cookie
    const session = await getSessionFromCookie();
    if (session && session.accessToken) {
      const { sid } = decodeJwt<AccessToken>(session.accessToken);
      sessionId = sid;
    }
  } finally {
    const nextCookies = await cookies();
    const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
    nextCookies.delete({ name: cookieName, ...cookieOptions });

    if (sessionId) {
      redirect(
        workos.userManagement.getLogoutUrl({ sessionId, returnTo })
      );
    } else {
      redirect(returnTo ?? '/');
    }
  }
}

Session revocation

WorkOS can revoke sessions server-side if:
  • User changes their password
  • Admin terminates the session
  • Session is compromised
  • Organization requires re-authentication
When a revoked refresh token is used, the middleware will detect the error and delete the local session.

Custom session storage

For advanced use cases, you can manually save sessions after custom authentication flows:
import { saveSession } from '@workos-inc/authkit-nextjs';
import { workos } from './workos';

export async function POST(request: NextRequest) {
  const { code } = await request.json();
  
  // Custom authentication (e.g., email verification)
  const authResponse = await workos.userManagement
    .authenticateWithEmailVerification({
      clientId: process.env.WORKOS_CLIENT_ID!,
      code,
    });
  
  // Save the session
  await saveSession(authResponse, request);
  
  return NextResponse.json({ success: true });
}
Manual session management bypasses standard security checks. Only use this for authenticated flows that don’t go through the standard callback handler.
Cookie behavior is controlled by environment variables:
.env.local
# Cookie name (default: wos-session)
WORKOS_COOKIE_NAME=my-session

# Encryption password (required, min 32 characters)
WORKOS_COOKIE_PASSWORD=complex-secret-at-least-32-characters-long

# Max age in seconds (default: 400 days)
WORKOS_COOKIE_MAX_AGE=34560000

# Domain for cookie (default: current domain)
WORKOS_COOKIE_DOMAIN=.example.com

# SameSite setting (default: lax)
WORKOS_COOKIE_SAMESITE=strict
Cookies are configured with security-first defaults from cookie.ts:82-93:
return {
  path: '/',
  httpOnly: true,           // No JavaScript access
  secure: isHttps,          // HTTPS only (except localhost)
  sameSite: 'lax',          // CSRF protection
  maxAge: 60 * 60 * 24 * 400, // 400 days (Chrome maximum)
  domain: WORKOS_COOKIE_DOMAIN || undefined,
};
The long cookie expiry (400 days) is safe because access tokens expire quickly. The refresh token controls the actual session lifetime.

Debugging sessions

Enable debug mode to see session operations in your server logs:
middleware.ts
export default authkitMiddleware({
  debug: true,
});
This logs:
  • Session refresh attempts
  • Token expiration details
  • Refresh success/failure
  • Session validation results

Next steps

Middleware

Learn how middleware protects routes and manages sessions

Authentication flow

Understand the complete authentication process

Build docs developers (and LLMs) love