Authentication & Authorization
Eviworx ITSM verwendet JWT (JSON Web Tokens) mit HttpOnly Cookies, Microsoft Entra ID SSO, API-Keys für externe Systeme und ein granulares RBAC-System mit 28+ Permission-Modulen.
Authentication-Methoden
1. Email/Password (Klassisch)
POST /api/auth/login
{
"email": "admin@company.com",
"password": "SecurePassword123!"
}
Response (200 OK)
{
"user": {
"id": "clx...",
"email": "admin@company.com",
"name": "Admin User",
"role": "ADMIN",
"avatar": "https://..."
},
"expiresAt": "2026-01-28T11:45:00Z",
"sessionConfig": {
"accessTokenMinutes": 60,
"maxSessionHours": 12,
"idleTimeoutMinutes": 30
}
}
WICHTIG: Der JWT-Token wird als HttpOnly Cookie gesetzt (auth_token + refresh_token). Tokens werden NICHT im Response-Body zurückgegeben. JavaScript kann auf diese Cookies NICHT zugreifen (XSS-Protection).
Kontostatus: Nur aktive Konten melden sich an. Gesperrte und archivierte Konten werden an jedem Anmeldeweg abgewiesen: Passwort-Login, 2FA-Abschluss und Token-Refresh antworten mit 403 ACCOUNT_DEACTIVATED, die Microsoft-Anmeldung bricht ab und führt zurück auf die Anmeldeseite mit einem übersetzten Hinweis. Die Antwort ist für beide Zustände gleich, damit von außen nicht erkennbar ist, welcher vorliegt; das Audit hält den Grund fest (DENIED mit ACCOUNT_LOCKED bzw. ACCOUNT_ARCHIVED). Ein Login ändert den Kontostatus nie — ein archiviertes Konto wird nur in der User-Verwaltung ent-archiviert (users.archive). lastLogin wird erst nach vollständiger Anmeldung gesetzt, bei aktiver 2FA also nach dem zweiten Faktor. Die drei Kontozustände: Users & Roles API.
Eine Statusänderung wirkt sofort: Mit der Sperre enden alle Zugänge des Kontos — laufende Sitzungen, Refresh-Token, Push-Abos und offene Echtzeit-Verbindungen; der Rechte-Cache wird geleert. Danach antworten Anfragen, der Socket-Handshake und der Refresh mit 403 ACCOUNT_DEACTIVATED statt mit 401 SESSION_REVOKED, damit der Grund beim Konto steht und nicht bei der Sitzung. Wird das Konto wieder entsperrt, bleibt die alte Sitzung beendet und meldet wieder 401 SESSION_REVOKED — ein bereits getrennter Zugang kommt über sie nicht zurück.
Ein aktives Konto braucht zusätzlich eine nutzbare Rolle: Fehlt sie oder ist sie deaktiviert, antworten Passwort-Login und Token-Refresh mit 403 NO_USABLE_ROLE, und die Microsoft-Anmeldung bricht mit derselben Aussage ab. Der Kontostatus bleibt dabei aktiv — die Ursache liegt an der Rolle, und die Meldung sagt das auch so. Wird die Rolle eines angemeldeten Benutzers deaktiviert, endet sein Zugang ebenso sofort wie bei einer Kontosperre; der nächste Refresh nennt diesen Grund.
sessionConfig
Die Session-Timings gehören dem Server und werden dem Client mitgeliefert — bei Login, nach dem 2FA-Abschluss und bei GET /auth/me. Ein Client konfiguriert sie nicht selbst, sondern leitet daraus ab, wann er die Inaktivitäts-Warnung zeigt und wann er proaktiv refresht. Enthalten sind genau die drei Werte, die eine Client-Entscheidung beeinflussen:
| Feld | Bedeutung |
|---|---|
accessTokenMinutes | Laufzeit des Access-Tokens — Basis für das proaktive Refresh-Intervall |
maxSessionHours | Hartes Session-Ende (nicht verlängerbar) |
idleTimeoutMinutes | Inaktivitäts-Grenze — Basis für Warn-Dialog und Auto-Logout |
Response bei aktivierter MFA/TOTP (kein Cookie gesetzt!)
{
"requiresTwoFactor": true,
"twoFactorEnabled": true,
"requiresSetup": false,
"twoFactorPendingToken": "pending-jwt-token...",
"expiresAt": "2026-03-17T12:05:00Z",
"user": { "id": "...", "email": "admin@company.com" }
}
Danach muss der 2FA-Code verifiziert werden:
POST /api/auth/2fa/complete-login
{
"twoFactorPendingToken": "pending-jwt-token...",
"code": "123456"
}
Turnstile CAPTCHA (nach 3 Fehlversuchen)
Nach 3 fehlgeschlagenen Login-Versuchen innerhalb von 15 Minuten wird ein Cloudflare Turnstile CAPTCHA verlangt. Das Login-Request muss dann ein zusätzliches Feld enthalten:
{
"email": "admin@company.com",
"password": "SecurePassword123!",
"captchaToken": "turnstile-response-token..."
}
2. MFA / Zwei-Faktor-Authentifizierung (TOTP)
Eviworx bietet native TOTP-basierte MFA (SHA-256, FIPS 140-2 kompatibel). TOTP-Secrets werden mit AES-256-GCM verschlüsselt gespeichert (TWO_FACTOR_ENCRYPTION_KEY).
| Method | Endpoint | Beschreibung |
|---|---|---|
POST | /api/auth/2fa/setup | 2FA initialisieren → { secret, qrCodeDataUrl, backupCodes } (die Codes werden nur hier gezeigt) |
POST | /api/auth/2fa/verify-setup | Ersten Code verifizieren & 2FA aktivieren → 204 |
POST | /api/auth/2fa/complete-login | Login nach 2FA-Verifizierung abschließen |
POST | /api/auth/2fa/disable | 2FA deaktivieren (Passwort + Code erforderlich) → 204 |
POST | /api/auth/2fa/backup-codes | Backup-Codes neu generieren (Passwort + Code) → { backupCodes }; die alten verfallen |
GET | /api/auth/2fa/status | 2FA-Status → { enabled, required, verifiedAt, backupCodesRemaining, isEntraIdManaged } |
POST | /api/admin/users/:id/reset-2fa | Admin: 2FA für User zurücksetzen → { user } (Permission users.reset2FA; Self-Reset blockiert) |
Die /api/auth/2fa/*-Routen gelten für den eingeloggten User selbst; der Admin-Reset erfordert users.reset2FA. Ein Admin kann sein EIGENES 2FA nicht über den Reset-Endpoint zurücksetzen (400 SELF_RESET_NOT_ALLOWED — Backup-Codes oder zweiter Admin nötig); ein Konto ohne aktives 2FA ergibt 400 TWO_FACTOR_NOT_ENABLED.
3. Microsoft Entra ID SSO
# Step 1: Check if SSO is enabled
GET /api/auth/entra-id/config/public
Response
{
"isEnabled": true,
"tenantId": "your-tenant-id",
"clientId": "your-client-id"
}
# Step 2: Get OAuth URL
GET /api/auth/entra-id/login
Response
{
"authUrl": "https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/authorize?client_id=...&state=abc123..."
}
# Step 3: User authenticated by Microsoft, redirected to:
GET /api/auth/entra-id/callback?code=OAUTH_CODE&state=abc123...
# Callback validates state (CSRF protection), exchanges code for token
# User is redirected to /login?oauth=success
# JWT set in HttpOnly cookie
Konfiguration & User-Sync
Die EntraID-Routen sind unter zwei Präfixen gemountet: /api/auth/entra-id (OAuth-Login) und /api/entra-id (Admin-Konfiguration & Sync). Der User-Sync läuft als eingebauter täglicher Job („Entra ID User Sync", 01:00 UTC) und lässt sich zusätzlich jederzeit von Hand anstoßen (202 mit runId); ohne aktive Integration tut er nichts. Er arbeitet als Hintergrund-Job im Backend. Es läuft immer nur ein Sync: Wird er angestoßen, während schon einer läuft, antwortet die API mit 409 ENTRA_ID_SYNC_ALREADY_RUNNING und liefert runId und Fortschritt mit — ein Client hängt sich damit an den laufenden Lauf, statt einen zweiten zu starten. Der Fortschritt ist abfragbar. Die Anmeldung nutzt den OAuth-2.0-Authorization-Code-Flow; der User-Sync ruft Microsoft Graph mit den Zugangsdaten der App-Registrierung ab (Client-Credentials).
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/auth/entra-id/config/public | Öffentlicher SSO-Status (für Login-Seite) |
GET | /api/auth/entra-id/login | OAuth-URL (mit state) |
GET | /api/auth/entra-id/callback | OAuth-Callback (validiert state, setzt Cookie) |
GET/PUT | /api/entra-id/config | Konfiguration lesen/speichern (Admin) |
POST | /api/entra-id/test-connection | Verbindung testen (Admin) |
POST | /api/entra-id/sync | User-Sync von Hand anstoßen (202; läuft bereits einer: 409 ENTRA_ID_SYNC_ALREADY_RUNNING) |
GET | /api/entra-id/sync-progress | Live-Fortschritt des laufenden Syncs |
GET | /api/entra-id/sync-status | Status/Ergebnis des letzten Syncs |
Microsoft-Login und Entra-verwaltete Konten
Der Microsoft-Login ordnet eine Anmeldung allein über die unveränderliche Microsoft-ID (oid) einem Konto zu. Verknüpft wird ausschließlich im Sync: Wen der Sync nicht verknüpft hat, weist der Callback ab — auch einen passwortlosen E-Mail-Kontakt mit passender Adresse. Die Anmeldung endet dann auf der Anmeldeseite mit dem Hinweis OAUTH_NOT_SYNCED, denselben Grund hält das Audit fest (OAUTH_LOGIN, DENIED). Name und E-Mail eines verknüpften Kontos pflegt ebenfalls nur der Sync — so bleibt das Verzeichnis die führende Quelle und eine Anmeldung kann die dort gepflegten Daten nicht überschreiben.
Ein Konto, das der Sync verwaltet, meldet sich nur über Microsoft an: Der Passwort-Login antwortet 401 ACCOUNT_REQUIRES_PASSWORD_SETUP, ein Passwortwechsel 400 ACCOUNT_NO_PASSWORD. Beim Verknüpfen entfernt der Sync ein lokales Passwort samt lokaler Zwei-Faktor-Anmeldung und beendet offene Sitzungen — damit bleibt kein zweiter Anmeldeweg offen, der die Vorgaben des Verzeichnisses umgeht. Das interne System-Konto, unter dem automatisierte Vorgänge laufen, meldet sich auf keinem Weg an.
4. API-Keys (für externe Systeme)
Externe Systeme authentifizieren über den Header X-API-Key (kein Bearer-Token). Ein API-Key ist eine rollenbasierte Maschinen-Identität und erhält die Rechte seiner zugewiesenen Rolle (Unified Actor):
curl https://your-instance.com/api/tickets \
-H "X-API-Key: apk_8sJ2...your-key..."
Erstellung, Verwaltung (settings.manageRoles), Felder, Klartext-Key-Handling (Präfix apk_, nur 1× sichtbar), IP-Whitelist, Rate-Limit und die Laufzeit-Prüfungen sind vollständig dokumentiert unter API Keys.
Forgot / Reset Password
| Method | Endpoint | Beschreibung |
|---|---|---|
POST | /api/auth/forgot-password | Passwort-Reset-E-Mail anfordern → 204 (auch bei unbekannter Adresse oder gesperrtem Konto) |
POST | /api/auth/reset-password | Passwort mit Reset-Token setzen → 204 |
# Request password reset
POST /api/auth/forgot-password
{ "email": "user@company.com" }
# Reset password (with token from email)
POST /api/auth/reset-password
{ "token": "reset-token-from-email", "newPassword": "NewSecure123!" }
Einladungsemail / Invitation
Wenn ein Admin einen neuen Benutzer erstellt und eine Einladungsemail versendet, erhält der User einen Link zum Passwort-Setup:
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/auth/validate-invitation | Einladungstoken prüfen → { valid: true, email, userName } oder { valid: false, reason } mit reason INVALID_OR_EXPIRED | USER_NOT_FOUND | ALREADY_USED |
POST | /api/auth/setup-password | Passwort über Einladungslink setzen → 204 |
Auf einem gesperrten oder archivierten Konto setzen reset-password und setup-password kein Passwort. Die Antwort ist 400 INVALID_RESET_TOKEN bzw. 400 INVALID_INVITATION_TOKEN, wie bei einem ungültigen Token, und der Token ist danach verbraucht; validate-invitation meldet für solche Konten { valid: false, reason: INVALID_OR_EXPIRED }. So ist von außen nicht erkennbar, ob ein Konto gesperrt ist — das Audit hält den Grund fest (DENIED mit ACCOUNT_LOCKED bzw. ACCOUNT_ARCHIVED).
Session-Management
Aktive Sessions
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/auth/sessions | Alle aktiven Sessions anzeigen → { data } |
DELETE | /api/auth/sessions/others | Alle anderen Sessions beenden → { revokedCount } |
DELETE | /api/auth/sessions/:sessionId | Einzelne Session beenden (sessionId = sid) → 204 |
Session-Identität (sid)
Jede Session hat eine stabile UUID (sid), die im Access-Token steckt und über alle Token-Rotationen hinweg gleich bleibt. Sie ist die ID, mit der die Sessions-Liste arbeitet und mit der eine Session beendet wird. Ein Revoke trifft die Session daher unabhängig davon, wie oft der Token inzwischen rotiert wurde. Auch zwei Anmeldungen in derselben Sekunde bleiben getrennte Sessions.
GET /api/auth/sessions
{
"data": [
{
"id": "3f2a91c4-8d17-4b6e-9c05-1a7de2b8f430",
"deviceName": "Chrome on Windows",
"browser": "Chrome",
"os": "Windows",
"ipAddress": "192.0.2.10",
"createdAt": "2026-07-16T08:00:00.000Z",
"lastRefresh": "2026-07-16T11:30:00.000Z",
"expiresAt": "2026-07-16T20:00:00.000Z",
"isCurrent": true
}
]
}
| Feld | Bedeutung |
|---|---|
id | die sid (UUID) — stabil, für DELETE /sessions/:sessionId zu verwenden |
expiresAt | das harte Session-Ende (12 Std. ab Login); bleibt bei Refreshes unverändert |
lastRefresh | letzte Token-Erneuerung — der Aktivitäts-Indikator |
Die eigene Session lässt sich nicht per Revoke beenden (400 CANNOT_REVOKE_CURRENT) — dafür ist der Logout da. Eine unbekannte sid liefert 404 SESSION_NOT_FOUND, eine Anfrage ohne gültige Sitzung 400 NO_SESSION.
Token-Refresh
POST /api/auth/refresh
Response (200 OK)
{
"user": {
"id": "clx...",
"email": "admin@company.com",
"name": "Admin User"
},
"expiresAt": "2026-01-28T12:45:00Z"
}
Hinweis: Der neue Token wird wieder als HttpOnly Cookie gesetzt. Frontend muss NICHTS speichern - Cookie wird automatisch bei jedem Request mitgeschickt.
Rotation, Grace-Fenster & Reuse-Erkennung
Jeder Refresh rotiert den Refresh-Token: Der Aufrufer bekommt einen neuen, der alte wird entwertet. Das ist Standard — hat aber eine Tücke: Geht die Antwort unterwegs verloren (Timeout, Verbindungsabbruch), hat der Server bereits rotiert, der Client hält aber noch den alten Token. Ohne Gegenmaßnahme wäre die Session tot. Deshalb:
| Zeitpunkt der Einlösung | Verhalten |
|---|---|
| innerhalb 60 Sek. nach der Rotation | Der alte Token wird akzeptiert und erneut rotiert — dieselbe Session läuft weiter (Kette). Eine verlorene Antwort kostet damit nichts. |
| danach (bis 10 Min.) | Reuse-Verdacht: Ein längst ersetzter Token taucht wieder auf — das deutet auf einen entwendeten Token hin. Die GESAMTE Session wird sofort beendet (Echtzeit-Verbindungen getrennt), Audit-Eintrag REFRESH_TOKEN_REUSE. Folge-Requests erhalten 401 SESSION_REVOKED. |
| noch später | Der Token ist schlicht unbekannt → 401 REFRESH_TOKEN_EXPIRED. |
| Session wurde beendet | Vorrang vor allem anderen: Wurde die Session zwischenzeitlich beendet (Sessions-UI, Passwortwechsel, Reuse-Erkennung), wird JEDER Refresh abgelehnt — auch eine Einlösung innerhalb des Grace-Fensters. Sonst könnte ein gerade widerrufener Zugang über den alten Token wieder auferstehen. |
Die Session-Identität (sid) bleibt über alle Rotationen hinweg dieselbe — Sessions-Liste und Revoke hängen daran, nicht am wechselnden Token. Daraus folgt: Ist eine Session einmal beendet, stellt kein Pfad je wieder Tokens für sie aus.
Logout
POST /api/auth/logout
Response
{
"message": "Logged out successfully"
}
Automatisch:
- Access-Token wird geblacklistet (Redis, bis zum Ablauf)
- Refresh-Token wird invalidiert, alle Access-Tokens der Session werden sofort ungültig, WebSocket-Verbindungen getrennt
- Beide HttpOnly-Cookies werden gelöscht
- Audit-Log: LOGOUT (SUCCESS)
Logout ist bewusst nachsichtig: Der Endpunkt verlangt KEINE gültige Authentifizierung und antwortet IMMER mit 200 — die Cookies werden in jedem Fall gelöscht. Grund: Verlangte der Logout ein gültiges Access-Token, könnte man sich mit abgelaufenem oder widerrufenem Token nicht mehr abmelden; die HttpOnly-Cookies (inklusive des noch gültigen Refresh-Tokens) blieben im Browser liegen, und das Frontend kann sie selbst nicht entfernen. Davor greift ein IP-basiertes Rate-Limit, weil der Endpunkt unauthentifiziert erreichbar ist.
Nachsichtig heißt aber nicht leichtgläubig: Die zerstörerischen Nebenwirkungen — WebSocket-Verbindungen trennen und den Rechte-Cache verwerfen — laufen NUR, wenn die Identität belegt ist. Belegt heißt: gültig signiertes Access-Token, oder ein Refresh-Token-Cookie, dessen Datensatz serverseitig auf denselben Benutzer zeigt. Ein frei erfundenes Token führt also weiterhin zu 200 und gelöschten Cookies, aber nicht dazu, dass jemand Fremdes aus seinen Verbindungen geworfen wird.
Erneute Anmeldung ohne Logout
Meldet sich jemand neu an, ohne sich vorher abzumelden (derselbe Browser, altes Cookie noch da), wird die Vorgänger-Session vollständig beendet — nicht nur ihr Refresh-Token: Die alten Access-Tokens werden sofort ungültig, die WebSockets der Alt-Session werden getrennt, und die Sessions-Liste zeigt nur noch die neue Session. Push-Benachrichtigungen ziehen dabei mit: Meldet sich derselbe Benutzer neu an, wandert die Anmeldung auf die neue Session; wechselt der Benutzer im selben Browser, wird sie entfernt.
Aktuellen User abrufen
GET /api/auth/me
Response
{
"id": "clx...",
"email": "admin@company.com",
"name": "Admin User",
"roleId": "clx...",
"roleName": "admin",
"roleDisplayName": "Administrator",
"roleColor": "#1F6FEB",
"roleIsSystem": true,
"avatar": "https://...",
"language": "de",
"theme": "dark",
"timezone": "Europe/Berlin",
"dateTimeFormat": "dd.MM.yyyy HH:mm",
"showRealtimeUpdateToasts": true,
"workspaceSettings": { "version": 2, "dashboard": { "widgets": [ ... ] }, "workList": { "scope": "own" } },
"agentGroups": [{ "id": "clx...", "name": "IT Support L1", "color": "#3B82F6" }],
"createdAt": "2026-01-15T09:00:00Z",
"profile": {
"firstName": "Admin",
"lastName": "User",
"phone": "+49...",
"department": "IT",
"location": "Munich",
"isActive": true,
"isSyncedFromEntraID": false,
"lastLogin": "2026-08-14T07:12:00Z"
},
"sessionConfig": {
"accessTokenMinutes": 60,
"maxSessionHours": 12,
"idleTimeoutMinutes": 30
}
}
sessionConfig gehört nicht zum User-Objekt, sondern beschreibt die Session-Timings (siehe oben) — ein Client sollte es vor dem Speichern des Users abtrennen. Die Rechte des Aufrufers liefert GET /api/auth/permissions (siehe unten).
Nur Permissions abrufen
GET /api/auth/permissions
Response
{
"tickets": {
"viewAll": true,
"viewOwn": true,
"create": true,
"createForOthers": true,
"editStatus": true,
"editPriority": true,
"editCategory": true,
"assign": true,
"editAll": true,
"viewInternal": true,
"delete": true,
"restore": true,
"viewDeleted": true
},
"incidents": {
"viewAll": true,
"create": true,
"editAll": true,
"changeStatus": true,
"requestClosure": true,
"delete": true
},
"problems": { ... },
"changes": { ... },
"assets": { ... },
"workflows": { ... },
"cronjobs": { ... },
"contracts": { ... },
"licenses": { ... },
"knowledgeBase": { ... },
"users": { ... },
"roles": { ... },
"settings": { ... },
"audit": { ... }
}
Token-Lebensdauer & Sicherheit
| Eigenschaft | Wert |
|---|---|
| Access Token | 60 Min (konfigurierbar: ACCESS_TOKEN_EXPIRY_MINUTES) |
| Refresh Token | 100 Min (konfigurierbar: REFRESH_TOKEN_EXPIRY_MINUTES). Bewusst knapp bemessen: der späteste legitime Einsatz ist der Refresh nach einer Idle-Phase bei ~60 Min Token-Alter — längere Laufzeiten wären nur zusätzliche Angriffsfläche für gestohlene Tokens. |
| Max Session | 12 Std. hartes Limit, serverseitig durchgesetzt; auch per Refresh NICHT verlängerbar (konfigurierbar: SESSION_MAX_HOURS) |
| Idle Timeout | 30 Min Inaktivität → Auto-Logout (konfigurierbar: IDLE_TIMEOUT_MINUTES). Der Wert gehört dem Backend und wird dem Client mitgeteilt; durchgesetzt wird er im Frontend. |
| Rotation | Jeder Refresh rotiert den Refresh-Token; der alte bleibt 60 Sek. einlösbar (Grace) und wird danach 10 Min. als Stolperdraht behalten (Reuse-Erkennung). |
| Speicherort | HttpOnly Cookies (auth_token + refresh_token) |
| SameSite | lax (CSRF-Protection) |
| Secure | in Production true; per COOKIE_SECURE explizit überschreibbar (z.B. internes HTTP-Deployment) |
| Blacklist | Redis (bei Logout, bis zum Token-Ablauf) |
| Passwort-Hashing | PBKDF2-SHA512, 210k Iterationen (FIPS 140-2 kompatibel) |
Autorisierung (RBAC)
Ob eine Request per Session-Cookie (User) oder per X-API-Key (API-Key mit Rolle) kommt — beide durchlaufen dieselbe Rechteprüfung gegen die Permission-Matrix ihrer Rolle. Die aktuellen Rechte des eingeloggten Actors liefert GET /api/auth/permissions (siehe oben).
Der vollständige, maßgebliche Permission-Katalog (alle Module/Actions, System-Rollen, die drei Prüfebenen, Laden & Caching) ist zentral dokumentiert unter Permissions & RBAC. Die Verwaltung von API-Keys (Erstellung, Felder, Rate-Limit, IP-Whitelist, Laufzeit-Prüfungen) steht unter API Keys.
Profile-Management
Eigene Voreinstellungen aktualisieren
PUT /api/auth/profile
{
"theme": "dark",
"timezone": "Europe/Berlin",
"dateTimeFormat": "dd.MM.yyyy HH:mm",
"showRealtimeUpdateToasts": true,
"workspaceSettings": {
"version": 2,
"dashboard": { "widgets": [{ "id": "open-tickets" }, { "id": "sla-breaches", "hidden": true }] },
"workList": { "scope": "own", "sort": "dueDate", "groupBy": "dueDate" }
}
}
- Dieser Endpoint ist der Weg für die persönlichen Voreinstellungen des angemeldeten Benutzers: theme (light | dark | system), timezone und dateTimeFormat (jeweils auch "system" für die globale Vorgabe bzw. "local" für die Browser-Zeitzone), showRealtimeUpdateToasts und workspaceSettings.
- workspaceSettings ist die Arbeitsplatz-Personalisierung: dashboard.widgets trägt Reihenfolge (Array-Ordnung) und Sichtbarkeit ({ id, hidden? }, höchstens 50 Einträge), workList die Voreinstellungen der Arbeitsliste (scope: own | group | substitute, sort: dueDate | priority | updatedAt, groupBy: dueDate | type | none). version ist Pflicht. null setzt den Benutzer auf das Preset zurück, das sich aus seinen Rechten ergibt; fehlende Widgets hängt die Oberfläche in Preset-Reihenfolge an. Ein PUT ersetzt den gesamten Block — wer nur dashboard schreibt, muss workList aus dem Bestand mitschicken.
- Unbekannte Felder werden mit 400 abgelehnt. Stammdaten laufen über eigene Wege mit eigener Prüfung — Name und Avatar über PATCH /api/users/:id, die Sprache über PUT /api/users/:id/language.
- Änderungen werden auditiert (Feld-Diff), aber nur, wenn sich tatsächlich etwas ändert — ein Speichern ohne Änderung erzeugt keinen Eintrag.
Passwort ändern
POST /api/auth/change-password
{
"currentPassword": "OldPassword123!",
"newPassword": "NewSecurePassword456!",
"confirmPassword": "NewSecurePassword456!"
}
Antwort: 204 ohne Body. Dasselbe gilt für forgot-password, reset-password und setup-password. forgot-password antwortet bewusst auch dann mit 204, wenn die Adresse unbekannt ist — die API verrät nicht, welche Konten existieren.
Frontend-Integration
Login-Flow (JavaScript)
// 1. Login
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // ← IMPORTANT! So the cookie gets set
body: JSON.stringify({
email: 'admin@company.com',
password: 'your-password'
})
});
const { user, expiresAt, sessionConfig } = await response.json();
// Token is now in an HttpOnly cookie (invisible to JavaScript).
// sessionConfig tells you the server's timings — do not hardcode your own.
// 2. API request (cookie is sent automatically)
const tickets = await fetch('/api/tickets', {
credentials: 'include' // ← IMPORTANT!
}).then(r => r.json());
// 3. Load permissions (for UI)
const permissions = await fetch('/api/auth/permissions', {
credentials: 'include'
}).then(r => r.json());
// 4. Proactive token refresh — derive the interval from sessionConfig
// (here: refresh once ~80% of the access token lifetime has passed)
const refreshEveryMs = sessionConfig.accessTokenMinutes * 60 * 1000 * 0.8;
setInterval(async () => {
const res = await fetch('/api/auth/refresh', {
method: 'POST',
credentials: 'include'
});
// Only a 401/403 means the session is really gone. A network error or a
// 5xx is transient — retry later instead of logging the user out.
if (res.status === 401 || res.status === 403) redirectToLogin();
}, refreshEveryMs);
// 5. Logout — always succeeds, cookies are cleared even without a valid token
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
SSO-Integration (Entra ID)
Setup-Schritte
- Azure Portal: App-Registration erstellen
- Redirect-URI: https://your-domain.com/api/auth/entra-id/callback
- Client-Secret generieren
- Permissions: User.Read, GroupMember.Read.All
- Base-Group erstellen (z.B. "ITSM-Users")
- Config in Eviworx UI eintragen
Das Entra-ID clientSecret wird beim Speichern verschlüsselt abgelegt (AES-256-GCM). GET-Responses liefern es nie zurück (auch nicht maskiert) — die „konfiguriert"-Anzeige läuft über ein _hasClientSecret-Flag; ein unverändertes Secret lässt man beim Speichern einfach weg (Bestand bleibt erhalten). Ein bereits verschlüsselter Wert (Präfix enc:v1:) wird als Eingabe abgelehnt — 400 ENTRA_ID_SECRET_CIPHERTEXT, damit ein zurückgespieltes Chiffrat nicht ein zweites Mal verschlüsselt wird. OAuth-Login und User-Sync entschlüsseln es zur Laufzeit.
Zugang und Rollenzuordnung
- Zugang haben nur Konten, die der Entra-Sync aus der Basis-Gruppe angelegt hat; Änderungen an der Gruppe greifen mit dem nächsten Sync — täglich um 01:00 UTC oder sofort von Hand. Der Microsoft-Login selbst fragt keine Gruppenmitgliedschaft ab und legt keine Konten an: Er ordnet die Anmeldung allein über die Microsoft-ID (oid) einem synchronisierten Konto zu und lässt nur ein aktives Konto mit nutzbarer Rolle durch.
- Die Rollenzuordnung steht an der Rolle: Feld entraIDRoleId mit der ID einer Entra-Gruppe. Eine Gruppe gehört zu höchstens einer Rolle; es zählen nur aktive Rollen.
- Ein Treffer: das Konto erhält diese Rolle. Mehrere Treffer: die Rolle mit der niedrigsten Priority-Zahl gewinnt.
- Kein Treffer: Konflikt (entraIDConflict = true). Bestehende Konten behalten ihre Rolle, neue erhalten END_USER.
Sync-Ablauf, Sperren beim Verlassen der Basis-Gruppe, Schutzschwellen und die Rollenauswahl bei Konflikt-Konten: User Management.
Abbruch der Microsoft-Anmeldung: Scheitert eine Microsoft-Anmeldung, endet sie immer auf der Anmeldeseite: Der Callback leitet auf /login?oauthError=<CODE> weiter, und die Oberfläche zeigt den Grund in der Anmeldesprache. Die Codes sind eine feste Menge: OAUTH_MISSING_STATE, OAUTH_INVALID_STATE, OAUTH_STATE_EXPIRED und OAUTH_STATE_ERROR (der Callback prüft seinen state, gültig für 5 Minuten), OAUTH_INTEGRATION_DISABLED (SSO ist abgeschaltet), OAUTH_NOT_SYNCED (der Sync kennt diese Identität nicht), ACCOUNT_DEACTIVATED, NO_USABLE_ROLE sowie OAUTH_CALLBACK_FAILED für alles Übrige; ein unbekannter Wert wird wie OAUTH_CALLBACK_FAILED behandelt. Die Ablehnungen am Konto hält das Audit mit demselben Code fest (OAUTH_LOGIN, DENIED).
Error-Handling
Jede abgelehnte Anfrage antwortet im selben Aufbau: error trägt den Meldungstext, errorCode den maschinenlesbaren Grund aus der folgenden Tabelle, details je nach Fall zusätzliche Angaben. Ein Client wertet immer errorCode aus, nie den Meldungstext — auch bei den Token-Ablehnungen, die denselben Aufbau tragen.
| Error Code | HTTP Status | Beschreibung |
|---|---|---|
INVALID_CREDENTIALS | 401 | Email oder Password falsch |
ACCOUNT_DEACTIVATED | 403 | Konto gesperrt oder archiviert — dieselbe Antwort für beide Zustände |
NO_USABLE_ROLE | 403 | Das Konto ist aktiv, seine Rolle trägt aber keine nutzbaren Rechte (keine Rolle, deaktivierte Rolle oder unbrauchbare Rechte-Matrix). Gilt für Login, Refresh und die Microsoft-Anmeldung. |
ACCOUNT_REQUIRES_PASSWORD_SETUP | 401 | Kein Passwort-Login möglich: Das Konto wird über Entra ID verwaltet (Anmeldung nur über Microsoft) oder hat noch kein Passwort gesetzt |
TOKEN_EXPIRED | 401 | Access-Token abgelaufen → Refresh ist der richtige nächste Schritt |
TOKEN_REVOKED | 401 | Access-Token per Logout geblacklistet |
SESSION_REVOKED | 401 | Session beendet (Sessions-UI, Passwortwechsel, Reuse-Erkennung) |
MAX_SESSION_EXCEEDED | 401 | 12-Std-Hard-Limit erreicht — Re-Login nötig, Refresh hilft nicht |
INVALID_TOKEN | 403 | Signatur/Format ungültig (NICHT „abgelaufen" — das ist TOKEN_EXPIRED mit 401) |
MISSING_USER_ID | 403 | Der Token ist formal gültig, trägt aber keine Benutzerkennung — er ist als Zugang unbrauchbar |
NO_TOKEN | 401 | Die Anfrage trägt weder ein auth_token-Cookie noch einen API-Key |
NO_REFRESH_TOKEN | 401 | Kein refresh_token-Cookie im Request |
REFRESH_TOKEN_EXPIRED | 401 | Refresh-Token unbekannt oder abgelaufen |
REFRESH_TOKEN_REUSE | 401 | Bereits ersetzter Refresh-Token erneut eingelöst → gesamte Session beendet |
AUTH_SERVICE_ERROR | 503 | Redis nicht erreichbar; im Zweifel wird der Zugriff verweigert |
AUTH_RATE_LIMIT_EXCEEDED | 429 | Zu viele Versuche (Header Retry-After). Denselben Code trägt die Konto-Sperre nach zu vielen Fehlversuchen; ein CAPTCHA wird dann nicht verlangt, weil es an einer laufenden Sperre nichts ändert. Sagt NICHTS über die Gültigkeit der Session aus — ein Client darf darauf NICHT ausloggen, sondern muss es später erneut versuchen. Grenzen und ENV-Overrides: Security → Brute-Force-Schutz |
CAPTCHA_REQUIRED | 400 | Login verlangt ein CAPTCHA (details.requiresCaptcha: true) |
CAPTCHA_INVALID | 400 | CAPTCHA-Prüfung fehlgeschlagen (details.requiresCaptcha: true) |
INVALID_CODE · INVALID_BACKUP_CODE · CODE_ALREADY_USED | 401 / 400 | 2FA-Code abgelehnt, mit details.remainingAttempts. 401 beim Login-Abschluss, 400 in den angemeldeten Step-Up-Routen (verify-setup, disable, backup-codes) — ein 401 dort würde einen Token-Refresh auslösen. |
TWO_FACTOR_NOT_ENABLED | 401 / 400 | Für das Konto ist keine 2FA aktiv (auch beim Admin-Reset) |
TWO_FACTOR_ALREADY_ENABLED | 400 | 2FA ist bereits aktiv — erst deaktivieren, dann neu einrichten |
SELF_RESET_NOT_ALLOWED | 400 | Ein Admin kann sein eigenes 2FA nicht zurücksetzen |
RATE_LIMITED | 429 | Zu viele 2FA-Fehlversuche — die Sperre läuft serverseitig ab |
CANNOT_REVOKE_CURRENT | 400 | Eigene Session kann nicht revoked werden → Logout nutzen |
NO_SESSION | 400 | Die Anfrage trägt keine gültige Sitzung (Sessions-Routen) |
SESSION_NOT_FOUND | 404 | Keine Session mit dieser sid |
CURRENT_PASSWORD_INCORRECT | 400 | Aktuelles Passwort falsch (bei change-password) |
ACCOUNT_NO_PASSWORD | 400 | Passwortwechsel auf einem Konto ohne lokales Passwort — etwa einem Konto, das über Entra ID verwaltet wird |
ENTRA_ID_NOT_CONFIGURED | 400 | SSO nicht konfiguriert |
Best Practices
💡 Tipps
1. Frontend-Integration
- • IMMER credentials: "include" bei fetch()
- • Token NICHT in localStorage speichern (XSS-Gefahr)
- • Permissions separat laden (nicht im Token)
- • Refresh-Intervall aus sessionConfig ableiten statt Zeiten fest zu verdrahten
- • Nur bei 401/403 ausloggen — Netzwerkfehler und 5xx sind vorübergehend und dürfen keine Session beenden
2. SSO (Entra ID)
- • Base-Group für Zugriffskontrolle: nur der Sync legt aus ihr Konten an
- • Rollenzuordnung per entraIDRoleId an der Rolle für automatische Role-Zuweisung
- • Der User-Sync läuft täglich um 01:00 UTC und lässt sich zusätzlich von Hand anstoßen
- • Gesperrte und archivierte Konten werden an jedem Anmeldeweg abgewiesen; ent-archivieren nur in der User-Verwaltung
3. Security
- • JWT_SECRET rotieren (alle 90 Tage, mit JWT_SECRET_OLD)
- • HTTPS erzwingen (Secure-Cookie nur bei HTTPS)
- • Audit-Logs monitoren (Failed-Logins, Brute-Force)
- • Token-Blacklist in Redis (bei Logout/Compromise)
Hinweis: Die Authentication ist integriert mit dem Enterprise Audit-System. Alle Login/Logout-Events werden geloggt mit IP, UserAgent, Success/Failure für Compliance & Forensik.