From edbcbea5b4d6bfb715d765b16f022e21cf92a6ec Mon Sep 17 00:00:00 2001 From: MOHAN Date: Thu, 16 Jul 2026 22:17:21 +0530 Subject: [PATCH] Proxy rotating session nonce cookies --- app/api/auth/login/route.ts | 8 ++++---- app/api/auth/logout/route.ts | 6 +++++- app/api/auth/refresh/route.ts | 12 +++++++----- app/api/auth/register/route.ts | 8 ++++---- .../auth/social/[provider]/callback/route.ts | 8 ++++---- lib/auth-cookies.ts | 11 +++++++++-- lib/backend.ts | 17 +++++++++++++++-- 7 files changed, 48 insertions(+), 22 deletions(-) diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index 0f70bf8..9e64f17 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -4,11 +4,11 @@ import { setAuthCookies } from "@/lib/auth-cookies"; export async function POST(req: NextRequest) { const res = await proxyRequest(req, "auth/login"); - const payload = await res.clone().json().catch(() => null) as { data?: { accessToken?: string; refreshToken?: string; [key: string]: unknown } } | null; - if (res.ok && payload?.data?.accessToken && payload.data.refreshToken) { - const { accessToken, refreshToken, ...safeData } = payload.data; + const payload = await res.clone().json().catch(() => null) as { data?: { accessToken?: string; refreshToken?: string; sessionNonce?: string; [key: string]: unknown } } | null; + if (res.ok && payload?.data?.accessToken && payload.data.refreshToken && payload.data.sessionNonce) { + const { accessToken, refreshToken, sessionNonce, ...safeData } = payload.data; const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status }); - setAuthCookies(safeRes, accessToken, refreshToken); + setAuthCookies(safeRes, accessToken, refreshToken, sessionNonce); return safeRes; } return res; diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts index 5927095..b37321c 100644 --- a/app/api/auth/logout/route.ts +++ b/app/api/auth/logout/route.ts @@ -1,14 +1,18 @@ import { NextRequest, NextResponse } from "next/server"; import { getBackendUrl } from "@/lib/backend"; -import { REFRESH_COOKIE, clearAuthCookies } from "@/lib/auth-cookies"; +import { ACCESS_COOKIE, NONCE_COOKIE, REFRESH_COOKIE, clearAuthCookies } from "@/lib/auth-cookies"; export async function POST(req: NextRequest) { + const accessToken = req.cookies.get(ACCESS_COOKIE)?.value; const refreshToken = req.cookies.get(REFRESH_COOKIE)?.value; + const sessionNonce = req.cookies.get(NONCE_COOKIE)?.value; const backendRes = refreshToken ? await fetch(getBackendUrl("auth/logout"), { method: "POST", headers: { "Content-Type": "application/json", + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + ...(sessionNonce ? { "X-LedgerOne-Session-Nonce": sessionNonce } : {}), "User-Agent": req.headers.get("user-agent") ?? "", "X-Forwarded-For": req.headers.get("x-forwarded-for") ?? "", }, diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts index 8fc0c9f..f76fc37 100644 --- a/app/api/auth/refresh/route.ts +++ b/app/api/auth/refresh/route.ts @@ -1,9 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; import { getBackendUrl } from "@/lib/backend"; -import { REFRESH_COOKIE, clearAuthCookies, setAuthCookies } from "@/lib/auth-cookies"; +import { NONCE_COOKIE, REFRESH_COOKIE, clearAuthCookies, setAuthCookies } from "@/lib/auth-cookies"; export async function POST(req: NextRequest) { const refreshToken = req.cookies.get(REFRESH_COOKIE)?.value; + const sessionNonce = req.cookies.get(NONCE_COOKIE)?.value; if (!refreshToken) { const res = NextResponse.json( { @@ -23,20 +24,21 @@ export async function POST(req: NextRequest) { "Content-Type": "application/json", "User-Agent": req.headers.get("user-agent") ?? "", "X-Forwarded-For": req.headers.get("x-forwarded-for") ?? "", + ...(sessionNonce ? { "X-LedgerOne-Session-Nonce": sessionNonce } : {}), }, body: JSON.stringify({ refreshToken }), }); const payload = await backendRes.text(); - const parsed = JSON.parse(payload) as { data?: { accessToken?: string; refreshToken?: string; [key: string]: unknown } }; - const responsePayload = backendRes.ok && parsed.data?.accessToken && parsed.data.refreshToken + const parsed = JSON.parse(payload) as { data?: { accessToken?: string; refreshToken?: string; sessionNonce?: string; [key: string]: unknown } }; + const responsePayload = backendRes.ok && parsed.data?.accessToken && parsed.data.refreshToken && parsed.data.sessionNonce ? JSON.stringify({ ...parsed, data: {} }) : payload; const res = new NextResponse(responsePayload, { status: backendRes.status, headers: { "Content-Type": backendRes.headers.get("content-type") ?? "application/json" }, }); - if (backendRes.ok && parsed.data?.accessToken && parsed.data.refreshToken) { - setAuthCookies(res, parsed.data.accessToken, parsed.data.refreshToken); + if (backendRes.ok && parsed.data?.accessToken && parsed.data.refreshToken && parsed.data.sessionNonce) { + setAuthCookies(res, parsed.data.accessToken, parsed.data.refreshToken, parsed.data.sessionNonce); } else { clearAuthCookies(res); } diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index dcd0d08..d5f67aa 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -4,11 +4,11 @@ import { setAuthCookies } from "@/lib/auth-cookies"; export async function POST(req: NextRequest) { const res = await proxyRequest(req, "auth/register"); - const payload = await res.clone().json().catch(() => null) as { data?: { accessToken?: string; refreshToken?: string; [key: string]: unknown } } | null; - if (res.ok && payload?.data?.accessToken && payload.data.refreshToken) { - const { accessToken, refreshToken, ...safeData } = payload.data; + const payload = await res.clone().json().catch(() => null) as { data?: { accessToken?: string; refreshToken?: string; sessionNonce?: string; [key: string]: unknown } } | null; + if (res.ok && payload?.data?.accessToken && payload.data.refreshToken && payload.data.sessionNonce) { + const { accessToken, refreshToken, sessionNonce, ...safeData } = payload.data; const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status }); - setAuthCookies(safeRes, accessToken, refreshToken); + setAuthCookies(safeRes, accessToken, refreshToken, sessionNonce); return safeRes; } return res; diff --git a/app/api/auth/social/[provider]/callback/route.ts b/app/api/auth/social/[provider]/callback/route.ts index ca0061e..b5b2e1e 100644 --- a/app/api/auth/social/[provider]/callback/route.ts +++ b/app/api/auth/social/[provider]/callback/route.ts @@ -7,11 +7,11 @@ export async function POST( { params }: { params: { provider: string } }, ) { const res = await proxyRequest(req, `auth/social/${params.provider}/callback`); - const payload = await res.clone().json().catch(() => null) as { data?: { accessToken?: string; refreshToken?: string; [key: string]: unknown } } | null; - if (res.ok && payload?.data?.accessToken && payload.data.refreshToken) { - const { accessToken, refreshToken, ...safeData } = payload.data; + const payload = await res.clone().json().catch(() => null) as { data?: { accessToken?: string; refreshToken?: string; sessionNonce?: string; [key: string]: unknown } } | null; + if (res.ok && payload?.data?.accessToken && payload.data.refreshToken && payload.data.sessionNonce) { + const { accessToken, refreshToken, sessionNonce, ...safeData } = payload.data; const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status }); - setAuthCookies(safeRes, accessToken, refreshToken); + setAuthCookies(safeRes, accessToken, refreshToken, sessionNonce); return safeRes; } return res; diff --git a/lib/auth-cookies.ts b/lib/auth-cookies.ts index 2d6a1fc..bc93c2d 100644 --- a/lib/auth-cookies.ts +++ b/lib/auth-cookies.ts @@ -5,6 +5,7 @@ const IS_PROD = process.env.NODE_ENV === "production"; export const ACCESS_COOKIE = "ledgerone_access"; export const REFRESH_COOKIE = "ledgerone_refresh"; export const AUTH_COOKIE = "ledgerone_auth"; +export const NONCE_COOKIE = "ledgerone_nonce"; const cookieOptions = { httpOnly: true, @@ -13,7 +14,7 @@ const cookieOptions = { path: "/", }; -export function setAuthCookies(res: NextResponse, accessToken: string, refreshToken: string) { +export function setAuthCookies(res: NextResponse, accessToken: string, refreshToken: string, sessionNonce?: string) { res.cookies.set(ACCESS_COOKIE, accessToken, { ...cookieOptions, maxAge: 60, @@ -26,10 +27,16 @@ export function setAuthCookies(res: NextResponse, accessToken: string, refreshTo ...cookieOptions, maxAge: 30 * 24 * 60 * 60, }); + if (sessionNonce) { + res.cookies.set(NONCE_COOKIE, sessionNonce, { + ...cookieOptions, + maxAge: 30 * 24 * 60 * 60, + }); + } } export function clearAuthCookies(res: NextResponse) { - for (const name of [ACCESS_COOKIE, REFRESH_COOKIE, AUTH_COOKIE]) { + for (const name of [ACCESS_COOKIE, REFRESH_COOKIE, AUTH_COOKIE, NONCE_COOKIE]) { res.cookies.set(name, "", { ...cookieOptions, maxAge: 0, diff --git a/lib/backend.ts b/lib/backend.ts index dbe0d74..4ada50c 100644 --- a/lib/backend.ts +++ b/lib/backend.ts @@ -2,7 +2,7 @@ // Forwards requests to the NestJS backend, including the Bearer token. import { NextRequest, NextResponse } from "next/server"; -import { ACCESS_COOKIE } from "./auth-cookies"; +import { ACCESS_COOKIE, NONCE_COOKIE } from "./auth-cookies"; const BASE_URL = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; @@ -29,6 +29,7 @@ export async function proxyRequest( const method = options.method ?? req.method; const auth = req.headers.get("authorization") ?? ""; const cookieAccessToken = req.cookies.get(ACCESS_COOKIE)?.value ?? ""; + const sessionNonce = req.cookies.get(NONCE_COOKIE)?.value ?? ""; const contentType = req.headers.get("content-type") ?? ""; const userAgent = req.headers.get("user-agent") ?? ""; const forwardedFor = req.headers.get("x-forwarded-for") ?? ""; @@ -36,6 +37,7 @@ export async function proxyRequest( const headers: Record = { ...options.extraHeaders }; if (auth) headers["Authorization"] = auth; else if (cookieAccessToken) headers["Authorization"] = `Bearer ${cookieAccessToken}`; + if (sessionNonce) headers["X-LedgerOne-Session-Nonce"] = sessionNonce; if (userAgent) headers["User-Agent"] = userAgent; if (forwardedFor) headers["X-Forwarded-For"] = forwardedFor; @@ -61,12 +63,23 @@ export async function proxyRequest( body: body ?? undefined, }); const payload = await res.text(); - return new NextResponse(payload, { + const response = new NextResponse(payload, { status: res.status, headers: { "Content-Type": res.headers.get("content-type") ?? "application/json", }, }); + const nextNonce = res.headers.get("x-ledgerone-next-nonce"); + if (nextNonce) { + response.cookies.set(NONCE_COOKIE, nextNonce, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 30 * 24 * 60 * 60, + }); + } + return response; } catch { return NextResponse.json( {