Users are being redirected to login randomly a few times a day

Description:
Logged-in users are redirected back to the login flow randomly several times per day. It happens even when their session should still be valid. Our app must attach a bearer token in the Authorization header for every API request, so we suspect our 401 handling or refresh flow may be contributing.

Environment:

  • Next.js (App Router) with @auth0/nextjs-auth0 (edge)

  • Universal Login

  • Organizations enabled

Current approach (summary):

  • Refresh tokens via GET /api/auth/refresh-session using getAccessToken(req, res, { refresh: true }), then mirror to a cookie named access_token.

  • Browser Axios client:

    • Proactively calls /api/auth/refresh-session when the cached token is near expiry.

    • Injects Authorization: Bearer <access_token> on every API call.

    • If refresh returns 401, clears local cache and currently redirects to /api/auth/login.

Questions:

  1. Is there a recommended best practice for apps that must always attach Authorization: Bearer <token> to every API request?

  2. Should we handle this differently than our current approach (refresh endpoint + header injection + redirect to login on 401)?

  3. Are there known intermittent cases where getAccessToken can return 401 even when the session cookie appears valid (edge runtime and organizations enabled)?

Relevant code (redacted):

// app/api/auth/refresh-session/route.ts
import { getAccessToken } from "@auth0/nextjs-auth0/edge";
import { NextRequest, NextResponse } from "next/server";
import { jwtDecode } from "jwt-decode";
import { cookies } from "next/headers";
import { handleReqError } from "../../../../utils/errorHandler";

import { getAccessToken } from "@auth0/nextjs-auth0/edge";
import { NextRequest, NextResponse } from "next/server";
import { jwtDecode } from "jwt-decode";
import { cookies } from "next/headers";
import { handleReqError } from "../../../../utils/errorHandler";

export async function GET(req: NextRequest) {
  try {
    const cookieStore = await cookies();
    const existingToken = cookieStore.get("access_token")?.value;
    const res = new NextResponse();


    if (!existingToken) {
      // No cached token, get fresh one
      try {
        const { accessToken } = await getAccessToken(req, res, {
          refresh: true,
        });

        res.cookies.set("access_token", accessToken, {
          httpOnly: true,
          secure: true,
          sameSite: "lax",
          path: "/",
          maxAge: 60 * 60 * 24,
        });

        const updatedDecoded = jwtDecode(accessToken);
        return NextResponse.json(
          { token: accessToken, exp: updatedDecoded.exp },
          res
        );
      } catch (error) {
        return handleReqError(error, "Failed to get fresh token", req);
      }
    }

    const decoded: any = jwtDecode(existingToken);
    const now = Math.floor(Date.now() / 1000);

    const is_Expired = decoded.exp && decoded.exp < now;
    const hasMissingClaims =
      !decoded[`${process.env.BASE_API_URL}/claims/user_id`];

    if (is_Expired || hasMissingClaims) {
      try {
        const { accessToken } = await getAccessToken(req, res, {
          refresh: true,
        });

        if (!accessToken) {
          return handleReqError(
            { response: { status: 401 } },
            "No token received from Auth0",
            req
          );
        }

        res.cookies.set("access_token", accessToken, {
          httpOnly: true,
          secure: true,
          sameSite: "lax",
          path: "/",
          maxAge: 60 * 60 * 24,
        });

        const updatedDecoded = jwtDecode(accessToken);
        return NextResponse.json(
          {
            token: accessToken,
            exp: updatedDecoded.exp,
            isFirstLogin: hasMissingClaims,
          },
          res
        );
      } catch (error) {
        return handleReqError(error, "Failed to refresh token", req);
      }
    }

    // token is still valid and has the required claim
    return NextResponse.json({ ok: true, isFirstLogin: hasMissingClaims }, res);
  } catch (error) {
    return handleReqError(error, "Token validation failed", req);
  }
}


// browser Axios client (simplified)
// - Proactive refresh via /api/auth/refresh-session
// - Injects Authorization header
// - On 401 from refresh, clears cache and redirects to login
import axios from "axios";

let cachedToken: string | null = null;
let tokenExpiry = 0;
let refreshPromise: Promise<void> | null = null;

async function refreshIfNeeded() {
  const now = Math.floor(Date.now() / 1000);
  const needs = !cachedToken || !tokenExpiry || now >= tokenExpiry - 60;
  if (!needs) return;

  if (!refreshPromise) {
    refreshPromise = fetch("/api/auth/refresh-session", { credentials: "include" })
      .then(async (res) => {
        if (!res.ok) throw new Error(`Refresh failed: ${res.status}`);
        const { token, exp } = await res.json();
        cachedToken = token ?? null;
        tokenExpiry = exp ?? now + 3600;
      })
      .catch((e) => {
        cachedToken = null;
        tokenExpiry = 0;
        throw e;
      })
      .finally(() => { refreshPromise = null; });
  }
  await refreshPromise;
}

export function createBrowserAuthClient(baseURL = "/api") {
  const client = axios.create({ baseURL, withCredentials: true });

  client.interceptors.request.use(async (config) => {
    await refreshIfNeeded();
    if (cachedToken) {
     config.headers = config.headers ?? {};
     config.headers.Authorization = `Bearer ${cachedToken}`;
    }
    return config;
  });

  client.interceptors.response.use(
    (r) => r,
    (err) => {
      if (err?.response?.status === 401) {
        cachedToken = null;
        tokenExpiry = 0;
        // current behavior: send user to login
        window.location.replace("/api/auth/login");
      }
      return Promise.reject(err);
    }
  );

  return client;
}

Hi @isaferraram

Welcome to the Auth0 Community!

I am sorry about the delayed response to your inquiry!

From what I understand in the code that you have posted, it appears that you are refreshing tokens via the /auth/access-token as you have mentioned and demonstrated above.
To my knowledge, the NextJS SKD v4.9 currently has a dedicated route for checking the user’s session and refreshing it with a refresh token which is /auth/access-token

  1. /auth/access-token: the route to check the user’s session and return an access token (which will be automatically refreshed if a refresh token is available)

Also mentioned in the documentation, when using getAccessToken() the session should be automatically refreshed and persisted if an refresh token is available.

Could you please let me know what version are you exactly using at this time for your application?

Kind Regards,
Nik

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.