1
votes

I'm currently working on Azure AD authenticaton integration to Angular - .Net core 3.1 project. This is a project which was generated from Visual Studio 2019 template (ASP.NET Core Web App).
In Azure portal, I registered 2 application and configured by MS tutorial and this.

Two registed app:

  1. frontend_app (client id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx16e3)
  2. backend_api (client id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxfcc1)

But I published only one App service, which contains both SPA and API. After login, I got a token, which append to every api call with MSAL interceptor.

The problem is all of the calls return is: 401, due to 'audience is invalid'. in the auth token the audience value the client id of frontend_app.

How can I solve to accept the the audience? Is it correct to use 2 app registration for only one app service?

2
The 401 error is usually because your audience does not match the api you want to call, can you use jwt.ms to parse your access token and provide a screenshot?Carl Zhao

2 Answers

4
votes

I was having the same problem as you and believe I have come up with a solution. All the guides I originally followed were using the implicit flow. As Carl pointed out in his answer (which I don't believe properly addresses your issue), there's an auth flow which is the recommended way to go. Unfortunately the standard MSAL libraries from all the samples and guides are 1.x and don't support auth flow. Instead, you'll need to use MSAL.js 2.0. The catch is that the angular library is still in alpha

So, here's what I did to make it all work. I'm using an Angular 10 front-end with an ASP.NET Core 3.1 backend.

First, you create your backend api app registration (which you may not need to change). Here's the documentation for that: Register Web API. Important notes:

  • Using this method you do NOT need to add your front-end client id as an authorized application under the 'Expose an API' section. We'll handle that differently using auth flow.
  • No redirect URI is needed since your backend will not be logging the user in
  • You need at least one scope for everything to work

Then follow the MSAL.js 2.0 documentation to create the frontend app registration. The important notes are as follows:

  • Make sure you select the SPA platform and enter a valid redirect URI
  • DO NOT check the boxes for 'Implicit Grant'
  • Under 'API permissions', give your front-end application access to your backend api:
    • Under 'API permissions' click on 'Add permission', then click on the 'My APIs' tab
    • Find your backend application and select the appropriate scope.
    • Click 'Add permissions'
    • Optionally grant consent for your APIs

Here's what your app registrations should look similar to:

backend app registration expose an api

frontend app registration authentication

frontend app registration api permissions

Now for the code. For your angular app, first install the necessary modules:

npm install @azure/msal-browser @azure/msal-angular@alpha

Then add this to your app module:

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import {
  IPublicClientApplication,
  PublicClientApplication,
  InteractionType,
  BrowserCacheLocation,
  LogLevel,
} from '@azure/msal-browser';
import {
  MsalGuard,
  MsalInterceptor,
  MsalBroadcastService,
  MsalInterceptorConfiguration,
  MsalModule,
  MsalService,
  MSAL_GUARD_CONFIG,
  MSAL_INSTANCE,
  MSAL_INTERCEPTOR_CONFIG,
  MsalGuardConfiguration,
} from '@azure/msal-angular';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

const PROTECTED_RESOURCE_MAP: Map<string, Array<string>> = new Map([
  ['https://graph.microsoft.com/v1.0/me', ['user.read']],
  [
    'api/admin/users',
    ['api://<backend app id>/access_as_admin'],
  ],
]);

const IS_IE =
  window.navigator.userAgent.indexOf('MSIE ') > -1 ||
  window.navigator.userAgent.indexOf('Trident/') > -1;

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

export function MSALInstanceFactory(): IPublicClientApplication {
  return new PublicClientApplication({
    auth: {
      clientId: '<frontend app id>',
      authority:
        'https://login.microsoftonline.com/<azure ad tenant id>',
      redirectUri: 'http://localhost:4200',
      postLogoutRedirectUri: 'http://localhost:4200/#/logged-out',
    },
    cache: {
      cacheLocation: BrowserCacheLocation.LocalStorage,
      storeAuthStateInCookie: IS_IE, // set to true for IE 11
    },
    system: {
      loggerOptions: {
        loggerCallback,
        logLevel: LogLevel.Verbose,
        piiLoggingEnabled: false,
      },
    },
  });
}

export function MSALInterceptorConfigFactory(): MsalInterceptorConfiguration {
  return {
    interactionType: InteractionType.Redirect,
    protectedResourceMap: PROTECTED_RESOURCE_MAP,
  };
}

export function MSALGuardConfigFactory(): MsalGuardConfiguration {
  return {
    interactionType: InteractionType.Redirect,
  };
}

export function initializeApp(appConfig: AppConfigService) {
  const promise = appConfig
    .loadAppConfig()
    .pipe(tap((settings: IAppConfig) => {}))
    .toPromise();
  return () => promise;
}

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    HttpClientModule,
    MsalModule,
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MsalInterceptor,
      multi: true,
    },
    {
      provide: MSAL_INSTANCE,
      useFactory: MSALInstanceFactory,
    },
    {
      provide: MSAL_GUARD_CONFIG,
      useFactory: MSALGuardConfigFactory,
    },
    {
      provide: MSAL_INTERCEPTOR_CONFIG,
      useFactory: MSALInterceptorConfigFactory,
    },
    MsalService,
    MsalGuard,
    MsalBroadcastService,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Then you can simply toss the MsalGuard onto any route you want to protect.

For the backend, first install the Microsoft.Identity.Web package:

dotnet add package Microsoft.Identity.Web --version 1.3.0

Here's the relevant code in my Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
  // other stuff...
  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(options =>
    {
      Configuration.Bind("AzureAd", options);
    })
    .AddInMemoryTokenCaches();

  services.AddCors((options =>
  {
    options.AddPolicy("FrontEnd", builder =>
      builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
  }));
  // other stuff...
}
 
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  // other stuff...
  app.UseCors("FrontEnd");
  app.UseAuthentication();
  app.UseAuthorization();
  // other stuff...
}

appsettings.json contains:

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "Domain": "<azure ad domain>",
  "TenantId": "<azure ad tenant id>",
  "ClientId": "<backend app id>"
}
1
votes

As I said in the comment, the 401 error is usually because your audience does not match the api you want to call, so you need to make sure that your target audience is your api, in your question it should be your backend_api, I Use auth code flow to make a simple demonstration for you:

enter image description here

Get token: enter image description here

Parse the token:

enter image description here