feat: add SSO configuration page in admin section

New admin page at /admin/sso with:
- Live SSO status indicator (active/not configured)
- Step-by-step Azure App Registration guide
- Required values table (Tenant ID, Client ID, Client Secret)
- API permissions list (openid, profile, email, User.Read)
- Redirect URI with copy button
- Environment variables template
- Technical reference (endpoints, security info)

Accessible via sidebar: Administration → SSO-Konfiguration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-09 22:39:55 +01:00
parent 45cf644f81
commit eba738fdc5
3 changed files with 482 additions and 0 deletions

View file

@ -0,0 +1,472 @@
import { useQuery } from '@tanstack/react-query';
import api from '../api/client';
interface SsoStatus {
microsoft: boolean;
}
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 codeBlockStyle: React.CSSProperties = {
background: '#1e293b',
color: '#e2e8f0',
borderRadius: 'var(--radius-sm)',
padding: '1rem 1.25rem',
fontSize: '0.8125rem',
fontFamily: "'JetBrains Mono', 'Fira Code', 'Consolas', monospace",
lineHeight: 1.7,
overflowX: 'auto',
whiteSpace: 'pre',
marginTop: '0.75rem',
};
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',
};
export function AdminSsoPage() {
const { data: ssoStatus, isLoading } = useQuery<SsoStatus>({
queryKey: ['sso', 'status'],
queryFn: async () => {
const res = await api.get<SsoStatus>('/auth/sso/status');
return res.data;
},
});
const redirectUri = `${window.location.origin}/api/v1/auth/sso/microsoft/callback`;
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.5rem' }}>
<h1 style={{ fontSize: '1.5rem', fontWeight: 600 }}>SSO-Konfiguration</h1>
</div>
{/* Status-Anzeige */}
<div style={cardStyle}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem' }}>
<svg width="24" height="24" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="9" height="9" fill="#f25022" />
<rect x="11" y="1" width="9" height="9" fill="#7fba00" />
<rect x="1" y="11" width="9" height="9" fill="#00a4ef" />
<rect x="11" y="11" width="9" height="9" fill="#ffb900" />
</svg>
<h2 style={{ fontSize: '1.125rem', fontWeight: 600 }}>Microsoft Entra ID (Azure AD)</h2>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.75rem 1rem',
borderRadius: 'var(--radius-sm)',
background: ssoStatus?.microsoft ? '#f0fdf4' : '#fef2f2',
border: `1px solid ${ssoStatus?.microsoft ? '#bbf7d0' : '#fecaca'}`,
marginTop: '0.75rem',
}}>
<span style={{
display: 'inline-block',
width: 10,
height: 10,
borderRadius: '50%',
background: ssoStatus?.microsoft ? 'var(--color-success)' : 'var(--color-error)',
}} />
<span style={{
fontSize: '0.875rem',
fontWeight: 500,
color: ssoStatus?.microsoft ? '#166534' : '#991b1b',
}}>
{isLoading
? 'Status wird geladen...'
: ssoStatus?.microsoft
? 'SSO ist aktiv — Benutzer können sich mit Microsoft anmelden'
: 'SSO ist nicht konfiguriert — folge der Anleitung unten'}
</span>
</div>
</div>
{/* Einrichtungs-Anleitung */}
<div style={cardStyle}>
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, marginBottom: '1.25rem' }}>
Einrichtungsanleitung
</h2>
{/* Schritt 1 */}
<div style={stepStyle}>
<div style={stepNumberStyle}>1</div>
<div style={stepContentStyle}>
<h3 style={h3Style}>App Registration im Azure Portal anlegen</h3>
<p style={pStyle}>
Gehe zum{' '}
<a
href="https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade"
target="_blank"
rel="noopener noreferrer"
style={{ color: 'var(--color-primary)', textDecoration: 'underline' }}
>
Azure Portal &rarr; App registrations
</a>
{' '}und klicke auf <strong>New registration</strong>.
</p>
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>Feld</th>
<th style={thStyle}>Wert</th>
</tr>
</thead>
<tbody>
<tr>
<td style={tdStyle}><strong>Name</strong></td>
<td style={tdStyle}>INSIGHT Platform</td>
</tr>
<tr>
<td style={tdStyle}><strong>Supported account types</strong></td>
<td style={tdStyle}>
Accounts in this organizational directory only<br />
<span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>
(Single tenant nur euer Azure AD)
</span>
</td>
</tr>
<tr>
<td style={tdStyle}><strong>Redirect URI</strong></td>
<td style={tdStyle}>
Platform: <strong>Web</strong><br />
URI: <code style={inlineCodeStyle}>{redirectUri}</code>
</td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Schritt 2 */}
<div style={stepStyle}>
<div style={stepNumberStyle}>2</div>
<div style={stepContentStyle}>
<h3 style={h3Style}>Client Secret erstellen</h3>
<p style={pStyle}>
In der App Registration &rarr; <strong>Certificates &amp; secrets</strong> &rarr; <strong>New client secret</strong>.
</p>
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>Feld</th>
<th style={thStyle}>Wert</th>
</tr>
</thead>
<tbody>
<tr>
<td style={tdStyle}><strong>Description</strong></td>
<td style={tdStyle}>INSIGHT Platform SSO</td>
</tr>
<tr>
<td style={tdStyle}><strong>Expires</strong></td>
<td style={tdStyle}>
24 months (empfohlen)<br />
<span style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>
Danach muss das Secret erneuert werden
</span>
</td>
</tr>
</tbody>
</table>
<div style={{
marginTop: '0.75rem',
padding: '0.625rem 0.75rem',
background: '#fffbeb',
border: '1px solid #fde68a',
borderRadius: 'var(--radius-sm)',
fontSize: '0.8125rem',
color: '#92400e',
}}>
<strong>Wichtig:</strong> Den <strong>Value</strong> des Secrets sofort kopieren er wird nur einmalig angezeigt!
</div>
</div>
</div>
{/* Schritt 3 */}
<div style={stepStyle}>
<div style={stepNumberStyle}>3</div>
<div style={stepContentStyle}>
<h3 style={h3Style}>API Permissions konfigurieren</h3>
<p style={pStyle}>
In der App Registration &rarr; <strong>API permissions</strong> &rarr; <strong>Add a permission</strong> &rarr; <strong>Microsoft Graph</strong> &rarr; <strong>Delegated permissions</strong>.
</p>
<p style={pStyle}>Folgende Berechtigungen hinzufügen:</p>
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>Permission</th>
<th style={thStyle}>Typ</th>
<th style={thStyle}>Beschreibung</th>
</tr>
</thead>
<tbody>
<tr>
<td style={tdStyle}><code style={inlineCodeStyle}>openid</code></td>
<td style={tdStyle}>Delegated</td>
<td style={tdStyle}>Sign users in</td>
</tr>
<tr>
<td style={tdStyle}><code style={inlineCodeStyle}>profile</code></td>
<td style={tdStyle}>Delegated</td>
<td style={tdStyle}>View users' basic profile</td>
</tr>
<tr>
<td style={tdStyle}><code style={inlineCodeStyle}>email</code></td>
<td style={tdStyle}>Delegated</td>
<td style={tdStyle}>View users' email address</td>
</tr>
<tr>
<td style={tdStyle}><code style={inlineCodeStyle}>User.Read</code></td>
<td style={tdStyle}>Delegated</td>
<td style={tdStyle}>Sign in and read user profile</td>
</tr>
</tbody>
</table>
<p style={{ ...pStyle, marginTop: '0.5rem' }}>
Dann <strong>Grant admin consent</strong> klicken, um die Berechtigungen für alle Benutzer freizugeben.
</p>
</div>
</div>
{/* Schritt 4 */}
<div style={stepStyle}>
<div style={stepNumberStyle}>4</div>
<div style={stepContentStyle}>
<h3 style={h3Style}>Werte aus Azure Portal kopieren</h3>
<p style={pStyle}>
In der App Registration &rarr; <strong>Overview</strong> findest du die benötigten Werte:
</p>
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>Azure Portal Feld</th>
<th style={thStyle}>Umgebungsvariable</th>
</tr>
</thead>
<tbody>
<tr>
<td style={tdStyle}><strong>Directory (tenant) ID</strong></td>
<td style={tdStyle}><code style={inlineCodeStyle}>AZURE_TENANT_ID</code></td>
</tr>
<tr>
<td style={tdStyle}><strong>Application (client) ID</strong></td>
<td style={tdStyle}><code style={inlineCodeStyle}>AZURE_CLIENT_ID</code></td>
</tr>
<tr>
<td style={tdStyle}><strong>Client secret value</strong> (aus Schritt 2)</td>
<td style={tdStyle}><code style={inlineCodeStyle}>AZURE_CLIENT_SECRET</code></td>
</tr>
</tbody>
</table>
</div>
</div>
{/* Schritt 5 */}
<div style={stepStyle}>
<div style={stepNumberStyle}>5</div>
<div style={stepContentStyle}>
<h3 style={h3Style}>Umgebungsvariablen auf dem Server setzen</h3>
<p style={pStyle}>
Trage die Werte in die <code style={inlineCodeStyle}>.env</code>-Datei auf dem Server ein:
</p>
<div style={codeBlockStyle}>
{`AZURE_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
AZURE_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AZURE_REDIRECT_URI=${redirectUri}`}
</div>
</div>
</div>
{/* Schritt 6 */}
<div style={stepStyle}>
<div style={stepNumberStyle}>6</div>
<div style={stepContentStyle}>
<h3 style={h3Style}>Core-Service neu starten</h3>
<p style={pStyle}>
Nach dem Setzen der Umgebungsvariablen den Backend-Service neu starten:
</p>
<div style={codeBlockStyle}>
{`docker compose restart core`}
</div>
<p style={{ ...pStyle, marginTop: '0.75rem' }}>
Nach dem Neustart sollte der Status oben auf{' '}
<strong style={{ color: '#166534' }}>aktiv</strong> wechseln und
der Mit Microsoft anmelden"-Button auf der Login-Seite erscheinen.
</p>
</div>
</div>
</div>
{/* Funktionsweise */}
<div style={cardStyle}>
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, marginBottom: '1rem' }}>
Funktionsweise
</h2>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div style={{
padding: '1rem',
background: 'var(--color-bg)',
borderRadius: 'var(--radius-sm)',
}}>
<h3 style={{ ...h3Style, fontSize: '0.875rem' }}>Erstanmeldung</h3>
<p style={{ ...pStyle, fontSize: '0.8125rem', marginBottom: 0 }}>
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.
</p>
</div>
<div style={{
padding: '1rem',
background: 'var(--color-bg)',
borderRadius: 'var(--radius-sm)',
}}>
<h3 style={{ ...h3Style, fontSize: '0.875rem' }}>Sicherheit</h3>
<p style={{ ...pStyle, fontSize: '0.8125rem', marginBottom: 0 }}>
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.
</p>
</div>
</div>
</div>
{/* Technische Details */}
<div style={cardStyle}>
<h2 style={{ fontSize: '1.125rem', fontWeight: 600, marginBottom: '1rem' }}>
Technische Referenz
</h2>
<table style={tableStyle}>
<thead>
<tr>
<th style={thStyle}>Endpoint</th>
<th style={thStyle}>Beschreibung</th>
</tr>
</thead>
<tbody>
<tr>
<td style={tdStyle}><code style={inlineCodeStyle}>GET /api/v1/auth/sso/microsoft</code></td>
<td style={tdStyle}>Startet den SSO-Flow (Redirect zu Microsoft)</td>
</tr>
<tr>
<td style={tdStyle}><code style={inlineCodeStyle}>GET /api/v1/auth/sso/microsoft/callback</code></td>
<td style={tdStyle}>Callback von Microsoft (Token-Exchange + User-Provisioning)</td>
</tr>
<tr>
<td style={tdStyle}><code style={inlineCodeStyle}>GET /api/v1/auth/sso/status</code></td>
<td style={tdStyle}>Prüft ob SSO konfiguriert ist (für Login-Seite)</td>
</tr>
</tbody>
</table>
<h3 style={{ ...h3Style, marginTop: '1.25rem', fontSize: '0.9375rem' }}>Redirect URI</h3>
<p style={pStyle}>
Diese URI muss exakt so in der Azure App Registration eingetragen sein:
</p>
<div style={{
...codeBlockStyle,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '1rem',
}}>
<span>{redirectUri}</span>
<button
onClick={() => navigator.clipboard.writeText(redirectUri)}
style={{
padding: '0.25rem 0.5rem',
background: '#334155',
color: '#e2e8f0',
border: '1px solid #475569',
borderRadius: 'var(--radius-sm)',
fontSize: '0.75rem',
cursor: 'pointer',
whiteSpace: 'nowrap',
}}
>
Kopieren
</button>
</div>
</div>
</div>
);
}

View file

@ -6,6 +6,7 @@ import { AppLayout } from './AppLayout';
import { DashboardPage } from './DashboardPage';
import { AdminUsersPage } from '../admin/AdminUsersPage';
import { AdminTenantsPage } from '../admin/AdminTenantsPage';
import { AdminSsoPage } from '../admin/AdminSsoPage';
import { ProfilePage } from '../profile/ProfilePage';
function PrivateRoute({ children }: { children: React.ReactNode }) {
@ -46,6 +47,7 @@ export function App() {
<Route path="profile" element={<ProfilePage />} />
<Route path="admin/users" element={<AdminUsersPage />} />
<Route path="admin/tenants" element={<AdminTenantsPage />} />
<Route path="admin/sso" element={<AdminSsoPage />} />
</Route>
{/* Fallback */}

View file

@ -51,6 +51,14 @@ export function AppLayout() {
>
Mandanten
</NavLink>
<NavLink
to="/admin/sso"
className={({ isActive }) =>
`${styles.navLink} ${isActive ? styles.active : ''}`
}
>
SSO-Konfiguration
</NavLink>
</>
)}
</nav>