Files
Florian-netz/tests/e2e/auth-gating.spec.ts
Matthias Hochmeister ae5d3589c3 Workstream 3: Authentifizierung & Zugriffskontrolle (Phase 2)
Einheitliche Auth.js-v5-Sitzung (role + brigadeId) über Authentik-OIDC
(Platform-Admins) und Credentials/argon2id (Wehr-Konten). Default-deny
dreifach: Middleware + Layout-Guard im (app)-Segment + 401/403 in API-Routen
und Guards in Server Actions.

- src/lib/auth/roles.ts: Rollen als Single Source of Truth (aus DB-Enum
  abgeleitet) + canAccessBrigade-Wehr-Scoping.
- src/lib/auth/password.ts: argon2id mit OWASP-Minima (Node-only).
- src/lib/auth/rate-limit.ts: Sliding Window (5 Fehlversuche/15 min) auf
  login_attempts; greift im authorize-Callback (beide Login-Pfade).
- src/auth.config.ts: Edge-sicher (kein @/db, kein argon2), Cookie-secure/
  __Secure- umgebungsabhaengig (isHttps).
- src/auth.ts: Credentials + DB-Lookup + Rate-Limit + Authentik-signIn-Gate.
- src/middleware.ts: Allowlist inkl. api/auth, api/health, login, Statics.
- src/lib/auth/guards.ts: requireSession/requireRole/requirePlatformAdmin/
  requireWehrAdmin/requireOwnBrigade + API-Varianten (401/403 ohne Daten-Leak).
- (app)/layout.tsx: requireSession() als erste Zeile aktiviert.
- (auth)/login: page + login-form + Server Actions (Zod, Authentik + lokal).
- api/auth/[...nextauth]/route.ts; api/health/route.ts (anonym 200).
- scripts/seed-auth.ts: idempotenter Erst-Admin-Seed.
- Typen: src/types/next-auth.d.ts.
- Tests: Unit (password/roles/rate-limit) gruen; E2E-Gating-Spec geschrieben
  (deferred, kein Server/DB in Sandbox).

Offline verifiziert: tsc --noEmit, next lint, next build, drizzle-kit check,
vitest (13 Unit-Tests) je ohne Fehler; Edge-Safety-Grep ohne DB/argon2-Import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 09:17:02 +02:00

77 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect } from "@playwright/test";
/**
* Auth-Gating-Garantie (Definition of Done, oberstes Prinzip).
*
* NICHT in der Sandbox ausführbar (kein Server/DB) — deferred. Wird über
* `npm run test:e2e:gating` gegen einen laufenden Server ausgeführt.
*
* Kerngarantie (Querschnittsstandard 13, default-deny dreifach):
* - Anonyme Aufrufe von Seiten -> Redirect auf /login (mit callbackUrl).
* - Anonyme Aufrufe von API-Routen -> 401 OHNE Daten-Leak.
* - Öffentliche Routen (Login, Health) bleiben erreichbar.
*
* Negativ-Probe (manuell/CI): Entfernen von `requireSession()` aus
* `(app)/layout.tsx` muss diese Suite rot machen.
*/
// Geschützte Seiten (Redirect-Manifest). Neue Seiten hier ergänzen.
const PROTECTED_PAGES = [
"/",
"/start",
"/fahrzeuge",
"/geraete",
"/wehren",
"/verwaltung",
"/admin",
];
// Geschützte API-Routen (401-Manifest). Neue API-Routen hier ergänzen.
const PROTECTED_API = ["/api/fahrzeuge", "/api/geraete", "/api/verwaltung"];
// Öffentliche Routen (Middleware-Allowlist).
const PUBLIC_ROUTES = ["/login", "/api/health"];
test.describe("Default-deny: geschützte Seiten", () => {
for (const path of PROTECTED_PAGES) {
test(`anonymer Aufruf von ${path} leitet auf /login um`, async ({
page,
}) => {
await page.goto(path);
await expect(page).toHaveURL(/\/login/);
});
}
});
test.describe("Default-deny: geschützte API-Routen", () => {
for (const path of PROTECTED_API) {
test(`anonymer Aufruf von ${path} liefert 401 ohne Daten-Leak`, async ({
request,
}) => {
const res = await request.get(path);
expect(res.status()).toBe(401);
const body = await res.text();
// Kein Daten-Leak: nur eine generische Fehlermeldung.
expect(body).not.toContain("brigade");
expect(body).not.toContain("passwort");
});
}
});
test.describe("Öffentliche Routen bleiben erreichbar", () => {
test("Login-Seite ist anonym erreichbar", async ({ page }) => {
await page.goto("/login");
await expect(
page.getByRole("heading", { name: "Anmelden" }),
).toBeVisible();
});
test("Health-Check ist anonym 200", async ({ request }) => {
const res = await request.get("/api/health");
expect(res.status()).toBe(200);
expect(await res.json()).toEqual({ status: "ok" });
});
});
void PUBLIC_ROUTES;