fix(auth): bind request auth snapshots
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import type { FastifyRequest, FastifyReply, preHandlerHookHandler } from "fastify";
|
||||
import { localPrincipal, type PrincipalContext, upstreamPrincipal } from "./principal.js";
|
||||
import { rolesToPermissions } from "./config.js";
|
||||
import type { AuthenticationConfigProvider, AuthMode, AuthSessionRecord } from "./types.js";
|
||||
import { AuthSessionOperationalError, type AuthSessionStore } from "./session-store.js";
|
||||
import type { AuthenticationConfigProvider, AuthMode, AuthSessionRecord, LoadedAuthConfig } from "./types.js";
|
||||
import { AuthSessionOperationalError, type AuthSessionStore, type AuthSessionValidity } from "./session-store.js";
|
||||
import { deriveCsrfToken, csrfTokensEqual } from "./csrf.js";
|
||||
import { requireSameOriginOrNonBrowser } from "./authorization.js";
|
||||
|
||||
@@ -13,6 +13,10 @@ declare module "fastify" {
|
||||
/** Internal only: never serialize or write this opaque cookie token to logs. */
|
||||
authSessionToken?: string;
|
||||
authPublicOrigin?: string;
|
||||
/** One immutable configuration load for the whole request, including CORS. */
|
||||
authConfigSnapshot?: LoadedAuthConfig;
|
||||
authConfigSnapshotCaptured?: boolean;
|
||||
authConfigSnapshotUnavailable?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +29,22 @@ export interface AuthDependencies {
|
||||
publicExposure?: boolean;
|
||||
authentication?: AuthenticationConfigProvider;
|
||||
sessionStore?: AuthSessionStore;
|
||||
sessionValidityForSnapshot?: (snapshot: LoadedAuthConfig) => AuthSessionValidity;
|
||||
}
|
||||
|
||||
/** Capture the authentication configuration once; CORS calls this before every other hook. */
|
||||
export function captureAuthConfigSnapshot(
|
||||
request: FastifyRequest,
|
||||
authentication: AuthenticationConfigProvider | undefined,
|
||||
): LoadedAuthConfig | undefined {
|
||||
if (request.authConfigSnapshotCaptured) return request.authConfigSnapshot;
|
||||
request.authConfigSnapshotCaptured = true;
|
||||
try {
|
||||
request.authConfigSnapshot = authentication?.current();
|
||||
} catch {
|
||||
request.authConfigSnapshotUnavailable = true;
|
||||
}
|
||||
return request.authConfigSnapshot;
|
||||
}
|
||||
|
||||
export function authPreHandler(mode: "none" | "mock" | "upstream", publicExposure = false) {
|
||||
@@ -59,6 +79,7 @@ export function authenticateSession(deps: AuthDependencies): preHandlerHookHandl
|
||||
: undefined;
|
||||
|
||||
const handle = async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
|
||||
const snapshot = captureAuthConfigSnapshot(request, deps.authentication);
|
||||
if (isPublicRoute(request)) return;
|
||||
|
||||
if (legacy) {
|
||||
@@ -67,8 +88,8 @@ export function authenticateSession(deps: AuthDependencies): preHandlerHookHandl
|
||||
return requireSameOriginOrNonBrowser(request, reply);
|
||||
}
|
||||
|
||||
const origin = configuredOrigin(deps.authentication);
|
||||
if (!origin || !deps.sessionStore) {
|
||||
const origin = configuredOrigin(snapshot);
|
||||
if (!snapshot || !origin || !deps.sessionStore) {
|
||||
return reply.code(503).send({ code: "auth_unavailable", error: "Authentication is unavailable" });
|
||||
}
|
||||
const token = readSessionCookie(request);
|
||||
@@ -76,7 +97,11 @@ export function authenticateSession(deps: AuthDependencies): preHandlerHookHandl
|
||||
|
||||
let session: AuthSessionRecord | undefined;
|
||||
try {
|
||||
session = await deps.sessionStore.resolve(token);
|
||||
session = await deps.sessionStore.resolve(token, undefined, deps.sessionValidityForSnapshot?.(snapshot));
|
||||
if (session && session.authConfigRevision !== snapshot.revision) {
|
||||
try { await deps.sessionStore.revoke(token); } catch { /* the mismatch remains denied */ }
|
||||
return authenticationRequired(reply);
|
||||
}
|
||||
if (session) await deps.sessionStore.touch(token);
|
||||
} catch (error) {
|
||||
if (error instanceof AuthSessionOperationalError) {
|
||||
@@ -150,9 +175,9 @@ function csrfFailed(reply: FastifyReply): FastifyReply {
|
||||
return reply.code(403).send({ code: "csrf_failed", error: "Request origin validation failed" });
|
||||
}
|
||||
|
||||
function configuredOrigin(authentication: AuthenticationConfigProvider | undefined): string | undefined {
|
||||
export function configuredOrigin(snapshot: LoadedAuthConfig | undefined): string | undefined {
|
||||
try {
|
||||
const publicUrl = authentication?.current().value.publicUrl;
|
||||
const publicUrl = snapshot?.value.publicUrl;
|
||||
return publicUrl ? new URL(publicUrl).origin : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
|
||||
Reference in New Issue
Block a user