Skip to content

msal-browser v5.4.0 and msal-react v5.0.6 artifacts versions missing from npm #8426

@hrithik-skypoint

Description

@hrithik-skypoint

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

Image

Error Message

No response

MSAL Logs

No response

Network Trace (Preferrably Fiddler)

  • Sent
  • Pending

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

npm outdated

Reproduction Steps

  1. project was on latest msal-browser v5.4.0 and msal-react v5.0.6
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug-unconfirmedA reported bug that needs to be investigated and confirmedmsal-browserRelated to msal-browser packagemsal-reactRelated to @azure/msal-reactpublic-clientIssues regarding PublicClientApplicationsquestionCustomer is asking for a clarification, use case or information.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions