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>"
}