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:
68
src/auth.ts
Normal file
68
src/auth.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { z } from "zod";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
import { authConfig } from "./auth.config";
|
||||
import { verifyPassword } from "@/lib/auth/password";
|
||||
import { checkRateLimit, recordAttempt } from "@/lib/auth/rate-limit";
|
||||
|
||||
const credSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const { handlers, auth, signIn, signOut } = NextAuth({
|
||||
...authConfig,
|
||||
providers: [
|
||||
...authConfig.providers,
|
||||
Credentials({
|
||||
credentials: { email: {}, password: {} },
|
||||
authorize: async (raw) => {
|
||||
const parsed = credSchema.safeParse(raw);
|
||||
if (!parsed.success) return null;
|
||||
const { email, password } = parsed.data;
|
||||
const key = `email:${email.toLowerCase()}`;
|
||||
// Rate-Limit greift für ALLE Credentials-Logins (default-deny).
|
||||
if (!(await checkRateLimit(key))) return null;
|
||||
const u = await db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
});
|
||||
if (!u || !u.aktiv || u.authTyp !== "local" || !u.passwortHash) {
|
||||
await recordAttempt(key, "fail");
|
||||
return null;
|
||||
}
|
||||
if (!(await verifyPassword(u.passwortHash, password))) {
|
||||
await recordAttempt(key, "fail");
|
||||
return null;
|
||||
}
|
||||
await recordAttempt(key, "ok");
|
||||
return {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
name: u.name,
|
||||
role: u.rolle,
|
||||
brigadeId: u.brigadeId,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
...authConfig.callbacks,
|
||||
// Authentik-Login-Gate: nur vorgemerkte, aktive authentik-Konten zulassen.
|
||||
async signIn({ user, account }) {
|
||||
if (account?.provider === "authentik") {
|
||||
const email = user.email;
|
||||
if (!email) return false;
|
||||
const u = await db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
});
|
||||
if (!u || !u.aktiv || u.authTyp !== "authentik") return false;
|
||||
user.role = u.rolle;
|
||||
user.brigadeId = u.brigadeId ?? null;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user