feat(auth): shared dashboard session verifier that requires the authenticated claim

Refs #13298
This commit is contained in:
diegosouzapw
2026-09-11 17:36:09 -03:00
parent cfa2fc7548
commit be43641be4
2 changed files with 130 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
/**
* Dashboard session token — the ONE verifier for the `auth_token` cookie.
*
* A dashboard session is a JWT that verifies against JWT_SECRET AND carries
* `authenticated: true` — the claim every session minter emits
* (`api/auth/login`, `api/auth/oidc/callback`, the authz pipeline refresh).
* Other tokens signed with the same secret exist (the Cursor CLI passthrough
* mints `iss "omniroute" / aud "cursor-cli"` tokens for any key holder) and
* MUST NOT verify as a session: before #13298 any such token forged the
* cookie and reached instance-wide operations. Every place that trusts the
* cookie goes through `verifyDashboardSessionToken`; a bare `jwtVerify` on
* `auth_token` is a regression (guarded by
* tests/unit/dashboard-session-verifier-source-guard.test.ts).
*/
import { jwtVerify, type JWTPayload } from "jose";
export const DASHBOARD_SESSION_COOKIE = "auth_token";
export const DASHBOARD_SESSION_CLAIM = "authenticated";
export function getDashboardJwtSecret(): Uint8Array | null {
const secret = process.env.JWT_SECRET?.trim();
return secret ? new TextEncoder().encode(secret) : null;
}
/**
* Returns the verified payload when `token` is a dashboard session, else null.
* Never throws: a malformed, expired, foreign-secret or claim-less token is
* simply "not a session". Pass `secret` explicitly when the caller already
* resolved it (the authz pipeline does); `null` means "no secret → no session".
*/
export async function verifyDashboardSessionToken(
token: string | null | undefined,
secret: Uint8Array | null = getDashboardJwtSecret()
): Promise<JWTPayload | null> {
if (!token || typeof token !== "string" || !secret) return null;
try {
const { payload } = await jwtVerify(token, secret);
return payload[DASHBOARD_SESSION_CLAIM] === true ? payload : null;
} catch {
return null;
}
}

View File

@@ -0,0 +1,88 @@
/**
* #13298 — a dashboard session is a JWT that (a) verifies against JWT_SECRET AND
* (b) carries `authenticated: true`, the claim every dashboard minter emits
* (login, OIDC callback, pipeline refresh). Any other JWT signed with the same
* secret — notably the Cursor CLI passthrough token (iss "omniroute", aud
* "cursor-cli", no claim) — is NOT a session.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { SignJWT } from "jose";
process.env.JWT_SECRET = "dashboard-session-token-13298-secret";
const { verifyDashboardSessionToken, getDashboardJwtSecret, DASHBOARD_SESSION_COOKIE } =
await import("../../src/shared/utils/dashboardSessionToken.ts");
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const sign = (
claims: Record<string, unknown>,
opts: { exp?: string; iss?: string; aud?: string } = {}
) => {
let j = new SignJWT(claims)
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime(opts.exp ?? "1h");
if (opts.iss) j = j.setIssuer(opts.iss);
if (opts.aud) j = j.setAudience(opts.aud);
return j.sign(secret);
};
test("cookie name constant", () => {
assert.equal(DASHBOARD_SESSION_COOKIE, "auth_token");
});
test("getDashboardJwtSecret encodes the trimmed env secret, null when unset/blank", () => {
assert.ok(getDashboardJwtSecret() instanceof Uint8Array);
const saved = process.env.JWT_SECRET;
process.env.JWT_SECRET = " ";
assert.equal(getDashboardJwtSecret(), null);
delete process.env.JWT_SECRET;
assert.equal(getDashboardJwtSecret(), null);
process.env.JWT_SECRET = saved;
});
test("accepts the login-shaped token { authenticated: true } and returns its payload", async () => {
const payload = await verifyDashboardSessionToken(await sign({ authenticated: true }));
assert.ok(payload, "login-shaped token is a session");
assert.equal(payload!.authenticated, true);
assert.equal(typeof payload!.exp, "number");
});
test("REJECTS the Cursor CLI passthrough token (same secret, iss omniroute / aud cursor-cli, no claim)", async () => {
const cursorToken = await sign({ name: "some-key" }, { iss: "omniroute", aud: "cursor-cli" });
assert.equal(await verifyDashboardSessionToken(cursorToken), null);
});
test("rejects a verified token whose claim is missing or not strictly true", async () => {
assert.equal(await verifyDashboardSessionToken(await sign({ sub: "admin" })), null);
assert.equal(await verifyDashboardSessionToken(await sign({ authenticated: "true" })), null);
assert.equal(await verifyDashboardSessionToken(await sign({ authenticated: 1 })), null);
});
test("rejects expired, wrong-secret, garbage, empty and missing tokens without throwing", async () => {
assert.equal(
await verifyDashboardSessionToken(await sign({ authenticated: true }, { exp: "-1s" })),
null
);
const other = new TextEncoder().encode("another-secret");
const foreign = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(other);
assert.equal(await verifyDashboardSessionToken(foreign), null);
assert.equal(await verifyDashboardSessionToken("not.a.jwt"), null);
assert.equal(await verifyDashboardSessionToken(""), null);
assert.equal(await verifyDashboardSessionToken(null), null);
assert.equal(await verifyDashboardSessionToken(undefined), null);
});
test("an explicit secret argument wins over the env; a null secret means no session", async () => {
const other = new TextEncoder().encode("explicit-secret");
const tok = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("1h")
.sign(other);
assert.ok(await verifyDashboardSessionToken(tok, other));
assert.equal(await verifyDashboardSessionToken(tok), null);
assert.equal(await verifyDashboardSessionToken(await sign({ authenticated: true }), null), null);
});