Proxy rotating session nonce cookies

This commit is contained in:
MOHAN 2026-07-16 22:17:21 +05:30
parent f0e6405b11
commit edbcbea5b4
7 changed files with 48 additions and 22 deletions

View File

@ -4,11 +4,11 @@ import { setAuthCookies } from "@/lib/auth-cookies";
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const res = await proxyRequest(req, "auth/login"); 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; 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) { if (res.ok && payload?.data?.accessToken && payload.data.refreshToken && payload.data.sessionNonce) {
const { accessToken, refreshToken, ...safeData } = payload.data; const { accessToken, refreshToken, sessionNonce, ...safeData } = payload.data;
const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status }); const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status });
setAuthCookies(safeRes, accessToken, refreshToken); setAuthCookies(safeRes, accessToken, refreshToken, sessionNonce);
return safeRes; return safeRes;
} }
return res; return res;

View File

@ -1,14 +1,18 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getBackendUrl } from "@/lib/backend"; 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) { export async function POST(req: NextRequest) {
const accessToken = req.cookies.get(ACCESS_COOKIE)?.value;
const refreshToken = req.cookies.get(REFRESH_COOKIE)?.value; const refreshToken = req.cookies.get(REFRESH_COOKIE)?.value;
const sessionNonce = req.cookies.get(NONCE_COOKIE)?.value;
const backendRes = refreshToken const backendRes = refreshToken
? await fetch(getBackendUrl("auth/logout"), { ? await fetch(getBackendUrl("auth/logout"), {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
...(sessionNonce ? { "X-LedgerOne-Session-Nonce": sessionNonce } : {}),
"User-Agent": req.headers.get("user-agent") ?? "", "User-Agent": req.headers.get("user-agent") ?? "",
"X-Forwarded-For": req.headers.get("x-forwarded-for") ?? "", "X-Forwarded-For": req.headers.get("x-forwarded-for") ?? "",
}, },

View File

@ -1,9 +1,10 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getBackendUrl } from "@/lib/backend"; 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) { export async function POST(req: NextRequest) {
const refreshToken = req.cookies.get(REFRESH_COOKIE)?.value; const refreshToken = req.cookies.get(REFRESH_COOKIE)?.value;
const sessionNonce = req.cookies.get(NONCE_COOKIE)?.value;
if (!refreshToken) { if (!refreshToken) {
const res = NextResponse.json( const res = NextResponse.json(
{ {
@ -23,20 +24,21 @@ export async function POST(req: NextRequest) {
"Content-Type": "application/json", "Content-Type": "application/json",
"User-Agent": req.headers.get("user-agent") ?? "", "User-Agent": req.headers.get("user-agent") ?? "",
"X-Forwarded-For": req.headers.get("x-forwarded-for") ?? "", "X-Forwarded-For": req.headers.get("x-forwarded-for") ?? "",
...(sessionNonce ? { "X-LedgerOne-Session-Nonce": sessionNonce } : {}),
}, },
body: JSON.stringify({ refreshToken }), body: JSON.stringify({ refreshToken }),
}); });
const payload = await backendRes.text(); const payload = await backendRes.text();
const parsed = JSON.parse(payload) as { data?: { accessToken?: string; refreshToken?: string; [key: string]: unknown } }; 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 const responsePayload = backendRes.ok && parsed.data?.accessToken && parsed.data.refreshToken && parsed.data.sessionNonce
? JSON.stringify({ ...parsed, data: {} }) ? JSON.stringify({ ...parsed, data: {} })
: payload; : payload;
const res = new NextResponse(responsePayload, { const res = new NextResponse(responsePayload, {
status: backendRes.status, status: backendRes.status,
headers: { "Content-Type": backendRes.headers.get("content-type") ?? "application/json" }, headers: { "Content-Type": backendRes.headers.get("content-type") ?? "application/json" },
}); });
if (backendRes.ok && 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); setAuthCookies(res, parsed.data.accessToken, parsed.data.refreshToken, parsed.data.sessionNonce);
} else { } else {
clearAuthCookies(res); clearAuthCookies(res);
} }

View File

@ -4,11 +4,11 @@ import { setAuthCookies } from "@/lib/auth-cookies";
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
const res = await proxyRequest(req, "auth/register"); 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; 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) { if (res.ok && payload?.data?.accessToken && payload.data.refreshToken && payload.data.sessionNonce) {
const { accessToken, refreshToken, ...safeData } = payload.data; const { accessToken, refreshToken, sessionNonce, ...safeData } = payload.data;
const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status }); const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status });
setAuthCookies(safeRes, accessToken, refreshToken); setAuthCookies(safeRes, accessToken, refreshToken, sessionNonce);
return safeRes; return safeRes;
} }
return res; return res;

View File

@ -7,11 +7,11 @@ export async function POST(
{ params }: { params: { provider: string } }, { params }: { params: { provider: string } },
) { ) {
const res = await proxyRequest(req, `auth/social/${params.provider}/callback`); 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; 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) { if (res.ok && payload?.data?.accessToken && payload.data.refreshToken && payload.data.sessionNonce) {
const { accessToken, refreshToken, ...safeData } = payload.data; const { accessToken, refreshToken, sessionNonce, ...safeData } = payload.data;
const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status }); const safeRes = NextResponse.json({ ...payload, data: safeData }, { status: res.status });
setAuthCookies(safeRes, accessToken, refreshToken); setAuthCookies(safeRes, accessToken, refreshToken, sessionNonce);
return safeRes; return safeRes;
} }
return res; return res;

View File

@ -5,6 +5,7 @@ const IS_PROD = process.env.NODE_ENV === "production";
export const ACCESS_COOKIE = "ledgerone_access"; export const ACCESS_COOKIE = "ledgerone_access";
export const REFRESH_COOKIE = "ledgerone_refresh"; export const REFRESH_COOKIE = "ledgerone_refresh";
export const AUTH_COOKIE = "ledgerone_auth"; export const AUTH_COOKIE = "ledgerone_auth";
export const NONCE_COOKIE = "ledgerone_nonce";
const cookieOptions = { const cookieOptions = {
httpOnly: true, httpOnly: true,
@ -13,7 +14,7 @@ const cookieOptions = {
path: "/", 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, { res.cookies.set(ACCESS_COOKIE, accessToken, {
...cookieOptions, ...cookieOptions,
maxAge: 60, maxAge: 60,
@ -26,10 +27,16 @@ export function setAuthCookies(res: NextResponse, accessToken: string, refreshTo
...cookieOptions, ...cookieOptions,
maxAge: 30 * 24 * 60 * 60, maxAge: 30 * 24 * 60 * 60,
}); });
if (sessionNonce) {
res.cookies.set(NONCE_COOKIE, sessionNonce, {
...cookieOptions,
maxAge: 30 * 24 * 60 * 60,
});
}
} }
export function clearAuthCookies(res: NextResponse) { 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, "", { res.cookies.set(name, "", {
...cookieOptions, ...cookieOptions,
maxAge: 0, maxAge: 0,

View File

@ -2,7 +2,7 @@
// Forwards requests to the NestJS backend, including the Bearer token. // Forwards requests to the NestJS backend, including the Bearer token.
import { NextRequest, NextResponse } from "next/server"; 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"; 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 method = options.method ?? req.method;
const auth = req.headers.get("authorization") ?? ""; const auth = req.headers.get("authorization") ?? "";
const cookieAccessToken = req.cookies.get(ACCESS_COOKIE)?.value ?? ""; const cookieAccessToken = req.cookies.get(ACCESS_COOKIE)?.value ?? "";
const sessionNonce = req.cookies.get(NONCE_COOKIE)?.value ?? "";
const contentType = req.headers.get("content-type") ?? ""; const contentType = req.headers.get("content-type") ?? "";
const userAgent = req.headers.get("user-agent") ?? ""; const userAgent = req.headers.get("user-agent") ?? "";
const forwardedFor = req.headers.get("x-forwarded-for") ?? ""; const forwardedFor = req.headers.get("x-forwarded-for") ?? "";
@ -36,6 +37,7 @@ export async function proxyRequest(
const headers: Record<string, string> = { ...options.extraHeaders }; const headers: Record<string, string> = { ...options.extraHeaders };
if (auth) headers["Authorization"] = auth; if (auth) headers["Authorization"] = auth;
else if (cookieAccessToken) headers["Authorization"] = `Bearer ${cookieAccessToken}`; else if (cookieAccessToken) headers["Authorization"] = `Bearer ${cookieAccessToken}`;
if (sessionNonce) headers["X-LedgerOne-Session-Nonce"] = sessionNonce;
if (userAgent) headers["User-Agent"] = userAgent; if (userAgent) headers["User-Agent"] = userAgent;
if (forwardedFor) headers["X-Forwarded-For"] = forwardedFor; if (forwardedFor) headers["X-Forwarded-For"] = forwardedFor;
@ -61,12 +63,23 @@ export async function proxyRequest(
body: body ?? undefined, body: body ?? undefined,
}); });
const payload = await res.text(); const payload = await res.text();
return new NextResponse(payload, { const response = new NextResponse(payload, {
status: res.status, status: res.status,
headers: { headers: {
"Content-Type": res.headers.get("content-type") ?? "application/json", "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 { } catch {
return NextResponse.json( return NextResponse.json(
{ {