fix(auth): honor HTTPS sessions in Pi management

This commit is contained in:
2026-08-17 01:46:34 +02:00
parent 09e546c1ef
commit 8c67cb75dc
5 changed files with 146 additions and 14 deletions
+10
View File
@@ -23,11 +23,21 @@ export function requirePermission(
return reply.code(403).send({ code: "auth_forbidden", error: "This operation is not permitted" });
}
/**
* A resolved cookie session is populated only by the central auth boundary, after its
* request-snapshot Origin and CSRF checks. Route-specific legacy guards must not reinterpret
* the internal transport host/protocol for that already-authorized browser request.
*/
export function hasCookieBackedAuthSession(request: FastifyRequest): boolean {
return request.authSession !== undefined;
}
/** Permit non-browser clients and browsers whose declared origin matches the request host. */
export function requireSameOriginOrNonBrowser(
request: FastifyRequest,
reply: FastifyReply,
): FastifyReply | undefined {
if (hasCookieBackedAuthSession(request)) return undefined;
const origin = request.headers.origin;
if (origin === undefined) return undefined;
if (typeof origin !== "string" || typeof request.headers.host !== "string") {
+11 -10
View File
@@ -122,7 +122,8 @@ export function registerAuthRoutes(app: FastifyInstance, deps: AuthRouteDependen
});
app.post("/auth/local/login", async (request, reply) => {
const configured = currentLocalConfig(captureAuthConfigSnapshot(request, deps.authentication), deps);
const snapshot = captureAuthConfigSnapshot(request, deps.authentication);
const configured = currentLocalConfig(snapshot, deps);
if (configured.kind === "unavailable") {
return unavailableAfterDummy(verificationGate, argon2SafePassword(loginPayload(request).password), reply);
}
@@ -172,7 +173,7 @@ export function registerAuthRoutes(app: FastifyInstance, deps: AuthRouteDependen
idleTtlMs: (payload.remember ? configured.session.rememberIdleSeconds : configured.session.regularIdleSeconds) * 1000,
absoluteTtlMs: (payload.remember ? configured.session.rememberTtlSeconds : configured.session.regularTtlSeconds) * 1000,
});
reply.setCookie(sessionCookieName(), created.token, cookieOptions(configured.secure, payload.remember));
reply.setCookie(sessionCookieName(), created.token, cookieOptions(snapshot, payload.remember));
return reply.send({});
} catch {
return unavailable(reply);
@@ -187,7 +188,7 @@ export function registerAuthRoutes(app: FastifyInstance, deps: AuthRouteDependen
if (!token || !deps.sessionStore) return unavailable(reply);
try {
await deps.sessionStore.revoke(token);
reply.clearCookie(sessionCookieName(), cookieOptions(currentSecure(request), false));
reply.clearCookie(sessionCookieName(), cookieOptions(request.authConfigSnapshot, false));
return reply.code(204).send();
} catch {
return unavailable(reply);
@@ -237,7 +238,6 @@ function currentLocalConfig(loaded: LoadedAuthConfig | undefined, deps: AuthRout
| {
revision: string;
origin: string;
secure: boolean;
session: { regularTtlSeconds: number; regularIdleSeconds: number; rememberTtlSeconds: number; rememberIdleSeconds: number };
registry: LocalUserRegistry;
kind: "local";
@@ -254,7 +254,6 @@ function currentLocalConfig(loaded: LoadedAuthConfig | undefined, deps: AuthRout
kind: "local",
revision: loaded.revision,
origin: url.origin,
secure: url.protocol === "https:",
session: loaded.value.session,
registry,
};
@@ -263,11 +262,13 @@ function currentLocalConfig(loaded: LoadedAuthConfig | undefined, deps: AuthRout
}
}
function currentSecure(request: FastifyRequest): boolean {
return request.authConfigSnapshot?.value.publicUrl.startsWith("https:") ?? false;
}
function cookieOptions(secure: boolean, remembered: boolean) {
function cookieOptions(snapshot: LoadedAuthConfig | undefined, remembered: boolean) {
let secure = false;
try {
secure = snapshot !== undefined && new URL(snapshot.value.publicUrl).protocol === "https:";
} catch {
// Invalid auth configurations are rejected before they can reach this route.
}
return {
httpOnly: true,
sameSite: "lax" as const,