Core Library
MSAL.js (@azure/msal-browser)
Core Library Version
5.4.0
Wrapper Library
MSAL React (@azure/msal-react)
Wrapper Library Version
5.0.6
Public or Confidential Client?
Public
Description
msal-browser v5.4.0 and msal-react v5.0.6 artifacts versions missing from npm
Error Message
No response
MSAL Logs
No response
Network Trace (Preferrably Fiddler)
MSAL Configuration
import {
Configuration,
LogLevel,
PublicClientApplication,
NavigationClient,
NavigationOptions,
} from "@azure/msal-browser";
import { logger } from "@skypointcloud/logger/react";
const log = logger.createComponentLogger("msalConfig");
/**
* MSAL Configuration for Azure AD B2C
*
* This configuration supports:
* - Azure AD B2C multi-policy flows (signin, resetpassword)
* - Token caching in localStorage/sessionStorage
* - Comprehensive logging for debugging
* - MSAL Browser v5 async initialization pattern
*/
interface B2CConfig {
instance: string;
tenant: string;
clientId: string;
cacheLocation: "localStorage" | "sessionStorage";
scopes: string[];
policies: {
signin: string;
resetpassword: string;
};
redirectUri: string;
postLogoutRedirectUri: string;
}
/**
* Default scopes required for all authentication flows
*/
const defaultScopes = ["openid"];
/**
* Creates MSAL configuration from B2C settings
* @param config B2C configuration object
* @returns MSAL Configuration object
*/
/**
* Custom navigation client that prevents MSAL from auto-redirecting away from /callback.
* MSAL v5 internally navigates to the URL where loginRedirect was initiated (ORIGIN_URI).
* This bypasses the Callback component, preventing createSession() from running.
* Returning false from navigateInternal tells MSAL to process the redirect hash in place.
*/
class CallbackAwareNavigationClient extends NavigationClient {
navigateInternal(url: string, options: NavigationOptions): Promise<boolean> {
// If we're on /callback and MSAL wants to navigate elsewhere, block it.
// Returning false tells MSAL to process the redirect response in place
// instead of navigating away (see RedirectClient.mjs line 238).
if (window.location.pathname === "/callback") {
log.info("Blocked MSAL navigation away from /callback", { targetUrl: url });
return Promise.resolve(false);
}
return super.navigateInternal(url, options);
}
}
function createMsalConfig(config: B2CConfig): Configuration {
// Merge custom scopes with default scopes (deduplicated)
return {
auth: {
authority: config.instance,
clientId: config.clientId,
redirectUri: config.redirectUri,
knownAuthorities: Object.values(config.policies).map((policy) => {
return config.instance.replace(config.policies.signin, policy);
}),
postLogoutRedirectUri: config.postLogoutRedirectUri,
},
cache: {
cacheLocation: config.cacheLocation,
},
system: {
navigationClient: new CallbackAwareNavigationClient(),
loggerOptions: {
loggerCallback: (level, message, containsPii) => {
if (containsPii) {
return;
}
switch (level) {
case LogLevel.Error:
log.error(message);
return;
case LogLevel.Info:
log.info(message);
return;
case LogLevel.Verbose:
log.debug(message);
return;
case LogLevel.Warning:
log.warn(message);
return;
}
},
logLevel: LogLevel.Warning,
},
},
};
}
/**
* Safely parse a JSON env var, returning the fallback on invalid JSON
* instead of crashing the app with an unhelpful SyntaxError.
*/
function safeJsonParse<T>(raw: string | undefined, fallback: T, envVarName: string): T {
if (!raw) return fallback;
try {
return JSON.parse(raw) as T;
} catch {
log.error(`Invalid JSON in ${envVarName}: "${raw}". Using fallback.`);
return fallback;
}
}
/**
* Initialize B2C configuration from environment variables.
* Validates required vars and fails fast with clear messages.
*/
function initializeB2CConfig(): B2CConfig {
const instance = import.meta.env.VITE_B2C_CONFIG_INSTANCE;
const tenant = import.meta.env.VITE_B2C_CONFIG_TENANT;
const clientId = import.meta.env.VITE_B2C_CONFIG_CLIENT_ID;
const missing: string[] = [];
if (!instance) missing.push("VITE_B2C_CONFIG_INSTANCE");
if (!tenant) missing.push("VITE_B2C_CONFIG_TENANT");
if (!clientId) missing.push("VITE_B2C_CONFIG_CLIENT_ID");
if (missing.length > 0) {
const msg = `Missing required environment variables: ${missing.join(", ")}. Check your .env file.`;
log.error(msg);
throw new Error(msg);
}
return {
instance,
tenant,
clientId,
cacheLocation: "sessionStorage",
scopes: safeJsonParse<string[]>(
import.meta.env.VITE_B2C_CONFIG_SCOPES,
[],
"VITE_B2C_CONFIG_SCOPES",
),
policies: safeJsonParse<{ signin: string; resetpassword: string }>(
import.meta.env.VITE_B2C_CONFIG_POLICIES,
{ signin: "", resetpassword: "" },
"VITE_B2C_CONFIG_POLICIES",
),
redirectUri: window.location.origin + "/callback",
postLogoutRedirectUri: window.location.href,
};
}
/**
* Clear stuck MSAL interaction state before initializing
* This prevents "interaction_in_progress" errors that lock users out
*/
function clearStuckInteractionState(): void {
const clearMsalKeys = (storage: Storage, storageName: string) => {
const keysToRemove: string[] = [];
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key && key.startsWith("msal.")) {
keysToRemove.push(key);
}
}
if (keysToRemove.length > 0) {
keysToRemove.forEach((k) => storage.removeItem(k));
log.info(`Cleared ${keysToRemove.length} keys from ${storageName}`);
}
};
// Check for stuck interaction state
let hasStuckState = false;
const checkStorage = (storage: Storage) => {
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
if (key && key.includes("interaction.status")) {
const value = storage.getItem(key);
if (value === "interaction_in_progress") {
return true;
}
}
}
return false;
};
if (typeof sessionStorage !== "undefined" && checkStorage(sessionStorage)) {
hasStuckState = true;
}
if (typeof localStorage !== "undefined" && checkStorage(localStorage)) {
hasStuckState = true;
}
if (hasStuckState) {
log.warn("Detected stuck interaction state, clearing before initialization...");
if (typeof sessionStorage !== "undefined") {
clearMsalKeys(sessionStorage, "sessionStorage");
}
if (typeof localStorage !== "undefined") {
clearMsalKeys(localStorage, "localStorage");
}
}
}
/**
* Create and initialize MSAL instance
*
* IMPORTANT: MSAL Browser v4 requires async initialization
* This must be called before any authentication operations
*
* @returns Initialized PublicClientApplication instance
*/
export async function createMsalInstance(): Promise<PublicClientApplication> {
// Clear any stuck interaction state BEFORE creating MSAL instance
clearStuckInteractionState();
const b2cConfig = initializeB2CConfig();
// Log B2C configuration for debugging (without sensitive values)
log.debug("B2C Configuration", {
instance: b2cConfig.instance,
tenant: b2cConfig.tenant,
policies: b2cConfig.policies,
redirectUri: b2cConfig.redirectUri,
});
const msalConfig = createMsalConfig(b2cConfig);
// Log derived MSAL configuration
log.debug("MSAL Configuration", {
authority: msalConfig.auth?.authority,
knownAuthorities: msalConfig.auth?.knownAuthorities,
});
const msalInstance = new PublicClientApplication(msalConfig);
// MSAL v4+ requirement: initialize() must be called and awaited
await msalInstance.initialize();
// Handle redirect promise to complete auth flow
// This is CRITICAL to prevent no_token_request_cache_error
try {
const response = await msalInstance.handleRedirectPromise();
if (response) {
log.info("Successfully handled redirect response");
}
} catch (error) {
log.error("Error handling redirect", error instanceof Error ? error : undefined, { error });
// Don't throw - let the app handle auth errors gracefully
}
return msalInstance;
}
/**
* Get login scopes (including default scopes)
*/
function getLoginScopes(b2cConfig: B2CConfig): string[] {
return Array.from(new Set([...b2cConfig.scopes, ...defaultScopes]));
}
/**
* Export singleton B2C config for use across the app
*/
export const b2cConfig = initializeB2CConfig();
/**
* Export policies for B2C flows (signin, resetpassword)
* Used by useB2CPolicies hook for policy-specific redirect handling
*/
export const policies = b2cConfig.policies;
/**
* Export login scopes for use in authentication flows
*/
export const loginScopes = getLoginScopes(b2cConfig);
/**
* Login request configuration for redirect flows
* Used by ProtectedRoute and other authentication flows
*/
export const loginRequest = {
scopes: loginScopes,
};
Relevant Code Snippets
Reproduction Steps
- project was on latest msal-browser v5.4.0 and msal-react v5.0.6
- run npm outdated -> it shows these are not available
Expected Behavior
msal-browser v5.4.0 and msal-react v5.0.6 should be available on npm
Identity Provider
Entra ID (formerly Azure AD) / MSA
Browsers Affected (Select all that apply)
None (Server)
Regression
No response
Core Library
MSAL.js (@azure/msal-browser)
Core Library Version
5.4.0
Wrapper Library
MSAL React (@azure/msal-react)
Wrapper Library Version
5.0.6
Public or Confidential Client?
Public
Description
msal-browser v5.4.0 and msal-react v5.0.6 artifacts versions missing from npm
Error Message
No response
MSAL Logs
No response
Network Trace (Preferrably Fiddler)
MSAL Configuration
Relevant Code Snippets
Reproduction Steps
Expected Behavior
msal-browser v5.4.0 and msal-react v5.0.6 should be available on npm
Identity Provider
Entra ID (formerly Azure AD) / MSA
Browsers Affected (Select all that apply)
None (Server)
Regression
No response