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>
This commit is contained in:
76
tests/e2e/auth-gating.spec.ts
Normal file
76
tests/e2e/auth-gating.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
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 1–3, 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;
|
||||
Reference in New Issue
Block a user