Skip to content

MSAL Angular protectedResourceMap does not reliably match base URLs (requires specific endpoint paths) #8499

@prrami

Description

@prrami

Core Library

MSAL.js (@azure/msal-browser)

Core Library Version

^5.1.0

Wrapper Library

MSAL Angular (@azure/msal-angular)

Wrapper Library Version

^5.0.0

Public or Confidential Client?

Public

Description

We are experiencing inconsistent behavior with MsalInterceptor in @azure/msal-angular when using protectedResourceMap.

The interceptor does not reliably attach tokens when using a base API URL, but works when using specific endpoint paths.

This makes it impractical to configure applications with multiple endpoints.

Error Message

The MSAL Angular interceptor does not consistently attach an access token when using a base API URL in protectedResourceMap.

Scenario

When configuring the interceptor with a base path:

protectedResourceMap.set('https://localhost:7203/api/', [
  'api://<client-id>/access_as_user',
]);

and making an API call:
https://localhost:7203/api/users/

Observed Behavior
❌ The Authorization header is not attached
❌ No access token is sent with the request

Workaround

When configuring the interceptor with a more specific endpoint path:

protectedResourceMap.set('https://localhost:7203/api/users/', [
  'api://<client-id>/access_as_user',
]);

Observed Behavior
✅ The Authorization header is attached
✅ The access token is correctly included in the request

MSAL Logs

No response

Network Trace (Preferrably Fiddler)

  • Sent
  • Pending

MSAL Configuration

export function MSALInstanceFactory(): IPublicClientApplication {
  return new PublicClientApplication({
    auth: {
      clientId: environment.CLIENT_ID,
      authority: environment.AUTHORITY,
      redirectUri: '/redirect',
      postLogoutRedirectUri: '/login',
    },
    cache: {
      cacheLocation: BrowserCacheLocation.LocalStorage,
    },
    system: {
      allowPlatformBroker: false,
      loggerOptions: {
        loggerCallback,
        logLevel: LogLevel.Info,
        piiLoggingEnabled: false,
      },
    },
  });
}

Relevant Code Snippets

import {
  ApplicationConfig,
  provideBrowserGlobalErrorListeners,
  provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter, withInMemoryScrolling } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi, HTTP_INTERCEPTORS } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { providePrimeNG } from 'primeng/config';
import { routes } from './app.routes';
import Aura from '@primeuix/themes/aura';
import {
  MsalService,
  MsalInterceptor,
  MsalInterceptorConfiguration,
  MSAL_INTERCEPTOR_CONFIG,
  MsalGuardConfiguration,
  MSAL_INSTANCE,
  MSAL_GUARD_CONFIG,
  MsalGuard,
  MsalBroadcastService,
} from '@azure/msal-angular';
import {
  PublicClientApplication,
  InteractionType,
  BrowserCacheLocation,
  IPublicClientApplication,
  LogLevel,
} from '@azure/msal-browser';
import { environment } from '../environments/environment';

export function loggerCallback(logLevel: LogLevel, message: string) {
  console.log(message);
}

/* 🔹 MSAL instance */
export function MSALInstanceFactory(): IPublicClientApplication {
  return new PublicClientApplication({
    auth: {
      clientId: environment.CLIENT_ID,
      authority: environment.AUTHORITY,
      redirectUri: '/redirect',
      postLogoutRedirectUri: '/login',
    },
    cache: {
      cacheLocation: BrowserCacheLocation.LocalStorage,
    },
    system: {
      allowPlatformBroker: false,
      loggerOptions: {
        loggerCallback,
        logLevel: LogLevel.Info,
        piiLoggingEnabled: false,
      },
    },
  });
}

export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
  const protectedResourceMap = new Map<string, Array<string>>();
  protectedResourceMap.set('https://localhost:7203/api/', [
    'api://<client-id>/access_as_user',
  ]);

  protectedResourceMap.set(environment.GRAPH_API, ['user.read', 'profile']);

  return {
    interactionType: InteractionType.Redirect,
    protectedResourceMap,
  };
}

export function MSALGuardConfigFactory(): MsalGuardConfiguration {
  return {
    interactionType: InteractionType.Redirect,
    authRequest: {
      scopes: ['openid', 'profile', 'User.Read'],
    },
    loginFailedRoute: '/login',
  };
}

export const appConfig: ApplicationConfig = {
  providers: [
    /* 🔹 HttpClient required by MSAL */
    provideHttpClient(withInterceptorsFromDi()),
    {
      provide: MSAL_INSTANCE,
      useFactory: MSALInstanceFactory,
    },
    {
      provide: MSAL_GUARD_CONFIG,
      useFactory: MSALGuardConfigFactory,
    },
    {
      provide: MSAL_INTERCEPTOR_CONFIG,
      useFactory: MSALInterceptorConfigFactory,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true,
    },
    MsalService,
    MsalGuard,
    MsalBroadcastService,
    provideBrowserGlobalErrorListeners(),
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(
      routes,
      withInMemoryScrolling({
        scrollPositionRestoration: 'top',
        anchorScrolling: 'enabled',
      }),
    ),
    provideAnimations(),
    providePrimeNG({
      theme: {
        preset: Aura,
      },
    }),
  ],
};

Reproduction Steps

  1. Configure MSAL in app.config.ts with interceptor:
const protectedResourceMap = new Map<string, Array<string>>();

protectedResourceMap.set('https://localhost:7203/api/', [
  'api://<client-id>/access_as_user',
]);
  1. Register interceptor:
 provideHttpClient(withInterceptorsFromDi()),
    {
      provide: MSAL_INSTANCE,
      useFactory: MSALInstanceFactory,
    },
    {
      provide: MSAL_GUARD_CONFIG,
      useFactory: MSALGuardConfigFactory,
    },
    {
      provide: MSAL_INTERCEPTOR_CONFIG,
      useFactory: MSALInterceptorConfigFactory,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true,
    },
  1. Make an API call using Angular HttpClient:
    this.http.get('https://localhost:7203/api/users').subscribe();

  2. Open browser DevTools → Network tab → inspect the request
    Observe
    ❌ Authorization header is NOT attached
    ❌ No token is added to the request

  3. Update interceptor configuration to a more specific path:

protectedResourceMap.set('https://localhost:7203/api/users/', [
  'api://<client-id>/access_as_user',
]);
  1. Repeat the same API call:
    this.http.get('https://localhost:7203/api/studies').subscribe();

7.Observe
✅ Authorization header is now attached
✅ Token is added to the request

  1. Repeat similar test with Microsoft Graph:
    protectedResourceMap.set('https://graph.microsoft.com/v1.0', ['User.Read']);

Call:

this.http.get('https://graph.microsoft.com/v1.0/me').subscribe();
this.http.get('https://graph.microsoft.com/v1.0/me/photo/$value').subscribe();

Observe
/me may work
/me/photo/$value returns 401 (token not attached)

  1. Add exact endpoint mapping:
protectedResourceMap.set(
  'https://graph.microsoft.com/v1.0/me/photo/$value',
  ['User.Read']
);

Observe
✅ Token is attached
✅ Request succeeds

Conclusion from Reproduction:
Token is only attached for specific or exact paths

Expected Behavior

Base URL mapping should work:
protectedResourceMap.set('https://localhost:7203/api/', [...]);

👉 Should attach token for ALL:
/api/studies
/api/users
/api/orders/123

Actual Behavior:
Token is only attached when:
Full endpoint path is used

👉 This forces developers to define multiple mappings per endpoint, which is not scalable.

Identity Provider

Entra ID (formerly Azure AD) / MSA

Browsers Affected (Select all that apply)

Chrome, Edge

Regression

v3

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug-unconfirmedA reported bug that needs to be investigated and confirmedmsal-angularRelated to @azure/msal-angular packagemsal-browserRelated to msal-browser packagepublic-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