From 2b47756acfc34fd93c49ff3707d45672a51ed2a5 Mon Sep 17 00:00:00 2001 From: hlbmtc Date: Tue, 27 Jan 2026 15:45:29 +0000 Subject: [PATCH 1/2] Nextjs Middleware: simplified JWT token verification and legacy token migration --- front_end/src/middleware.ts | 97 +++++++++---------- .../src/services/auth_tokens_migration.ts | 5 +- 2 files changed, 48 insertions(+), 54 deletions(-) diff --git a/front_end/src/middleware.ts b/front_end/src/middleware.ts index ec791db8b3..874fea184b 100644 --- a/front_end/src/middleware.ts +++ b/front_end/src/middleware.ts @@ -12,48 +12,44 @@ import { getAlphaTokenSession } from "@/services/session"; import { getAlphaAccessToken } from "@/utils/alpha_access"; import { getPublicSettings } from "@/utils/public_settings.server"; -async function verifyToken(responseAuth: AuthCookieManager): Promise { - try { - await ServerAuthApi.verifyToken(); - } catch { - // Token is invalid (user banned, token revoked, etc.) - clear all auth cookies - console.error("Token verification failed, clearing auth cookies"); - responseAuth.clearAuthTokens(); - } -} - /** - * Refresh tokens and apply new cookies to response. + * Refresh tokens using refresh token. * Returns true if tokens were refreshed. */ -async function refreshTokensIfNeeded( +async function refreshTokens( requestAuth: AuthCookieReader, responseAuth: AuthCookieManager ): Promise { const refreshToken = requestAuth.getRefreshToken(); - - // No refresh token = can't refresh if (!refreshToken) return false; - // Access token still valid = no refresh needed - if (!requestAuth.isAccessTokenExpired()) return false; - - let tokens; try { - tokens = await ServerAuthApi.refreshTokens(refreshToken); + const tokens = await ServerAuthApi.refreshTokens(refreshToken); + responseAuth.setAuthTokens(tokens); + return true; } catch (error) { console.error("Middleware token refresh failed:", error); return false; } +} - responseAuth.setAuthTokens(tokens); - return true; +/** + * Verify access token is valid. + * Returns true if valid. + */ +async function verifyToken(): Promise { + try { + await ServerAuthApi.verifyToken(); + return true; + } catch { + console.error("Token verification failed"); + return false; + } } export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; const requestAuth = new AuthCookieReader(request.cookies); - let hasSession = requestAuth.hasAuthSession(); const requestHeaders = new Headers(request.headers); requestHeaders.set("x-url", request.url); @@ -61,16 +57,35 @@ export async function middleware(request: NextRequest) { const response = NextResponse.next({ request: { headers: requestHeaders } }); const responseAuth = new AuthCookieManager(response.cookies); - // DEPRECATED: Legacy token migration - remove after 30-day grace period - // Must run before auth checks so users with legacy tokens aren't rejected - const wasMigrated = await handleLegacyTokenMigration( - request, - response, - requestAuth, - responseAuth - ); - if (wasMigrated) { - hasSession = true; + let hasSession = false; + const accessToken = requestAuth.getAccessToken(); + const refreshToken = requestAuth.getRefreshToken(); + + // 1. Verify non-expired access token + if (accessToken && !requestAuth.isAccessTokenExpired()) { + hasSession = await verifyToken(); + } + + // 2. Try refresh if no valid session yet + if (!hasSession && refreshToken) { + hasSession = await refreshTokens(requestAuth, responseAuth); + } + + // 3. Clear invalid JWT tokens + if (!hasSession && (accessToken || refreshToken)) { + responseAuth.clearAuthTokens(); + // Clear legacy auth token + response.cookies.delete("auth_token"); + } + + // 4. No JWT tokens - try legacy migration + // DEPRECATED: Remove after 30-day migration period + if (!hasSession && !accessToken && !refreshToken) { + hasSession = await handleLegacyTokenMigration( + request, + response, + responseAuth + ); } const { PUBLIC_AUTHENTICATION_REQUIRED } = getPublicSettings(); @@ -103,24 +118,6 @@ export async function middleware(request: NextRequest) { } } - // Proactive token refresh (MUST happen in middleware to persist cookies) - if (hasSession) { - const tokensRefreshed = await refreshTokensIfNeeded( - requestAuth, - responseAuth - ); - // Skip verification if tokens were just refreshed (they're valid by definition) - // Only verify existing tokens to catch banned users or revoked tokens - if (!tokensRefreshed) { - if (requestAuth.getAccessToken()) { - await verifyToken(responseAuth); - } else { - // No access token and refresh failed - clear the invalid refresh token - responseAuth.clearAuthTokens(); - } - } - } - const locale_in_url = request.nextUrl.searchParams.get("locale"); const locale_in_cookie = request.cookies.get(LOCALE_COOKIE_NAME)?.value; if (locale_in_url && locale_in_url !== locale_in_cookie) { diff --git a/front_end/src/services/auth_tokens_migration.ts b/front_end/src/services/auth_tokens_migration.ts index de0993c36a..5f713e6ea3 100644 --- a/front_end/src/services/auth_tokens_migration.ts +++ b/front_end/src/services/auth_tokens_migration.ts @@ -7,7 +7,7 @@ import "server-only"; import { NextRequest, NextResponse } from "next/server"; -import { AuthCookieManager, AuthCookieReader } from "@/services/auth_tokens"; +import { AuthCookieManager } from "@/services/auth_tokens"; import { getPublicSettings } from "@/utils/public_settings.server"; const LEGACY_COOKIE_NAME = "auth_token"; @@ -20,11 +20,8 @@ const LEGACY_COOKIE_NAME = "auth_token"; export async function handleLegacyTokenMigration( request: NextRequest, response: NextResponse, - requestAuth: AuthCookieReader, responseAuth: AuthCookieManager ): Promise { - if (requestAuth.hasAuthSession()) return false; - const legacyToken = request.cookies.get(LEGACY_COOKIE_NAME)?.value; if (!legacyToken) return false; From 72b494b9fa3558e2de0f55a8611bd76d781a1ff7 Mon Sep 17 00:00:00 2001 From: hlbmtc Date: Tue, 27 Jan 2026 16:59:33 +0000 Subject: [PATCH 2/2] Don't clean tokens for 5xx errors --- front_end/src/middleware.ts | 60 ++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/front_end/src/middleware.ts b/front_end/src/middleware.ts index 874fea184b..89b38d5115 100644 --- a/front_end/src/middleware.ts +++ b/front_end/src/middleware.ts @@ -10,11 +10,21 @@ import { } from "@/services/language_service"; import { getAlphaTokenSession } from "@/services/session"; import { getAlphaAccessToken } from "@/utils/alpha_access"; +import { ApiError } from "@/utils/core/errors"; import { getPublicSettings } from "@/utils/public_settings.server"; +/** + * Returns true on 4xx (definitive client error), false otherwise. + * Used to determine if we should clear tokens or preserve them on transient errors. + */ +function isClientError(error: unknown): boolean { + return ApiError.isApiError(error) && error.response.status < 500; +} + /** * Refresh tokens using refresh token. - * Returns true if tokens were refreshed. + * Returns true if refreshed, false on 4xx. + * Throws on transient errors (5xx, network). */ async function refreshTokens( requestAuth: AuthCookieReader, @@ -28,22 +38,29 @@ async function refreshTokens( responseAuth.setAuthTokens(tokens); return true; } catch (error) { - console.error("Middleware token refresh failed:", error); - return false; + if (isClientError(error)) { + console.error("Middleware token refresh failed", error); + return false; + } + throw error; } } /** * Verify access token is valid. - * Returns true if valid. + * Returns true if valid, false on 4xx. + * Throws on transient errors (5xx, network). */ async function verifyToken(): Promise { try { await ServerAuthApi.verifyToken(); return true; - } catch { - console.error("Token verification failed"); - return false; + } catch (error) { + if (isClientError(error)) { + console.error("Token verification failed", error); + return false; + } + throw error; } } @@ -61,21 +78,24 @@ export async function middleware(request: NextRequest) { const accessToken = requestAuth.getAccessToken(); const refreshToken = requestAuth.getRefreshToken(); - // 1. Verify non-expired access token - if (accessToken && !requestAuth.isAccessTokenExpired()) { - hasSession = await verifyToken(); - } + try { + // 1. Verify non-expired access token + if (accessToken && !requestAuth.isAccessTokenExpired()) { + hasSession = await verifyToken(); + } - // 2. Try refresh if no valid session yet - if (!hasSession && refreshToken) { - hasSession = await refreshTokens(requestAuth, responseAuth); - } + // 2. Try refresh if no valid session yet + if (!hasSession && refreshToken) { + hasSession = await refreshTokens(requestAuth, responseAuth); + } - // 3. Clear invalid JWT tokens - if (!hasSession && (accessToken || refreshToken)) { - responseAuth.clearAuthTokens(); - // Clear legacy auth token - response.cookies.delete("auth_token"); + // 3. Clear invalid JWT tokens (only on definitive 4xx, not transient errors) + if (!hasSession && (accessToken || refreshToken)) { + responseAuth.clearAuthTokens(); + } + } catch (error) { + // Transient error (5xx, network) - don't clear tokens + console.error("Auth service error, preserving tokens:", error); } // 4. No JWT tokens - try legacy migration