Frontend: - Add @azure/msal-browser and @azure/msal-react packages - Create authConfig.ts with MSAL configuration for PKCE flow - Create authService.ts for token acquisition and user info - Wrap App with MsalProvider in index.tsx - Replace dummy login with real MSAL loginPopup() in Login.tsx - Update App.tsx to use useIsAuthenticated/useMsal hooks - Update Profile.tsx to display real user data from claims - Update geminiService.ts to include access_token in WebSocket messages - Update WIPReviewer.tsx to pass msalInstance for auth Backend: - Add python-jose and httpx dependencies for JWT verification - Create auth_service.py with Azure AD JWKS fetching and token verification - Create auth.py FastAPI dependency for protected REST endpoints - Update main.py to verify tokens on WebSocket and protect /info endpoint - Add AZURE_TENANT_ID, AZURE_CLIENT_ID, DISABLE_AUTH to config 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
/**
|
|
* Authentication service for token management and user info extraction.
|
|
*/
|
|
import { IPublicClientApplication, InteractionRequiredAuthError } from '@azure/msal-browser';
|
|
import { apiTokenRequest } from './authConfig';
|
|
|
|
/**
|
|
* Acquires an access token silently, or via popup if interaction required.
|
|
* Use this before making authenticated API calls.
|
|
*/
|
|
export const getAccessToken = async (msalInstance: IPublicClientApplication): Promise<string | null> => {
|
|
const account = msalInstance.getActiveAccount();
|
|
if (!account) {
|
|
console.error('No active account found');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
// Try silent token acquisition first
|
|
const response = await msalInstance.acquireTokenSilent({
|
|
...apiTokenRequest,
|
|
account,
|
|
});
|
|
return response.accessToken;
|
|
} catch (error) {
|
|
if (error instanceof InteractionRequiredAuthError) {
|
|
// Fallback to popup if silent fails (e.g., token expired, new consent required)
|
|
try {
|
|
const response = await msalInstance.acquireTokenPopup(apiTokenRequest);
|
|
return response.accessToken;
|
|
} catch (popupError) {
|
|
console.error('Failed to acquire token via popup:', popupError);
|
|
return null;
|
|
}
|
|
}
|
|
console.error('Failed to acquire token:', error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* User info interface matching Azure AD claims
|
|
*/
|
|
export interface UserInfo {
|
|
name: string;
|
|
email: string;
|
|
firstName: string;
|
|
lastName: string;
|
|
accountType: string;
|
|
}
|
|
|
|
/**
|
|
* Extract user information from the active account
|
|
*/
|
|
export const getUserInfo = (msalInstance: IPublicClientApplication): UserInfo | null => {
|
|
const account = msalInstance.getActiveAccount();
|
|
if (!account) return null;
|
|
|
|
const idTokenClaims = account.idTokenClaims as Record<string, unknown>;
|
|
|
|
return {
|
|
name: account.name || 'Unknown User',
|
|
email: account.username || '',
|
|
firstName: (idTokenClaims?.given_name as string) || account.name?.split(' ')[0] || '',
|
|
lastName: (idTokenClaims?.family_name as string) || account.name?.split(' ').slice(1).join(' ') || '',
|
|
accountType: 'Enterprise User', // Single tenant = enterprise users
|
|
};
|
|
};
|