INSIGHT-MVP/packages/frontend/src/auth/AuthContext.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

160 lines
3.8 KiB
TypeScript

import {
createContext,
useContext,
useState,
useCallback,
useEffect,
type ReactNode,
} from 'react';
import api, { setAccessToken } from '../api/client';
interface User {
id: string;
email: string;
firstName: string;
lastName: string;
avatar?: string | null;
phone?: string | null;
mobile?: string | null;
street?: string | null;
postalCode?: string | null;
city?: string | null;
role: string;
twoFactorEnabled: boolean;
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
login: (email: string, password: string, totpCode?: string) => Promise<LoginResult>;
loginWithToken: (accessToken: string) => Promise<void>;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
interface LoginResult {
success: boolean;
requiresTwoFactor?: boolean;
error?: string;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Beim Start: Silent Refresh versuchen
useEffect(() => {
const initAuth = async () => {
try {
const { data } = await api.post<{ accessToken: string }>(
'/auth/refresh',
);
setAccessToken(data.accessToken);
// User-Profil laden
const profileResponse = await api.get<User>('/users/me');
setUser(profileResponse.data);
} catch {
// Nicht eingeloggt - normal
setAccessToken(null);
setUser(null);
} finally {
setIsLoading(false);
}
};
initAuth();
}, []);
const login = useCallback(
async (
email: string,
password: string,
totpCode?: string,
): Promise<LoginResult> => {
try {
const { data } = await api.post<{
accessToken?: string;
user?: User;
requiresTwoFactor?: boolean;
}>('/auth/login', { email, password, totpCode });
if (data.requiresTwoFactor) {
return { success: false, requiresTwoFactor: true };
}
if (data.accessToken && data.user) {
setAccessToken(data.accessToken);
setUser(data.user);
return { success: true };
}
return { success: false, error: 'Unerwartete Antwort vom Server' };
} catch (err: unknown) {
const error = err as { response?: { data?: { message?: string } } };
return {
success: false,
error: error.response?.data?.message ?? 'Login fehlgeschlagen',
};
}
},
[],
);
/**
* SSO-Login: Access-Token direkt setzen und User-Profil laden.
* Wird von SsoCallbackPage aufgerufen.
*/
const loginWithToken = useCallback(async (token: string) => {
setAccessToken(token);
const { data } = await api.get<User>('/users/me');
setUser(data);
}, []);
const refreshUser = useCallback(async () => {
try {
const { data } = await api.get<User>('/users/me');
setUser(data);
} catch {
// Fehler ignorieren - User bleibt unverändert
}
}, []);
const logout = useCallback(async () => {
try {
await api.post('/auth/logout');
} catch {
// Fehler ignorieren
} finally {
setAccessToken(null);
setUser(null);
}
}, []);
return (
<AuthContext.Provider
value={{
user,
isAuthenticated: !!user,
isLoading,
login,
loginWithToken,
logout,
refreshUser,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth muss innerhalb von AuthProvider verwendet werden');
}
return context;
}