INSIGHT-MVP/packages/frontend/src/auth/SsoCallbackPage.tsx
Thomas Reitz 45cf644f81 feat: add Microsoft Entra ID (Azure AD) SSO integration
Backend-driven Authorization Code Flow with @azure/msal-node:
- EntraIdService: MSAL ConfidentialClientApplication, auth URL generation, token exchange
- SsoController: /auth/sso/microsoft (initiate) + /auth/sso/microsoft/callback (callback)
- AuthService.loginViaSso(): User provisioning (find by OID, auto-link by email, or create new)
- CSRF protection via state parameter stored in Redis
- SSO status endpoint for frontend feature detection

Frontend:
- "Mit Microsoft anmelden" button on login page (shown only when SSO is configured)
- SsoCallbackPage: handles redirect from backend, sets token, loads user profile
- AuthContext.loginWithToken(): new method for SSO token handling

Configuration:
- AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_REDIRECT_URI env vars
- docker-compose.yml updated to pass Azure vars to core service

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 22:31:34 +01:00

101 lines
2.8 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { setAccessToken } from '../api/client';
import { useAuth } from './AuthContext';
/**
* SsoCallbackPage - Verarbeitet den SSO-Callback vom Backend.
*
* Das Backend redirectet hierher mit dem Access-Token als Query-Parameter:
* /auth/sso/callback?token=eyJhbGci...
*
* Diese Seite:
* 1. Liest den Token aus der URL
* 2. Setzt ihn im AuthContext (loginWithToken)
* 3. Laedt das User-Profil
* 4. Navigiert zum Dashboard
*/
export function SsoCallbackPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { loginWithToken } = useAuth();
const [error, setError] = useState('');
useEffect(() => {
const handleCallback = async () => {
const token = searchParams.get('token');
if (!token) {
setError('Kein Token in der SSO-Antwort gefunden.');
setTimeout(() => navigate('/login'), 2000);
return;
}
try {
// Token setzen und User-Profil laden
await loginWithToken(token);
// Zum Dashboard navigieren
navigate('/', { replace: true });
} catch {
setError('SSO-Anmeldung fehlgeschlagen. Bitte erneut versuchen.');
setAccessToken(null);
setTimeout(() => navigate('/login'), 2000);
}
};
handleCallback();
}, [searchParams, navigate, loginWithToken]);
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
background: 'linear-gradient(135deg, #1a56db 0%, #1e3a5f 100%)',
}}
>
<div
style={{
background: 'white',
borderRadius: '0.75rem',
padding: '2.5rem',
textAlign: 'center',
maxWidth: '400px',
width: '100%',
}}
>
{error ? (
<>
<p style={{ color: '#dc2626', fontSize: '0.9375rem', marginBottom: '0.5rem' }}>
{error}
</p>
<p style={{ color: '#6b7280', fontSize: '0.8125rem' }}>
Sie werden zur Login-Seite weitergeleitet...
</p>
</>
) : (
<>
<div
style={{
width: 32,
height: 32,
border: '3px solid #e5e7eb',
borderTopColor: '#1a56db',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
margin: '0 auto 1rem',
}}
/>
<p style={{ color: '#374151', fontSize: '0.9375rem' }}>
Anmeldung wird abgeschlossen...
</p>
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
</>
)}
</div>
</div>
);
}