Harden user management integration

This commit is contained in:
Julian Appel 2026-09-28 22:42:03 +02:00
parent 73579d1e79
commit 847f4e9493
18 changed files with 394 additions and 111 deletions

View file

@ -2,8 +2,12 @@ import type { NextFunction, Request, RequestHandler, Response } from "express";
import { SESSION_COOKIE, type AuthConfig } from "./auth-config.js";
import { readCookie } from "./cookie.js";
import type { SessionStore } from "./sessions.js";
import type { UserRepository } from "../../db/repositories/user.repository.js";
import type { UserStore } from "../../domain/ports/user.store.js";
import type { UserRole } from "../../shared/constants/user-roles.js";
import {
WRITE_REQUEST_HEADER_NAME,
WRITE_REQUEST_HEADER_VALUE,
} from "../../shared/constants/request-security.js";
export interface AuthUser {
id: string;
@ -30,7 +34,7 @@ export function sessionIdFromRequest(req: {
export async function identityFromRequest(
req: { headers: { cookie?: string | undefined } },
sessions: SessionStore,
users: UserRepository,
users: Pick<UserStore, "findById">,
slideExpiry = true
): Promise<AuthUser | undefined> {
const session = sessions.get(sessionIdFromRequest(req), slideExpiry);
@ -74,10 +78,11 @@ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
* CSRF defense for cookie-authenticated writes.
*
* SameSite=Lax alone is not enough here: every other service on the same
* host (other ports of the same IP) counts as "same-site" and would get the
* cookie attached. Browsers send Sec-Fetch-Site on every request, so a write
* is only accepted from the app's own origin. Clients without the header
* (curl, scripts) carry no ambient browser cookie and are not a CSRF vector.
* host (other ports of the same IP) counts as "same-site" and may receive the
* cookie. Fetch Metadata is also not guaranteed on plain HTTP in the LAN.
* Every write therefore needs an application header that HTML forms cannot
* set; a cross-origin fetch would require a CORS preflight, which the API does
* not permit. Sec-Fetch-Site remains an additional browser-side check.
*/
export function requireSameOriginForWrites(): RequestHandler {
return (req: Request, res: Response, next: NextFunction) => {
@ -85,6 +90,10 @@ export function requireSameOriginForWrites(): RequestHandler {
next();
return;
}
if (req.get(WRITE_REQUEST_HEADER_NAME) !== WRITE_REQUEST_HEADER_VALUE) {
res.status(403).json({ error: "Schreibzugriff ohne gültigen Anwendungsnachweis abgelehnt." });
return;
}
const fetchSite = req.headers["sec-fetch-site"];
if (fetchSite === undefined || fetchSite === "same-origin" || fetchSite === "none") {
next();
@ -98,7 +107,7 @@ export function requireSameOriginForWrites(): RequestHandler {
export function createAuthGuard(
authConfig: AuthConfig,
sessions: SessionStore,
users: UserRepository,
users: Pick<UserStore, "findById">,
openPaths: readonly string[] = ["/auth/login", "/auth/status", "/auth/logout"]
): RequestHandler {
return (req: Request, res: Response, next: NextFunction) => {