Files
OmniRoute/src/shared/utils/dashboardSessionToken.ts
Diego Rodrigues de Sa e Souza db6feb6fad fix(auth): dashboard session requires the authenticated claim — Cursor CLI tokens are not sessions (#13375)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:12 -03:00

43 lines
1.8 KiB
TypeScript

/**
* 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;
}
}