import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import api from '../api/client'; interface SsoStatus { microsoft: boolean; } interface SsoConfigResponse { configured: boolean; config: { tenantId: string; clientId: string; redirectUri: string; clientSecretMasked: string; } | null; } const cardStyle: React.CSSProperties = { background: 'var(--color-bg-card)', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-sm)', border: '1px solid var(--color-border)', padding: '1.5rem', marginBottom: '1.5rem', }; const inlineCodeStyle: React.CSSProperties = { background: 'var(--color-bg)', padding: '0.125rem 0.375rem', borderRadius: 'var(--radius-sm)', fontSize: '0.8125rem', fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace", color: '#1e40af', }; const stepStyle: React.CSSProperties = { display: 'flex', gap: '1rem', marginBottom: '1.5rem', }; const stepNumberStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'center', width: 32, height: 32, borderRadius: '50%', background: 'var(--color-primary)', color: 'white', fontSize: '0.875rem', fontWeight: 700, flexShrink: 0, }; const stepContentStyle: React.CSSProperties = { flex: 1, paddingTop: '0.25rem', }; const h3Style: React.CSSProperties = { fontSize: '1rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: '0.5rem', }; const pStyle: React.CSSProperties = { fontSize: '0.875rem', color: 'var(--color-text-secondary)', lineHeight: 1.6, marginBottom: '0.5rem', }; const tableStyle: React.CSSProperties = { width: '100%', borderCollapse: 'collapse', marginTop: '0.75rem', }; const thStyle: React.CSSProperties = { padding: '0.625rem 0.75rem', textAlign: 'left', fontSize: '0.75rem', textTransform: 'uppercase', color: 'var(--color-text-muted)', borderBottom: '1px solid var(--color-border)', background: 'var(--color-bg)', }; const tdStyle: React.CSSProperties = { padding: '0.625rem 0.75rem', fontSize: '0.8125rem', borderBottom: '1px solid var(--color-border)', verticalAlign: 'top', }; const inputStyle: React.CSSProperties = { width: '100%', padding: '0.625rem 0.75rem', fontSize: '0.875rem', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-sm)', background: 'var(--color-bg-card)', color: 'var(--color-text)', fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace", outline: 'none', boxSizing: 'border-box' as const, }; const labelStyle: React.CSSProperties = { display: 'block', fontSize: '0.8125rem', fontWeight: 600, color: 'var(--color-text)', marginBottom: '0.375rem', }; const hintStyle: React.CSSProperties = { fontSize: '0.75rem', color: 'var(--color-text-muted)', marginTop: '0.25rem', }; export function AdminSsoPage() { const queryClient = useQueryClient(); const { data: ssoStatus, isLoading: statusLoading } = useQuery({ queryKey: ['sso', 'status'], queryFn: async () => { const res = await api.get('/auth/sso/status'); return res.data; }, }); const { data: ssoConfig, isLoading: configLoading } = useQuery({ queryKey: ['sso', 'config'], queryFn: async () => { const res = await api.get('/auth/sso/config'); return res.data; }, }); // Formular-State const [tenantId, setTenantId] = useState(''); const [clientId, setClientId] = useState(''); const [clientSecret, setClientSecret] = useState(''); const [redirectUri, setRedirectUri] = useState(''); const [saveSuccess, setSaveSuccess] = useState(false); // Default Redirect-URI const defaultRedirectUri = `${window.location.origin}/api/v1/auth/sso/microsoft/callback`; // Formular mit bestehenden Werten befuellen useEffect(() => { if (ssoConfig?.config) { setTenantId(ssoConfig.config.tenantId); setClientId(ssoConfig.config.clientId); setRedirectUri(ssoConfig.config.redirectUri); // Secret bleibt leer — wird nur bei Aenderung gesendet } else { // Default Redirect-URI setzen wenn noch keine Config da setRedirectUri(defaultRedirectUri); } }, [ssoConfig, defaultRedirectUri]); const saveMutation = useMutation({ mutationFn: async (data: { tenantId: string; clientId: string; clientSecret?: string; redirectUri: string; }) => { const res = await api.post('/auth/sso/config', data); return res.data; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['sso', 'status'] }); queryClient.invalidateQueries({ queryKey: ['sso', 'config'] }); setClientSecret(''); // Secret-Feld leeren setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 4000); }, }); const handleSave = () => { const data: { tenantId: string; clientId: string; clientSecret?: string; redirectUri: string; } = { tenantId, clientId, redirectUri, }; // Secret nur mitsenden wenn es geaendert wurde if (clientSecret) { data.clientSecret = clientSecret; } saveMutation.mutate(data); }; const isLoading = statusLoading || configLoading; const hasExistingConfig = !!ssoConfig?.config; const canSave = tenantId.trim() && clientId.trim() && redirectUri.trim() && (clientSecret.trim() || hasExistingConfig); // Secret nur noetig bei Erstconfig return (

SSO-Konfiguration

{/* Status-Anzeige */}

Microsoft Entra ID (Azure AD)

{isLoading ? 'Status wird geladen...' : ssoStatus?.microsoft ? 'SSO ist aktiv — Benutzer können sich mit Microsoft anmelden' : 'SSO ist nicht konfiguriert — folge der Anleitung unten'}
{/* Einrichtungs-Anleitung */}

Einrichtungsanleitung

{/* Schritt 1 */}
1

App Registration im Azure Portal anlegen

Gehe zum{' '} Azure Portal → App registrations {' '} und klicke auf New registration.

Feld Wert
Name INSIGHT Platform
Supported account types Accounts in this organizational directory only
(Single tenant — nur euer Azure AD)
Redirect URI Platform: Web
URI:{' '} {defaultRedirectUri}
{/* Schritt 2 */}
2

Client Secret erstellen

In der App Registration →{' '} Certificates & secrets →{' '} New client secret.

Feld Wert
Description INSIGHT Platform SSO
Expires 24 months (empfohlen)
Danach muss das Secret erneuert werden
Wichtig: Den Value des Secrets sofort kopieren — er wird nur einmalig angezeigt!
{/* Schritt 3 */}
3

API Permissions konfigurieren

In der App Registration →{' '} API permissions →{' '} Add a permission →{' '} Microsoft Graph →{' '} Delegated permissions.

Folgende Berechtigungen hinzufügen:

Permission Typ Beschreibung
openid Delegated Sign users in
profile Delegated View users' basic profile
email Delegated View users' email address
User.Read Delegated Sign in and read user profile

Dann Grant admin consent klicken, um die Berechtigungen für alle Benutzer freizugeben.

{/* Schritt 4 — Konfiguration eingeben und speichern */}
4

Werte aus Azure Portal hier eintragen und speichern

Kopiere die Werte aus der App Registration →{' '} Overview und trage sie in die Felder ein. Beim Speichern wird die SSO-Verbindung automatisch aktiviert.

{/* Tenant ID */}
setTenantId(e.target.value)} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" style={inputStyle} />
Azure Portal → App Registration → Overview → Directory (tenant) ID
{/* Client ID */}
setClientId(e.target.value)} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" style={inputStyle} />
Azure Portal → App Registration → Overview → Application (client) ID
{/* Client Secret */}
setClientSecret(e.target.value)} placeholder={ hasExistingConfig ? `Gespeichert (${ssoConfig?.config?.clientSecretMasked}) — leer lassen um beizubehalten` : 'Client Secret Value eingeben' } style={inputStyle} />
Azure Portal → App Registration → Certificates & secrets → Client secret Value {hasExistingConfig && ( {' '} — Leer lassen, um das bestehende Secret zu behalten )}
{/* Redirect URI */}
setRedirectUri(e.target.value)} placeholder={defaultRedirectUri} style={inputStyle} />
Muss exakt mit der Redirect URI in der Azure App Registration übereinstimmen
{/* Speichern-Button + Status */}
{saveSuccess && ( SSO-Konfiguration gespeichert und aktiviert! )} {saveMutation.isError && ( Fehler:{' '} {(saveMutation.error as Error)?.message || 'Konfiguration konnte nicht gespeichert werden'} )}
{/* Funktionsweise */}

Funktionsweise

Erstanmeldung

Wenn ein Benutzer sich zum ersten Mal mit Microsoft anmeldet, wird automatisch ein Konto angelegt (Rolle: Benutzer). Existiert bereits ein Konto mit derselben E-Mail-Adresse, wird das Microsoft-Konto automatisch verknüpft.

Sicherheit

Der SSO-Flow nutzt den OAuth2 Authorization Code Flow. Das Client Secret verlässt nie den Server. CSRF-Schutz via State-Parameter. Die Multi-Faktor-Authentifizierung (MFA) von Microsoft wird unterstützt.

{/* Technische Details */}

Technische Referenz

Endpoint Beschreibung
GET /api/v1/auth/sso/microsoft Startet den SSO-Flow (Redirect zu Microsoft)
GET /api/v1/auth/sso/microsoft/callback Callback von Microsoft (Token-Exchange + User-Provisioning)
GET /api/v1/auth/sso/status Prüft ob SSO konfiguriert ist (für Login-Seite)

Redirect URI

Diese URI muss exakt so in der Azure App Registration eingetragen sein:

{defaultRedirectUri}
); }