First build
Some checks failed
Client Admin / build-deploy (push) Failing after 8s
Client Client / build-deploy (push) Failing after 3s
Client Registration / build-deploy (push) Failing after 20s
Client Tech / build-deploy (push) Failing after 1s
Client Home / build-deploy (push) Successful in 14s

This commit is contained in:
Grae Jones
2026-03-21 17:54:42 -07:00
parent 3647b304a3
commit fdb3e117a9
203 changed files with 35733 additions and 18189 deletions

View File

@@ -0,0 +1,199 @@
import React, { createContext, useContext, useState, useCallback, useMemo, useEffect, useRef } from 'react';
import { PublicClientApplication, InteractionRequiredAuthError } from '@azure/msal-browser';
import { msalConfig, loginRequest } from './authConfig';
/**
* AuthProvider for Entra External ID (Customer Identity / CIAM).
*
* Uses @azure/msal-browser v3 with popup flow.
* Supports Google, Apple, and Microsoft via identity_provider_hint.
*
* Exposed context shape:
* isLoading — true while MSAL initializes or a login is in progress
* isSignedIn — true when a user is authenticated
* user — { entraSubjectId, email, displayName, firstName, surname, provider }
* error — string | null
* signIn(hint) — initiates login popup for 'google' | 'apple' | 'microsoft'
* signOut() — clears session
* getAccessToken()— returns a fresh ID token for authenticating API calls
* clearError() — clears the error state
*/
// ── Provider hint → Entra External ID identity_provider_hint ─────────────
// Values must match the social IdP names configured in the CIAM tenant.
const PROVIDER_HINTS = {
google: 'google',
apple: 'apple.com',
microsoft: undefined, // No hint = email/password or MSA
};
// ── MSAL instance — module-level singleton ────────────────────────────────
const msalInstance = new PublicClientApplication(msalConfig);
// ── Context ───────────────────────────────────────────────────────────────
const AuthContext = createContext(null);
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within <AuthProvider>');
return ctx;
}
// ── Map ID token claims → user object ─────────────────────────────────────
function claimsToUser(claims, provider) {
return {
entraSubjectId: claims.oid ?? claims.sub ?? null,
email: claims.email ?? claims.preferred_username ?? null,
displayName: claims.name ?? null,
firstName: claims.given_name ?? null,
surname: claims.family_name ?? null,
provider: provider ?? 'unknown',
};
}
// ── Provider ──────────────────────────────────────────────────────────────
export function AuthProvider({ children }) {
const [isLoading, setIsLoading] = useState(true);
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
const initialized = useRef(false);
const isSignedIn = !!user;
// ── Initialize MSAL + handle redirect response on mount ───────────
useEffect(() => {
if (initialized.current) return;
initialized.current = true;
(async () => {
try {
await msalInstance.initialize();
// Handle returning from loginRedirect
const redirectResult = await msalInstance.handleRedirectPromise();
if (redirectResult?.idTokenClaims) {
const provider = sessionStorage.getItem('adp_pending_provider') ?? 'unknown';
sessionStorage.removeItem('adp_pending_provider');
setUser(claimsToUser(redirectResult.idTokenClaims, provider));
setIsLoading(false);
return;
}
// Restore existing cached account
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
try {
const silent = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});
if (silent?.idTokenClaims) {
const idp = accounts[0].idTokenClaims?.idp ?? 'unknown';
setUser(claimsToUser(silent.idTokenClaims, idp));
}
} catch (silentErr) {
if (silentErr instanceof InteractionRequiredAuthError) {
await msalInstance.clearCache();
} else {
console.warn('[Auth] Silent refresh failed:', silentErr.message);
}
}
}
} catch (err) {
console.error('[Auth] MSAL init error:', err);
setError('Authentication service unavailable. Please refresh the page.');
} finally {
setIsLoading(false);
}
})();
}, []);
// ── Sign in ────────────────────────────────────────────────────────
const signIn = useCallback(async (providerHint) => {
setIsLoading(true);
setError(null);
const hint = PROVIDER_HINTS[providerHint];
const request = {
...loginRequest,
...(hint && { extraQueryParameters: { identity_provider_hint: hint } }),
};
try {
const result = await msalInstance.loginPopup(request);
if (result?.idTokenClaims) {
setUser(claimsToUser(result.idTokenClaims, providerHint));
}
} catch (popupErr) {
const msg = popupErr?.message ?? '';
if (msg.includes('popup_window_error')) {
try {
sessionStorage.setItem('adp_pending_provider', providerHint ?? 'unknown');
await msalInstance.loginRedirect(request);
return;
} catch (redirectErr) {
console.error('[Auth] Redirect fallback failed:', redirectErr);
setError('Sign-in failed. Please try again.');
}
} else if (msg.includes('user_cancelled') || msg.includes('access_denied')) {
setError(null);
} else {
console.error('[Auth] Sign-in error:', popupErr);
setError('Sign-in failed. Please try again.');
}
} finally {
setIsLoading(false);
}
}, []);
// ── Get access token for API calls ────────────────────────────────
// Returns the ID token — used as Bearer token when calling the
// Registration Function. The Function validates it against the CIAM
// tenant and extracts the OID claim as the user's identity.
const getAccessToken = useCallback(async () => {
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) throw new Error('Not signed in');
try {
const result = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});
return result.idToken;
} catch (err) {
if (err instanceof InteractionRequiredAuthError) {
throw new Error('Session expired. Please sign in again.');
}
throw err;
}
}, []);
// ── Sign out ───────────────────────────────────────────────────────
const signOut = useCallback(async () => {
const accounts = msalInstance.getAllAccounts();
setUser(null);
try {
if (accounts.length > 0) {
await msalInstance.logoutPopup({ account: accounts[0] });
}
} catch {
// Local state already cleared
}
}, []);
const clearError = useCallback(() => setError(null), []);
const value = useMemo(() => ({
isLoading,
isSignedIn,
user,
error,
signIn,
signOut,
getAccessToken,
clearError,
}), [isLoading, isSignedIn, user, error, signIn, signOut, getAccessToken, clearError]);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

View File

@@ -0,0 +1,32 @@
/**
* MSAL Configuration for Entra External ID (Customer Identity)
*
* This uses the External ID tenant (CIAM) — different from the
* internal Entra tenant used by the Admin/Management console.
*
* TODO: Replace placeholder values with actual External ID tenant details.
*/
export const msalConfig = {
auth: {
clientId: '154c9111-14a0-4c0f-8132-7bc68254a74e',
authority: 'https://usimclients.ciamlogin.com/891f98f1-ed34-42a1-9b6c-28b0554d92c2',
redirectUri: window.location.origin,
postLogoutRedirectUri: window.location.origin,
knownAuthorities: ['usimclients.ciamlogin.com'],
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
};
export const loginRequest = {
scopes: ['openid', 'profile', 'email'],
};
// Registration Function API
export const API_BASE_URL = 'https://adpregapi.usimdev.com';
// Function key for Registration API (AuthorizationLevel.Function)
// TODO: Set this from your Azure Function → App Keys → default host key
export const API_FUNCTION_KEY = '';