Netlify caching nextJS user sessions

Hi all

I have a few sites ive made in nextJS (using AI)

And these 2 the last person signed in their session shows in the headers of the sites to anyone not logged in

Ive gone through every AI help topic i could find but i just cant seem to stop the session from returning back from the server into the cookies and the logout function doesnt work it keeps returning a logged session

ive gone mental with netlify toml headers and i just can’t seem to understand how this worls

Sorry im not an actual developer i use AI in VSCODE and i can get most things done but this one has been a week now and i cant resolve it

Example of one toml

[[scheduled-functions]]
function = “cleanup-sessions”
schedule = “@daily

Default cache control for all pages

[[headers]]
for = “/*”
[headers.values]
Cache-Control = “no-store, private, max-age=0, must-revalidate”
Pragma = “no-cache”
Expires = “0”
Surrogate-Control = “no-store”
Vary = “Cookie, Authorization, Accept-Encoding”
Netlify-Vary = “cookie=next-auth.session-token”

Public pages cache control

[[headers]]
for = “/:placeholder(|jobs|directory/|blog/)”
[headers.values]
Cache-Control = “public, no-store, max-age=0, must-revalidate”
Pragma = “no-cache”
Expires = “0”
Surrogate-Control = “no-store”
Vary = “*”

Authenticated routes cache control

[[headers]]
for = “/:section(employers|jobseeker)/*”
[headers.values]
Cache-Control = “private, no-cache, no-store, max-age=0, must-revalidate”
Pragma = “no-cache”
Expires = “0”
Surrogate-Control = “no-store”
Vary = “Cookie, Authorization, Accept-Encoding”
Netlify-Vary = “cookie=next-auth.session-token”
X-Cache-Policy = “auth-route-strict”

Netlify functions cache control

[[headers]]
for = “/.netlify/functions/*”
[headers.values]
Cache-Control = “private, no-cache, no-store, max-age=0, must-revalidate”
Pragma = “no-cache”
Expires = “0”
Surrogate-Control = “no-store”
Vary = “Cookie, Authorization, Accept-Encoding”
Netlify-Vary = “cookie=next-auth.session-token”

API routes cache control

[[headers]]
for = “/api/*”
[headers.values]
Cache-Control = “private, no-cache, no-store, max-age=0, must-revalidate”
Pragma = “no-cache”
Expires = “0”
Surrogate-Control = “no-store”
Vary = “Cookie, Authorization, Accept-Encoding”
Netlify-Vary = “cookie=next-auth.session-token”

Auth endpoints cache control

[[headers]]
for = “/api/auth/*”
[headers.values]
Cache-Control = “private, no-cache, no-store, max-age=0, must-revalidate”
Pragma = “no-cache”
Expires = “0”
Surrogate-Control = “no-store”
Vary = “Cookie, Authorization, Accept-Encoding”
Netlify-Vary = “cookie=next-auth.session-token”
X-Cache-Policy = “auth-api-strict”

Session endpoint - extra strict no-cache

[[headers]]
for = “/api/auth/session”
[headers.values]
Cache-Control = “private, no-cache, no-store, max-age=0, must-revalidate”
Pragma = “no-cache”
Expires = “0”
Surrogate-Control = “no-store”
Vary = “Cookie, Authorization, Accept-Encoding, *”
Netlify-Vary = “cookie=next-auth.session-token”
X-Cache-Policy = “session-api-strict”
X-No-Cache = “true”

Login pages cache control

[[headers]]
for = “/auth/login*”
[headers.values]
Cache-Control = “private, no-cache, no-store, max-age=0, must-revalidate”
Pragma = “no-cache”
Expires = “0”
Surrogate-Control = “no-store”
Vary = “*”
X-Cache-Policy = “login-page-strict”

Special handling for logout

[[redirects]]
from = “/api/auth/logout”
to = “/.netlify/builders/api/auth/logout”
status = 200
force = true

Special handling for auth session

[[redirects]]
from = “/api/auth/session”
to = “/.netlify/builders/api/auth/session”
status = 200
force = true

Special handling for pages with logout parameter

[[redirects]]
from = “/*?logout=
to = “/api/auth/logout”
status = 200
force = true

– and middleware:

import { NextResponse } from ‘next/server’;
import type { NextRequest } from ‘next/server’;
import { jwtVerify } from ‘jose’;
import { getToken } from ‘next-auth/jwt’;

// Helper function to create a redirect response with cache control headers
function createRedirectResponse(url: URL): NextResponse {
const redirectResponse = NextResponse.redirect(url);
redirectResponse.headers.set(‘Cache-Control’, ‘no-store, max-age=0, must-revalidate’);
redirectResponse.headers.set(‘Pragma’, ‘no-cache’);
redirectResponse.headers.set(‘Expires’, ‘0’);
redirectResponse.headers.set(‘Surrogate-Control’, ‘no-store’);
return redirectResponse;
}

export async function middleware(request: NextRequest) {
// Check if this is a public page that should never have auth cookies
const isPublicPage =
request.nextUrl.pathname === ‘/’ ||
request.nextUrl.pathname === ‘/jobs’ ||
request.nextUrl.pathname.startsWith(‘/directory’) ||
request.nextUrl.pathname.startsWith(‘/blog’) ||
request.nextUrl.pathname.startsWith(‘/about’) ||
request.nextUrl.pathname.startsWith(‘/auth/’) ||
(request.nextUrl.pathname.startsWith(‘/employers/’) &&
(request.nextUrl.pathname.includes(‘/login’) ||
request.nextUrl.pathname.includes(‘/signup’))) ||
(request.nextUrl.pathname.startsWith(‘/jobseeker/’) &&
(request.nextUrl.pathname.includes(‘/login’) ||
request.nextUrl.pathname.includes(‘/signup’)));

// For public pages, create a clean response with no auth cookies
if (isPublicPage && !request.cookies.has(‘_vercel_no_cookie’)) {
const response = NextResponse.next();

// Add a special cookie to prevent infinite loops
response.cookies.set('_vercel_no_cookie', '1', {
  maxAge: 10,
  path: '/',
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'lax'
});

// Set strict cache control headers
response.headers.set('Cache-Control', 'no-store, private, max-age=0, must-revalidate');
response.headers.set('Pragma', 'no-cache');
response.headers.set('Expires', '0');
response.headers.set('Vary', 'Cookie, Authorization');

// Clear any auth cookies that might have been set
response.cookies.delete('next-auth.session-token');
response.cookies.delete('next-auth.callback-url');
response.cookies.delete('next-auth.csrf-token');
response.cookies.delete('__Host-next-auth.csrf-token');
response.cookies.delete('__Secure-next-auth.callback-url');

return response;

}

// For all other responses, add cache control headers
const response = NextResponse.next();

// Add strict cache control headers to all responses
response.headers.set(‘Cache-Control’, ‘no-store, private, max-age=0, must-revalidate’);
response.headers.set(‘Pragma’, ‘no-cache’);
response.headers.set(‘Expires’, ‘0’);
response.headers.set(‘Surrogate-Control’, ‘no-store’); // Specific for Netlify CDN
response.headers.set(‘Vary’, ‘Cookie, Authorization, Accept-Encoding, *’);

// Add a unique header to prevent caching based on user
response.headers.set(‘X-User-Unique’, crypto.randomUUID());

// Add a timestamp to ensure the response is always different
response.headers.set(‘X-Response-Time’, Date.now().toString());
response.headers.set(‘X-Cache-Control-Debug’, ‘middleware-default’);

// Check if this is a logout request (has logout query parameter)
const isLogout = request.nextUrl.searchParams.has(‘logout’);

if (isLogout) {
// For logout requests, add extremely strict cache control headers
response.headers.set(‘Cache-Control’, ‘no-store, no-cache, max-age=0, must-revalidate’);
response.headers.set(‘Pragma’, ‘no-cache’);
response.headers.set(‘Expires’, ‘0’);
response.headers.set(‘Surrogate-Control’, ‘no-store’);
response.headers.set(‘X-Cache-Control-Debug’, ‘middleware-logout’);

// Clear all possible session cookies
response.cookies.delete('next-auth.session-token');
response.cookies.delete('next-auth.callback-url');
response.cookies.delete('next-auth.csrf-token');
response.cookies.delete('__Host-next-auth.csrf-token');
response.cookies.delete('__Secure-next-auth.callback-url');
response.cookies.delete('__Secure-next-auth.session-token');

// Clear cookies with standard path (Next.js doesn't support custom paths in delete)
response.cookies.delete('next-auth.session-token');
response.cookies.delete('next-auth.callback-url');
response.cookies.delete('next-auth.csrf-token');
response.cookies.delete('__Host-next-auth.csrf-token');
response.cookies.delete('__Secure-next-auth.callback-url');
response.cookies.delete('__Secure-next-auth.session-token');

// Set special headers to indicate this is a logout response
response.headers.set('X-Logout-Response', 'true');
response.headers.set('X-Logout-Time', Date.now().toString());

}

// Skip middleware for auth callbacks and API routes
if (
request.nextUrl.pathname.startsWith(‘/api/auth’) ||
request.nextUrl.pathname.includes(‘/callback’)
) {
return response;
}

// Handle admin routes
if (request.nextUrl.pathname.startsWith(‘/management’)) {
const token = request.cookies.get(‘admin_token’)?.value;

// Allow access to login page
if (request.nextUrl.pathname === '/management/secure-login') {
  return response;
}

// Redirect to login if no token
if (!token) {
  return createRedirectResponse(new URL('/management/secure-login', request.url));
}

try {
  const secret = new TextEncoder().encode(process.env.ADMIN_JWT_SECRET);
  await jwtVerify(token, secret);
  return response;
} catch (error) {
  console.error('Admin token verification failed:', error);
  // Clear the invalid token
  const redirectResponse = createRedirectResponse(new URL('/management/secure-login', request.url));
  redirectResponse.cookies.set({
    name: 'admin_token',
    value: '',
    expires: new Date(0),
    path: '/',
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax'
  });
  return redirectResponse;
}

}

// Handle protected routes based on role
if (request.nextUrl.pathname.startsWith(‘/jobseeker’) ||
request.nextUrl.pathname.startsWith(‘/employers’)) {

// Determine the required role for this route
const requiredRole = request.nextUrl.pathname.startsWith('/employers') ? 'employer' : 'jobseeker';

// Skip auth check for public routes
const isPublicRoute =
  request.nextUrl.pathname.includes('/login') ||
  request.nextUrl.pathname.includes('/signup') ||
  request.nextUrl.pathname.includes('/reset-password') ||
  (requiredRole === 'employer' && (
    request.nextUrl.pathname.includes('/claim-profile') ||
    // Only allow specific employer profile pages (/employers/123) but not sub-routes
    request.nextUrl.pathname.match(/^\/employers\/[a-zA-Z0-9-]+$/) ||
    // Allow admin access to employer profile
    (request.nextUrl.pathname === '/employers/profile' && request.nextUrl.searchParams.has('admin-access'))
  ));

if (isPublicRoute) {
  return response;
}

try {
  const session = await getToken({
    req: request,
    secret: process.env.NEXTAUTH_SECRET
  });

  if (!session) {
    // Redirect to the appropriate login page
    const loginUrl = requiredRole === 'employer'
      ? `/auth/login?role=employer&redirect=${encodeURIComponent(request.nextUrl.pathname)}`
      : `/auth/login?redirect=${encodeURIComponent(request.nextUrl.pathname)}`;

    return createRedirectResponse(new URL(loginUrl, request.url));
  }

  // Make sure session has a role property
  if (!session.role) {
    console.error('Session missing role property:', session);
    const loginUrl = requiredRole === 'employer'
      ? `/auth/login?role=employer&error=missing_role&redirect=${encodeURIComponent(request.nextUrl.pathname)}`
      : `/auth/login?error=missing_role&redirect=${encodeURIComponent(request.nextUrl.pathname)}`;

    return createRedirectResponse(new URL(loginUrl, request.url));
  }

  // Special case for jobseeker profile pages - allow both roles to view
  if (request.nextUrl.pathname.startsWith('/jobseeker/profile/')) {
    // Allow both roles to access profile pages
    if (session.role !== 'jobseeker' && session.role !== 'employer') {
      return createRedirectResponse(new URL('/auth/login', request.url));
    }
  } else {
    // For all other protected routes, check role match
    if (session.role !== requiredRole) {
      // Redirect to the appropriate login page
      const loginUrl = requiredRole === 'employer'
        ? `/auth/login?role=employer&redirect=${encodeURIComponent(request.nextUrl.pathname)}`
        : `/auth/login?redirect=${encodeURIComponent(request.nextUrl.pathname)}`;

      return createRedirectResponse(new URL(loginUrl, request.url));
    }
  }

  return response;
} catch (error) {
  console.error('Session verification failed:', error);

  // Redirect to the appropriate login page
  const loginUrl = requiredRole === 'employer'
    ? `/auth/login?role=employer&error=session`
    : `/auth/login?error=session`;

  return createRedirectResponse(new URL(loginUrl, request.url));
}

}

return response;
}

export const config = {
matcher: [
‘/((?!_next/static|_next/image|favicon.ico|images|fonts|api/auth/callback).*)’,
]
};

@Avxo Unfortunately this appears to be outside of the Scope of Support.

Base functionality of Netlify’s build and deploy pipeline is supported, but we cannot help you debug any source code used either during build or after deployment.

You’ll need to continue to debug yourself using AI, your own know-how, or by hiring someone.

Just continue to refer to the Netlify documentation and test your assumptions by creating basic tests where you can test/confirm one thing at a time.

1 Like

Appreciate the response!

@Avxo No problem.

If while you’re stepping through you believe you’ve located something on Netlify’s side that isn’t working correctly, just ensure you create and provide a bare minimum reproduction that demonstrates the issue.

That way the Netlify team will be able to check/confirm (and potentially rectify) quickly.

1 Like