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
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:
199
Client-Registration/src/auth/AuthProvider.jsx
Normal file
199
Client-Registration/src/auth/AuthProvider.jsx
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user