Application security
.NET Core Angular Architecture Azure Azure AD Best Practices C# JWT MS Graph SPA Tenants Web API

How to Protect a Web API with Azure AD Authentication

Welcome to today’s post.

In today’s post I will be showing you how to protect a Web API that is accessing Azure AD resources including using MS Graph API.

In previous posts I showed how to protect a custom web API using a custom JWT Bearer token. I also showed how to create a custom .NET Core Web API that accessed MS Graph Outlook Calendar with a bearer token passed from a web client application.

You might ask, why can’t we just use the approach we had before where we validate the JWT tokens generated in a custom format? The answer is that tokens issued from a third-party or custom identity provider are incompatible with access tokens issued from a Microsoft identity provider such as MS Graph or from Azure Active Directory.  

Also, when tokens are presented to a Web API for authorization, they require the correct scopes within the payload. In the case of MS Graph identity tokens, they cannot be validated when presented to a protected Web API method as they have different scopes that only allow access to consume Graph API methods.

With a custom Web API method that is protected by Azure AD, the token should be issued by an Azure AD authority and in addition, the Web API registered under the same tenant should have at least one API scope defined which is exposed. The client SPA application the user has logged into must also be configured in the tenant to access the exposed API scope of the registered Web API application. The exposed scope is then included in the generated id token when the user logs into the SPA application. When the HTTP request to the Web API is called with the id token as a bearer token, the token will then be accepted and validated by the Web API method provided the other properties of the token payload including the tenant Id, client Id, and authority are valid. The entire process is explanatory in the diagram below:

Secure Web API Azure AD

Note that in a previous post I showed how to call an MS Graph API from an Angular SPA application. In that case we authenticated through AD using MS Graph scopes and the id token was MS Graph compatible. If we are directly authenticating a user for access to the MS Graph API, the user token can be issued from any of the authorities personal MS account, work, or organization. The URI endpoints are:

https://login.microsoftonline.com/consumer/v2.0 (personal MS accounts)
https://login.microsoftonline.com/common/v2.0 (multi-tenant)
https://login.microsoftonline.com/[tenant-id]/v2.0 (tenant account)

However, when accessing a Web API, the user must be within an Azure Tenant as only an AD Administrator for the Tenant under which the Web API is registered can expose access to the Web API to a client SPA application. In addition, the token being authorized must include the tenant Id of the Web API being accessed.

With Web API applications, there is no user interaction, so the redirect URI is not required and is optional.

The following are the steps we will take to implement protection of a Web API application using Azure AD:

  1. Register a Client SPA application.
  2. Register a Web API application.
  3. Add API scopes to the Web API application.
  4. Permit the Client SPA to access the scope of the Web API application.
  5. Configure the Client SPA application for Azure AD authentication with the API scope.
  6. Configure the Web API application to accept Azure AD tokens with the API scope.

The steps are shown below:

Register the Client SPA Application

Within our Tenant we select the App Registration:

Create a new registration as shown:

Select the option Accounts in this organisation directory only (… – Single tenant) as shown in the registration screen below:

Select the URI redirection so that successful authentications are redirected to the specified endpoint:

As we are logging into this application from a SPA application, we will require that the generated id tokens be generated from the authorization endpoint, like the following URI:

https://login.microsoftonline.com/[tenant]/oauth2/v2.0/authorize/?...

For a SPA application select both Access tokens and ID tokens:

Expose an API Scope From the Registered API Application

We will need an Application ID URI to be defined within our registered API application. It is usually of the form:

api://[client-id]

This will be used to define what scopes will be exposed by our Web API app. With client applications we define scopes that are presented to the user when consenting access during the OAuth2 code flow authentication process. As Web API apps do not directly interact with users, they are required to expose delegated scopes.

To expose delegated API scopes, we open the action Expose an API:

 then add a scope as shown:

Enter the scope name as access_as_user:

Save the delegated scope.

Configure the Client SPA App for User Delegated API Permissions

For the user id token generated within the Client SPA application to be authenticated by the custom Web API, we will need to permit delegated access of the signed-in user of the Web API through the application scope that we created in the previous section. In the Manage menu select API permissions as shown:

In the Manage menu select API permissions as shown:

Select the menu item Add a permission:

Select the Application you want to provide permissions to access the exposed API:

Select or search for the API permissions you wish to provide delegated access for the signed-in user of your SPA application:

Once the permission is added, it will show in the configured permissions for the registered SPA application:

Changes to the Angular SPA Client

In the Angular SPA application, you will need to provide configuration so that your user can be validated for access to the Web API. You will need to provide an app Id, authority (including tenancy Id) and a redirect Uri:

OAuthSettings.ts

export const OAuthSettings = {
  appId: 'c0d3adff-3886-43b9-a203-76ec57bd997c',
  redirectUri: 'http://localhost:4200',
  authority: 'https://login.microsoftonline.com/thebookloanlibrary.onmicrosoft.com/',  
  validateAuthority: true,
  scopes: [
    "api://fce5149f-2969-4155-841e-a29e236262b2/access_as_user"
  ]
};

The authority can be of the forms:

https://login.microsoftonline.com/[domain-name].onmicrosoft.com 
https://login.microsoftonline.com/[tenant-id]

Note that unlike the case where we directly connect to Graph API from an Angular SPA client, we do not need to specify any Graph scopes such as user.read, calendars.read etc. here. If we did so, then the token generated by AD would be a Graph Token and will not be accepted as a Web API AD Azure bearer token. We will end up getting an unrecognized signature error in the API backend.

Our routing module will need to specify an authorization guard:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from '../app/home/home.component';
import { CalendarComponent } from './calendar/calendar.component';
import { AuthADGuard } from '../app/security/auth.adguard';

const routes: Routes = [
  {path: '', redirectTo: 'home', pathMatch: 'prefix'},
  {path: 'home', component: HomeComponent }, 
  {path: 'calendar', component: CalendarComponent, canActivate: [AuthADGuard]}  
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

The MSAL is configured using MSALModule.forRoot() and an AD interceptor is declared to attach the id token to the API HTTP request within the app module as shown:

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MsalModule } from '@azure/msal-angular';
import { OAuthSettings } from './reference/oauth-settings';
import { APP_CONFIG, CALENDAR_DI_CONFIG } from './app.config';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MenuComponent } from '../app/menu/menu.component';
import { CalendarComponent } from '../app/calendar/calendar.component';
import { MaterialModule } from './material/material.module';
import { AuthService } from './services/auth.service';
import { ApiService } from './services/api.service';
import { AuthADInterceptor } from './security/auth.adinterceptor';
import { HomeComponent } from './home/home.component';
import { ImagebannerComponent } from './imagebanner/imagebanner.component';

@NgModule({
  declarations: [
    AppComponent,
    MenuComponent,
    HomeComponent,
    ImagebannerComponent,
    CalendarComponent
  ],
  imports: [
    HttpClientModule,
    BrowserModule,
    AppRoutingModule,
    MaterialModule,
    BrowserAnimationsModule,
    NoopAnimationsModule,
    MsalModule.forRoot({
      auth: {
        authority: OAuthSettings.authority,
        clientId: OAuthSettings.appId,
        redirectUri: OAuthSettings.redirectUri,
        postLogoutRedirectUri: OAuthSettings.redirectUri 
      }
    })
  ],
  providers: [
    ApiService, 
    {
      provide: APP_CONFIG, 
      useValue: CALENDAR_DI_CONFIG
    },    
    AuthService,
    {
        provide: HTTP_INTERCEPTORS,
        useClass: AuthADInterceptor,
        multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

We implement a basic calendar component to call the Calendar Event Web API as shown:

import { Component, OnInit } from '@angular/core';
import { MsalService } from '@azure/msal-angular';
import { HttpClient } from '@angular/common/http';
import { OutlookCalendarEvent } from '../models/calendar-event'; 
import { ApiService } from '../services/api.service';

@Component({
  selector: 'app-calendar',
  templateUrl: './calendar.component.html',
  styleUrls: ['./calendar.component.scss']
})
export class CalendarComponent implements OnInit {

  calendar;
  events: OutlookCalendarEvent[] = [];

  constructor(private authService: MsalService, 
    private apiService: ApiService,
    private http: HttpClient) { 
    console.log(authService.getAllAccounts.length);
  }

  ngOnInit() {
    this.getCalendar();    
  }

  getCalendar() {
    console.log("getCalendar(): call apiService.getUserCalendarEvents()"); 
    this.apiService.getUserCalendarEvents().subscribe(([events]) => 
      {
        console.log("calendar API completed.");
      });
  }
}

Changes to .NET Core Web API

To configure our Web API for token validation we apply the following changes:

  1. Add AzureAD configuration settings.
{
  "AzureAd": {
  "Domain": "[domain-name].onmicrosoft.com",  
    "Instance": "https://login.microsoftonline.com/",
    "ClientId": "[client-id-of-registered-web-api]",
    "TenantId": "[tenant-id-your-app-is-contained-within]",
    "Audience": "web API custom app ID URI"
  },
	…
}

If the Audience value is omitted then the client ID is used if the token for the app is v2.0, else it is of the form:

http://api/[client-id]

for v1.0 tokens. To determine the version of the token and configure our API to validate OAuth2 Microsoft Identity generated JWT tokens, we will need to determine what version of JWT tokens are supported by our API app registered within the Azure tenant.

To determine the supported token version, we first open the app registration. Select Manifest.

Look for the value for the key accessTokenAcceptedVersion:

"acceptMappedClaims": null,
"accessTokenAcceptedVersion": 2,
"addIns": [],

If the value is 2, then the API will accept v2.0 tokens. If the value is null, the API will accept v1.0 tokens.

Install the following NuGet packages:

and

Enable JWT Bearer authentication and Azure AD token validation with Web API protection for the Azure AD configuration settings by using the following code in ConfigureServices():

using Microsoft.Identity.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;

...

public void ConfigureServices(IServiceCollection services)
{
    // Adds Microsoft Identity platform (AAD v2.0) support to protect this API
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddMicrosoftIdentityWebApi(Configuration, "AzureAd");
	…
}

To enable exceptions to show more details including user properties, enable the ShowPII property as shown:

IdentityModelEventSource.ShowPII = true;

This should only be enabled in pre-production environment during the testing and development stages. Due to changes for GDPR the default behavior of the Microsoft Identity Libraries since v2.2 no longer support logging of personal identifiable details.

In the application start up, in the Configure() method, ensure the following extensions are enabled:

app.UseAuthentication();
app.UseAuthorization();

In our controller to protect our API method with the Bearer authentication we make the following additions:

Include the following namespaces:

using Microsoft.AspNetCore.Authorization;

Add the declaration above our API method:

[Authorize]

Our API method will resemble the following:

[HttpGet("api/[controller]/GetCalendarEvents")]
[Authorize]
public async Task<IActionResult> GetCalendarEvents()
{
  try
  { 
    var calendarEvents = await _outlookCalendar.GetCalendarEvents();
    if (calendarEvents == null)
      return BadRequest("Calendar request failed!");
    return Ok(calendarEvents);
  }
  catch (Exception ex)
  {
    return BadRequest(ex.Message);                
  }
}

In our API the authorization provider we will need to create an MSAL client using the PublicClientApplicationBuilder with the correct authority and tenant ID:

public class DeviceCodeAuthProvider : IAuthenticationProvider
{
    private readonly IPublicClientApplication _msalClient;
    private readonly string[] _scopes;
    private IAccount _userAccount;
    private readonly string _accessToken;
    private readonly HttpContext _httpContext;

    public DeviceCodeAuthProvider(string appId, string tenantId, string[] scopes, HttpContext httpContext)
    {
        _httpContext = httpContext;

        _scopes = scopes;

        _msalClient = PublicClientApplicationBuilder
            .Create(appId)
            .WithAuthority(AadAuthorityAudience.AzureAdMyOrg, true)
            .WithTenantId(tenantId)
            .Build();
    }

Once our application is run, we will be presented the scopes to delegate access to the Graph API and profile details:

After submitting a request to the Web API from the SPA client, the token should successfully validate and the API endpoint executed.

The console output is shown below:

We can stop our debugger to inspect the request:

During debugging of our Web API, the bearer token that is accepted will be of the form:

eyJ0…………………………………………………….

As shown in the request object below:

If the token cannot be validated, you will see error messages such as those below in the console log:

Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Failed to validate the token.
Microsoft.IdentityModel.Tokens.SecurityTokenInvalidSignatureException: IDX10511: Signature validation failed.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: Bearer was challenged.

I will explain how the above errors occur and how to trouble shoot in a future post.

This has shown the API has been successfully protected using Azure Active Directory Authentication and Web API permissions.

That is all for today’s post. I hope you found the post useful and informative.

Social media & sharing icons powered by UltimatelySocial