Session-Sicherheit und User-Validierung
Übersicht
Das System implementiert eine mehrschichtige Sicherheitsarchitektur, um sicherzustellen, dass nur aktive und gültige User auf geschützte Inhalte zugreifen können.
Rollen und Berechtigungen
UserRole Enum
enum UserRole {
ADMIN // Systemrolle — bypasses alle Permission-Checks
MEMBER // Standardrolle — benötigt explizit zugewiesene Custom-Roles
}
ADMIN ist eine Systemrolle und nicht mit fachlichen Rollen (z. B. „Lehrer", „Schulleiter") gleichzusetzen. MEMBER war früher USER — Migration erfolgte mit ALTER TYPE "UserRole" RENAME VALUE 'USER' TO 'MEMBER'.
is_owner Flag
Das is_owner Boolean @default(false) Feld auf dem User-Modell kennzeichnet den Tenant-Inhaber:
- Exakt ein User pro Tenant hat
is_owner = true - Der Owner kann nicht gelöscht werden (
deleteUserByIdwirftDatabaseError) - Der Owner kann nicht zu
MEMBERdegradiert werden (updateUserByIdwirftDatabaseError) - Inhaberschaft kann per
transferOwnership()übertragen werden — nur an einen anderenADMIN
is_owner in JWT und Session
is_owner wird beim Login in den JWT geschrieben und bei jeder JWT-Validierung aus der DB aktualisiert:
// jwt-Callback in auth.ts — bei initialem Login und GitHub OAuth
token.isOwner = dbUser.is_owner ?? false;
// session-Callback — Weitergabe an Frontend
session.user.isOwner = (token.isOwner as boolean) ?? false;
Die Session-Typdefinition:
declare module 'next-auth' {
interface Session {
user: {
id: string;
email: string;
role: UserRole; // 'ADMIN' | 'MEMBER'
status: Status;
tenantId: string;
isOwner: boolean; // true = Tenant-Inhaber
emailVerifiedAt?: Date;
};
}
}
Sicherheitsebenen
1. JWT-Callback (auth.ts)
- Wann: Bei jeder JWT-Anfrage
- Was: Prüft, ob der User noch in der Datenbank existiert und aktiv ist; synchronisiert
role,isOwnerundtenantId - Aktion: Gibt leeren Token
{}zurück, wenn der User inaktiv oder gelöscht wurde
2. Session-Callback (auth.ts)
- Wann: Bei jeder Session-Anfrage
- Was: Überträgt alle Token-Felder inklusive
isOwnerin die Session - Aktion: Gibt eine leere Session zurück, wenn der Token ungültig ist
3. Middleware (proxy.ts)
Die Next.js-Middleware liegt in webapp/proxy.ts (in Next.js 15.5 wurde die Konventionsdatei von middleware.ts zu proxy.ts umbenannt; die Funktion heißt export default async function proxy(req)).
- Wann: Vor dem Zugriff auf geschützte Routen
- Was: Prüft den Session-Status ohne DB-Abfrage
- Aktion: Leitet zum Login weiter, wenn der User inaktiv ist
Funktionsweise
Automatische Session-Validierung
// Bei jeder JWT-Anfrage wird geprüft:
if (token.id && token.email && token.status) {
const dbUser = await prisma.user.findFirst({
where: { id: token.id, email: token.email }
});
if (!dbUser || dbUser.status !== Status.active) {
// Leerer Token invalidiert die Session
return {};
}
// Aktuelle DB-Werte in den Token schreiben
token.role = dbUser.role;
token.status = dbUser.status;
token.tenantId = dbUser.tenant_id?.toString() || '';
token.isOwner = dbUser.is_owner ?? false;
}
Session-Status-Behandlung
- User gelöscht: Leerer Token
{}→ Session wird invalidiert - User inaktiv: Leerer Token
{}→ Session wird invalidiert - User archiviert: Leerer Token
{}→ Session wird invalidiert - DB-Fehler: Leerer Token
{}→ Session wird invalidiert (fail-safe)
Middleware-Validierung (proxy.ts)
// Einfache Prüfung ohne DB-Abfrage
if (isProtectedRoute && isAuthenticated && session!.user.status !== 'active') {
return NextResponse.redirect(new URL('/login', req.nextUrl));
}
Vereinfachte Login-Logik
- Credentials-Login: Inaktive User werden direkt abgewiesen (
return false) - GitHub OAuth: Inaktive User werden zur Login-Seite weitergeleitet (
return '/login?error=auth_failed') - Keine separaten Deaktivierungs-Seiten: Einfache und konsistente Behandlung
- Generische Fehlermeldungen: Alle Fehler zeigen die gleiche, sichere Meldung
- Maximale Sicherheit: Keine Information über existierende User oder deren Status
- Einheitliche Weiterleitung: Alle Fehler führen zur Login-Seite
Berechtigungssystem
Das granulare Permission-System sitzt oberhalb der Session-Sicherheit. Während die Session-Sicherheit den Zugang zum Backend grundsätzlich schützt, regelt das Permission-System den Zugriff auf einzelne Ressourcen.
Vollständige Dokumentation: Berechtigungssystem (permissions.md)
Kurzzusammenfassung
| Rolle | Verhalten |
|---|---|
ADMIN | Bypasses alle Permission-Checks automatisch |
MEMBER | Benötigt explizit zugewiesene Custom-Roles mit Permissions |
Fehlender Zugriff führt zu redirect('/dashboard?no_access=1') — nicht zu einem HTTP-Fehler.
Session-Zugriff in Server Components
import { auth } from '@/auth';
import { hasPermission } from '@/lib/permissions';
import { redirect } from 'next/navigation';
export default async function MyPage() {
const session = await auth();
if (!session?.user) redirect('/login');
const allowed = await hasPermission(session, 'students', 'read');
if (!allowed) redirect('/dashboard?no_access=1');
// ...
}
Vorteile der neuen Architektur
Keine Endlosschleifen
- JWT-Callback: Setzt nur den Status, löscht nicht die Session
- Middleware: Prüft den bereits gesetzten Status ohne DB-Abfrage
- Effizient: Weniger Datenbankzugriffe
Bessere Performance
- Keine redundanten DB-Abfragen in der Middleware
- Status wird im Token gespeichert und wiederverwendet
- Schnellere Route-Validierung
Robuste Fehlerbehandlung
- Session bleibt bestehen bis zur nächsten JWT-Validierung
- Keine abrupten Session-Abbrüche
- Graceful Degradation bei DB-Problemen
Admin-Tools
Session-Bereinigung
- Route:
/api/auth/cleanup-sessions - Zugriff: Nur Administratoren
- Funktion: Löscht alle Sessions für inaktive User
Session-Statistiken
- Route:
GET /api/auth/cleanup-sessions - Zugriff: Nur Administratoren
- Funktion: Zeigt Übersicht über aktive/inaktive User und Sessions
Verwendung
Manuelle Session-Bereinigung
# Alle Sessions für inaktive User löschen
curl -X POST /api/auth/cleanup-sessions \
-H "Authorization: Bearer <admin-token>"
Session-Statistiken abrufen
# Statistiken über User und Sessions
curl GET /api/auth/cleanup-sessions \
-H "Authorization: Bearer <admin-token>"
Utility-Funktionen
isUserActive(userId, email)
Prüft, ob ein User existiert und aktiv ist.
userExists(userId, email)
Prüft, ob ein User existiert (unabhängig vom Status).
getUserStatus(userId, email)
Holt den aktuellen Status eines Users.
Konfiguration
Session-Lebensdauer
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 Tage
}
JWT-Lebensdauer
jwt: {
maxAge: 30 * 24 * 60 * 60, // 30 Tage
}
Monitoring und Logging
Logs
- User-Validierung:
User ${email} existiert nicht mehr oder ist inaktiv. Session wird gelöscht. - Middleware-Validierung (
proxy.ts): Keine DB-Abfragen, nur Status-Prüfung - Fehler:
Fehler bei der User-Validierung: ${error}
Dashboard
Die SessionManagement-Komponente bietet eine visuelle Übersicht über:
- Gesamtanzahl User
- Aktive/Inaktive User
- Aktive Sessions
- OAuth Accounts
Sicherheitsaspekte
Vorteile
- Echtzeit-Validierung: User-Status wird bei jeder Anfrage geprüft
- Mehrschichtige Sicherheit: Mehrere Validierungsebenen
- Keine Endlosschleifen: Optimierte Architektur
- Admin-Kontrolle: Manuelle Session-Verwaltung möglich
Best Practices
- Status-basierte Validierung: Keine DB-Abfragen in der Middleware
- Token-basierte Sicherheit: Status wird im JWT gespeichert
- Effiziente Middleware: Schnelle Route-Validierung
- Admin-Tools: Für manuelle Kontrolle
Sicherheitsfeatures
- Lautlose Fehlerbehandlung: Keine Information über existierende User
- Einheitliche Fehlermeldungen: Alle Login-Fehler werden gleich behandelt
- Keine User-Enumeration: Angreifer können nicht validieren, ob Emails existieren
- Keine Status-Preisgabe: User-Status wird nicht verraten
- Enterprise-Sicherheit: Erfüllt Sicherheitsstandards großer Plattformen
Troubleshooting
Häufige Probleme
-
ERR_TOO_MANY_REDIRECTS
- Ursache: Endlosschleife zwischen
proxy.tsund Auth - Lösung: Status-basierte Validierung ohne DB-Abfragen
- Ursache: Endlosschleife zwischen
-
User wird nach Login sofort abgemeldet
- Ursache: User-Status ist nicht
activein der Datenbank - Lösung: Prüfen Sie den User-Status in der DB
- Ursache: User-Status ist nicht
-
Session wird nicht gelöscht
- Ursache: JWT-Callback läuft nicht oder DB-Fehler
- Lösung: Überprüfen Sie die Logs und DB-Verbindung
Debugging
// Logs aktivieren
console.log('Session-Daten:', session);
console.log('Token-Daten:', token);
console.log('User-Status:', session?.user?.status);
// proxy.ts-Debugging
console.log('Route:', path);
console.log('Session vorhanden:', !!session);
console.log('User-Status:', session?.user?.status);
Architektur-Übersicht
Request → proxy.ts → NextAuth → JWT-Callback → Session-Callback
↓ ↓ ↓ ↓ ↓
Route-Check Status Token-Valid. DB-Check Session-Format
↓ ↓ ↓ ↓ ↓
Redirect Status Status-Set Status-Update Response
Datenfluss
- Request kommt an
- proxy.ts (Middleware) prüft Route und Session-Status (ohne DB)
- NextAuth validiert den JWT-Token
- JWT-Callback prüft User-Status in der DB
- Session-Callback formatiert die Session
- Response wird zurückgegeben