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) {
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;

View File

@ -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") ?? "",
},

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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<string, string> = { ...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(
{