Skip to content

MSAL Sign-in with Outlook Fails — CORS error & 400 Bad Request on Token Exchange #7958

Description

@yashkumawat21

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

4.16.0

Wrapper Library

MSAL React (@azure/msal-react)

Wrapper Library Version

3.0.16

Public or Confidential Client?

Public

Description

I'm trying to implement sign-up and login with Outlook (Microsoft account) using MSAL.js (@azure/msal-browser@4.16.0) in a Single Page Application running at http://localhost:4200. After the user is redirected back with an authorization code, the token exchange request to https://login.microsoftonline.com/common/oauth2/v2.0/token fails with a CORS error and a 400 Bad Request. The MSAL error reads: post_request_failed: Network request failed: If the browser threw a CORS error, check that the redirectUri is registered in the Azure App Portal as type 'SPA'. I’ve already added http://localhost:4200 as a redirect URI under the SPA platform in the Azure App Portal, and I’m using the Authorization Code Flow with PKCE. Still, I’m seeing this issue during token exchange. I'd appreciate guidance on how to resolve this and ensure a successful login flow with MSAL.

Error Message

MsalAuthProvider.tsx:79 Login failed: BrowserAuthError: post_request_failed: Network request failed: If the browser threw a CORS error, check that the redirectUri is registered in the Azure App Portal as type 'SPA'

https://login.microsoftonline.com/common/oauth2/v2.0/token?client-request-id=019860a7-0b2f-7a9f-bba6-88199b22d062' from origin 'http://localhost:4200' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

MSAL Logs

No response

Network Trace (Preferrably Fiddler)

  • Sent
  • Pending

MSAL Configuration

{
  auth: {
    clientId: process.env.NX_MICROSOFT_TEAMS_CLIENT_ID as string,
    authority: 'https://login.microsoftonline.com/common',
    redirectUri: window.location.origin, // Use dynamic origin
    postLogoutRedirectUri: window.location.origin,
    navigateToLoginRequestUrl: false,
  },
  cache: {
    cacheLocation: 'localStorage',
    storeAuthStateInCookie: false,
  },
  system: {
    allowPlatformBroker: false, // Disables WAM Broker
    loggerOptions: {
      loggerCallback: (level, message, containsPii) => {
        if (containsPii) {
          return;
        }
        switch (level) {
          case LogLevel.Error:
            console.error(message);
            return;
          case LogLevel.Info:
            console.info(message);
            return;
          case LogLevel.Verbose:
            console.debug(message);
            return;
          case LogLevel.Warning:
            console.warn(message);
            return;
          default:
            return;
        }
      },
    },
  },
}

Relevant Code Snippets

// msalConfig.ts

export const msalInstance = new PublicClientApplication(msalConfig);

export const loginRequest = {
  scopes: ['User.Read']
};

// Initialize MSAL instance
let isInitialized = false;
let initPromise: Promise<void> | null = null;

export const initializeMsal = async (): Promise<void> => {
  if (isInitialized) return;
  
  if (initPromise) return initPromise;
  
  initPromise = msalInstance.initialize().then(() => {
    isInitialized = true;
    console.log('MSAL initialized successfully');
  }).catch((error) => {
    console.error('MSAL initialization failed:', error);
    throw error;
  });
  
  return initPromise;
};

export const isMsalInitialized = (): boolean => isInitialized;



// MsalAuthProvider.tsx
import React, { createContext, useContext, useEffect, useState } from 'react';
import { 
  AuthenticationResult, 
  AccountInfo, 
  InteractionStatus, 
  SilentRequest,
  EndSessionRequest
} from '@azure/msal-browser';
import { msalInstance, loginRequest } from './msalconfig';

interface MsalContextType {
  instance: typeof msalInstance;
  accounts: AccountInfo[];
  inProgress: InteractionStatus;
  isAuthenticated: boolean;
  login: () => Promise<AuthenticationResult | null>;
  loginSilent: (account: AccountInfo) => Promise<AuthenticationResult | null>;
  logout: (account?: AccountInfo) => Promise<void>;
  acquireTokenSilent: (request: SilentRequest) => Promise<AuthenticationResult | null>;
}

// Export the context so it can be used elsewhere if needed
export const MsalContext = createContext<MsalContextType | undefined>(undefined);

interface MsalAuthProviderProps {
  children: React.ReactNode;
}

export const MsalAuthProvider: React.FC<MsalAuthProviderProps> = ({ children }) => {
  const [accounts, setAccounts] = useState<AccountInfo[]>([]);
  const [inProgress, setInProgress] = useState<InteractionStatus>(InteractionStatus.Startup);
  const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);

  useEffect(() => {
    const updateAccountState = () => {
      const currentAccounts = msalInstance.getAllAccounts();
      setAccounts(currentAccounts);
      setIsAuthenticated(currentAccounts.length > 0);
    };

    // Initial account state
    updateAccountState();

    // Listen for account changes
    const callbackId = msalInstance.addEventCallback((event) => {
      if (event.eventType === 'msal:loginSuccess' || 
          event.eventType === 'msal:logoutSuccess' ||
          event.eventType === 'msal:accountAdded' ||
          event.eventType === 'msal:accountRemoved') {
        updateAccountState();
      }

      if (event.eventType === 'msal:loginStart' || 
          event.eventType === 'msal:logoutStart') {
        setInProgress(InteractionStatus.Login);
      } else if (event.eventType === 'msal:loginSuccess' || 
                 event.eventType === 'msal:loginFailure' ||
                 event.eventType === 'msal:logoutSuccess') {
        setInProgress(InteractionStatus.None);
      }
    });

    setInProgress(InteractionStatus.None);

    return () => {
      if (callbackId) {
        msalInstance.removeEventCallback(callbackId);
      }
    };
  }, []);

  const login = async (): Promise<AuthenticationResult | null> => {
    try {
      setInProgress(InteractionStatus.Login);
      const response = await msalInstance.loginPopup(loginRequest);
      return response;
    } catch (error) {
      console.error('Login failed:', error);
      throw error;
    } finally {
      setInProgress(InteractionStatus.None);
    }
  };

  const loginSilent = async (account: AccountInfo): Promise<AuthenticationResult | null> => {
    try {
      const silentRequest = {
        ...loginRequest,
        account: account,
      };
      const response = await msalInstance.acquireTokenSilent(silentRequest);
      return response;
    } catch (error) {
      console.error('Silent login failed:', error);
      throw error;
    }
  };

  const logout = async (account?: AccountInfo): Promise<void> => {
    try {
      setInProgress(InteractionStatus.Logout);
      const logoutRequest: EndSessionRequest = {
        account: account || accounts[0],
        postLogoutRedirectUri: window.location.origin,
      };
      await msalInstance.logoutPopup(logoutRequest);
    } catch (error) {
      console.error('Logout failed:', error);
      throw error;
    } finally {
      setInProgress(InteractionStatus.None);
    }
  };

  const acquireTokenSilent = async (request: SilentRequest): Promise<AuthenticationResult | null> => {
    try {
      const response = await msalInstance.acquireTokenSilent(request);
      return response;
    } catch (error) {
      console.error('Token acquisition failed:', error);
      throw error;
    }
  };

  const contextValue: MsalContextType = {
    instance: msalInstance,
    accounts,
    inProgress,
    isAuthenticated,
    login,
    loginSilent,
    logout,
    acquireTokenSilent,
  };

  return (
    <MsalContext.Provider value={contextValue}>
      {children}
    </MsalContext.Provider>
  );
};

export const useMsal = (): MsalContextType => {
  const context = useContext(MsalContext);
  if (!context) {
    throw new Error('useMsal must be used within a MsalAuthProvider');
  }
  return context;
};

// Export the context type as well for external use
export type { MsalContextType };

Reproduction Steps

//Login.tsx
import { useMsal } from './MsalAuthProvider';

const { login: msalLogin, inProgress, accounts } = useMsal();

// Microsoft/Outlook signup function
const signUpWithOutlook = async () => {
try {
setLoading(true);

  const loginResponse = await msalLogin();
  
  if (!loginResponse || !loginResponse.account) {
    throw new Error('Login response is null or account is missing');
  }

  const account = loginResponse.account;
  
  // Extract user data
  const email = account.username;
  const fullName = account.name || '';
  const firstName = fullName.split(' ')[0] || '';
  const lastName = fullName.split(' ').slice(1).join(' ') || '';
  const microsoftId = account.localAccountId;
  
  const data = {
    email: email,
    firstName: firstName,
    microsoftId: microsoftId,
    lastName: lastName,
    orgName: email.split('@').pop()?.split('.')[0],
    password: '',
    microsoftToken: loginResponse.accessToken,
  };

 ........

} catch (error: any) {
  setLoading(false);
  console.error('Microsoft signup error:', error);
  
  if (error instanceof BrowserAuthError) {
    if (error.errorCode === 'user_cancelled') {
      setSignUpError('Signup was cancelled by user');
    } else if (error.errorCode === 'interaction_in_progress') {
      setSignUpError('Another login is in progress. Please wait.');
    } else {
      setSignUpError('Microsoft signup failed. Please try again.');
    }
  } else {
    setSignUpError('Microsoft signup failed. Please try again.');
  }
}

};

Expected Behavior

User should be redirected to Microsoft login, then redirected back with a code, which is exchanged for tokens without error.

Identity Provider

Entra ID (formerly Azure AD) / MSA

Browsers Affected (Select all that apply)

Chrome

Regression

No response

Metadata

Metadata

Labels

Needs: Author FeedbackAwaiting response from issue authorbug-unconfirmedA reported bug that needs to be investigated and confirmedmsal-browserRelated to msal-browser packagemsal-reactRelated to @azure/msal-reactno-issue-activityIssue author has not responded in 5 dayspublic-clientIssues regarding PublicClientApplicationsquestionCustomer is asking for a clarification, use case or information.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions