mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 09:06:40 +02:00
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>
101 lines
2.8 KiB
TypeScript
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>
|
|
);
|
|
}
|