Using multiple APIs in Angular and ASP.NET Core with Azure AD authentication

This article shows how an Angular application could be used to access many APIs in a secure way. An API is created specifically for the Angular UI and the further APIs can only be access from the trusted backend which is under our control.

Code: https://github.com/damienbod/AzureADAuthRazorUiServiceApiCertificate

Posts in this series

Setup

The applications are setup so that the Angular application only accesses a single API which was created specifically for the UI. All other APIs are deployed in a trusted zone and require a secret or a certificate to use the service. With this, only a single access token leaves the secure zone and there is no need to handle multiple tokens in an unsecure browser. Secondly the API calls can be optimized so that the network loads which come with so many SPAs can be improved. The API is our gateway to the data required by the UI.

This is very like the backend for frontend application architecture (BFF) which is more secure than this setup because the security for the UI is also implemented in the trusted backend for the UI, ie (no access tokens in the browser storage, no refresh/renew in the browser). The advantage here is the structure is easier to setup with existing UI teams, backend teams and the technology stacks like ASP.NET Core, Angular support this structure better.

In this demo, we will be implementing the SPA in Angular but this could easily be switched out for a Blazor, React or a Vue.js UI. The Authentication is implemented using Azure AD.

The APIs

The API which was created for the UI uses Microsoft.Identity.Web to implement the Azure AD security. All API HTTP requests to this service require a valid access token which was created for this service. In the Startup class, the AddMicrosoftIdentityWebApiAuthentication is used to add the auth services for Azure AD to the application. The AddHttpClient is used so that the IHttpClientFactory can be used to access the downstream APIs. The different API client services are added as scoped services. CORS is setup so the Angular application can access the API. The CORS setup for the UI API calls should be configured as strict as possible. An authorize policy is added which validates the azp claim. This value must match the App registration setup for your UI application. If different UIs or different access tokens are allowed, then you would have to change this. An in memory cache is used to store the downstream API access tokens. The API access three different types of downstream APIs, a delegated API which uses the OBO flow to get a token, an application API, which uses the client credentials flow and the default scope and a graph API delegated API which uses the OBO flow again.


public void ConfigureServices(IServiceCollection services)
{
	services.AddHttpClient();
	services.AddOptions();

	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
	IdentityModelEventSource.ShowPII = true;
	JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

	services.AddCors(options =>
	{
		options.AddPolicy("AllowAllOrigins",
			builder =>
			{
				builder
					.AllowCredentials()
					.WithOrigins(
						"https://localhost:4200")
					.SetIsOriginAllowedToAllowWildcardSubdomains()
					.AllowAnyHeader()
					.AllowAnyMethod();
			});
	});

	services.AddScoped<GraphApiClientService>();
	services.AddScoped<ServiceApiClientService>();
	services.AddScoped<UserApiClientService>();

	services.AddMicrosoftIdentityWebApiAuthentication(Configuration)
		 .EnableTokenAcquisitionToCallDownstreamApi()
		 .AddInMemoryTokenCaches();

	services.AddControllers(options =>
	{
		var policy = new AuthorizationPolicyBuilder()
			.RequireAuthenticatedUser()
			.Build();
		options.Filters.Add(new AuthorizeFilter(policy));
	});

	services.AddAuthorization(options =>
	{
		options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy =>
		{
			// Validate ClientId from token
			// only accept tokens issued ....
			validateAccessTokenPolicy.RequireClaim("azp", "ad6b0351-92b4-4ee9-ac8d-3e76e5fd1c67");
		});
	});

	// .... + swagger
}

The API using no extra services

The API which returns data directly uses the correct JwtBearerDefaults.AuthenticationScheme scheme to validate the token and requires that the ValidateAccessTokenPolicy succeeds the authorize checks. Then the data is returned. This is pretty straight forward.

using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ApiWithMutlipleApis.Controllers
{
    [Authorize(Policy = "ValidateAccessTokenPolicy", 
        AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [ApiController]
    [Route("[controller]")]
    public class DirectApiController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new List<string> { "some data", "more data", "loads of data" };
        }
    }
}

API which uses the Application API

The ServiceApiCallsController implements the API will uses the ServiceApiClientService to request data from the application API. This is an APP to APP request and cannot be used from any type of SPA because the API can only be accessed by using a secret or a certificate. SPAs cannot keep or use secrets. Using it from our trusted web API solves this and it can use the data as needed or allowed.

using System.Collections.Generic;
using System.Threading.Tasks;
using ApiWithMutlipleApis.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ApiWithMutlipleApis.Controllers
{
    [Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [ApiController]
    [Route("[controller]")]
    public class ServiceApiCallsController : ControllerBase
    {
        private ServiceApiClientService _serviceApiClientService;

        public ServiceApiCallsController(ServiceApiClientService serviceApiClientService)
        {
            _serviceApiClientService = serviceApiClientService;
        }

        [HttpGet]
        public async Task<IEnumerable<string>> Get()
        {
            return await _serviceApiClientService.GetApiDataAsync();
        }
    }
}

The ServiceApiClientService uses the ITokenAcquisition to get an access token for the .default scope of the API. The access_as_application scope is added to the Azure App Registration for this API. The access token is requested using the OAuth client credentials flow. This flow is normal not used for delegated users. This is good if you have some type of global service or application level type of features with no users involved.

using Microsoft.Identity.Web;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;

namespace ApiWithMutlipleApis.Services
{
    public class ServiceApiClientService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly ITokenAcquisition _tokenAcquisition;

        public ServiceApiClientService(
            ITokenAcquisition tokenAcquisition,
            IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
            _tokenAcquisition = tokenAcquisition;
        }

        public async Task<IEnumerable<string>> GetApiDataAsync()
        {

            var client = _clientFactory.CreateClient();

            var scope = "api://b178f3a5-7588-492a-924f-72d7887b7e48/.default"; // CC flow access_as_application";
            var accessToken = await _tokenAcquisition.GetAccessTokenForAppAsync(scope);

            client.BaseAddress = new Uri("https://localhost:44324");
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var response = await client.GetAsync("ApiForServiceData");
            if (response.IsSuccessStatusCode)
            {
                var data = await JsonSerializer.DeserializeAsync<List<string>>(
                    await response.Content.ReadAsStreamAsync());

                return data;
            }

            throw new Exception("oh no...");
        }
    }
}

API using the delegated API

The DelegatedUserApiCallsController is used to access a downstream API with uses delegated access tokens. This would be more the standard type of request in Azure. The UserApiClientService is used to access the API.

using System.Collections.Generic;
using System.Threading.Tasks;
using ApiWithMutlipleApis.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ApiWithMutlipleApis.Controllers
{
    [Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [ApiController]
    [Route("[controller]")]
    public class DelegatedUserApiCallsController : ControllerBase
    {
        private UserApiClientService _userApiClientService;

        public DelegatedUserApiCallsController(UserApiClientService userApiClientService)
        {
            _userApiClientService = userApiClientService;
        }

        [HttpGet]
        public async Task<IEnumerable<string>> Get()
        {
            return await _userApiClientService.GetApiDataAsync();
        }
    }
}

The UserApiClientService uses the ITokenAcquisition to get an access token for the access_as_user scope of the API. The access_as_user scope is added to the Azure App Registration for this API. The access token is requested using the On behalf flow (OBO). The access token are added to an in memory cache.

using Microsoft.Identity.Web;
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;

namespace ApiWithMutlipleApis.Services
{
    public class UserApiClientService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly ITokenAcquisition _tokenAcquisition;

        public UserApiClientService(
            ITokenAcquisition tokenAcquisition,
            IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
            _tokenAcquisition = tokenAcquisition;
        }

        public async Task<IEnumerable<string>> GetApiDataAsync()
        {

            var client = _clientFactory.CreateClient();

            var scopes = new List<string> { "api://b2a09168-54e2-4bc4-af92-a710a64ef1fa/access_as_user" };
            var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(scopes);

            client.BaseAddress = new Uri("https://localhost:44395");
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            var response = await client.GetAsync("ApiForUserData");
            if (response.IsSuccessStatusCode)
            {
                var data = await JsonSerializer.DeserializeAsync<List<string>>(
                    await response.Content.ReadAsStreamAsync());

                return data;
            }

            throw new Exception("oh no...");
        }
    }
}

API using the Graph API

The GraphApiCallsController API is used to access the Microsoft Graph API using the GraphApiClientService. This service uses a delegated access token to access the Microsoft Graph API delegated APIs which have been exposed in the Azure App Registration.

using System.Collections.Generic;
using System.Threading.Tasks;
using ApiWithMutlipleApis.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ApiWithMutlipleApis.Controllers
{
    [Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [ApiController]
    [Route("[controller]")]
    public class GraphApiCallsController : ControllerBase
    {
        private GraphApiClientService _graphApiClientService;

        public GraphApiCallsController(GraphApiClientService graphApiClientService)
        {
            _graphApiClientService = graphApiClientService;
        }

        [HttpGet]
        public async Task<IEnumerable<string>> Get()
        {
            var userData = await _graphApiClientService.GetGraphApiUser();
            return new List<string> { $"DisplayName: {userData.DisplayName}",
                $"GivenName: {userData.GivenName}", $"AboutMe: {userData.AboutMe}" };
        }
    }
}

The GraphApiClientService uses the ITokenAcquisition to get an access token for required Graph API scopes. Microsoft Graph API has also its own internal auth provider which also implements access token management like the Microsoft.Identity.Web. You could also use it. I use the ITokenAcquisition for token management like the previous two APIs for consistency.

using Microsoft.Graph;
using Microsoft.Identity.Web;
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;

namespace ApiWithMutlipleApis.Services
{
    public class GraphApiClientService
    {
        private readonly ITokenAcquisition _tokenAcquisition;
        private readonly IHttpClientFactory _clientFactory;

        public GraphApiClientService(ITokenAcquisition tokenAcquisition,
            IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
            _tokenAcquisition = tokenAcquisition;
        }

        public async Task<User> GetGraphApiUser()
        {
            var graphclient = await GetGraphClient(new string[] { "User.ReadBasic.All", "user.read" })
               .ConfigureAwait(false);

            return await graphclient.Me.Request().GetAsync().ConfigureAwait(false);
        }

        public async Task<string> GetGraphApiProfilePhoto()
        {
            try
            {
                var graphclient = await GetGraphClient(new string[] { "User.ReadBasic.All", "user.read" })
               .ConfigureAwait(false);

                var photo = string.Empty;
                // Get user photo
                using (var photoStream = await graphclient.Me.Photo
                    .Content.Request().GetAsync().ConfigureAwait(false))
                {
                    byte[] photoByte = ((MemoryStream)photoStream).ToArray();
                    photo = Convert.ToBase64String(photoByte);
                }

                return photo;
            }
            catch
            {
                return string.Empty;
            }   
        }

       
        private async Task<GraphServiceClient> GetGraphClient(string[] scopes)
        {
            var token = await _tokenAcquisition.GetAccessTokenForUserAsync(
             scopes).ConfigureAwait(false);

            var client = _clientFactory.CreateClient();
            client.BaseAddress = new Uri("https://graph.microsoft.com/beta");
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            GraphServiceClient graphClient = new GraphServiceClient(client)
            {
                AuthenticationProvider = new DelegateAuthenticationProvider(async (requestMessage) =>
                {
                    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
                })
            };

            graphClient.BaseUrl = "https://graph.microsoft.com/beta";
            return graphClient;
        }
    }
}

In the app.settings.json file, add the Azure AD App registration settings to match the the configuration for this application.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "your domain",
    "TenantId": "your tenant id",
    "ClientId": "your client id"
  }
}

Add the ClientSecret to the user secrets in your application. In a deployed version, you could add this to your Azure Key Vault.

{
  "AzureAd": {
    "ClientSecret": "your app registration secret"
  }
}

The Azure APIs which are used from this API must be exposed here. A client secret is also added to the App registration definition for the API project. Application scopes as well as delegated scopes are exposed here. This client secret is used to access the downstream APIs exposed here. You could also use a certificate instead of a client secret.

The Application API

The application API is very simple to setup. This uses the standard Microsoft.Identity.Web settings for an API. The authorization middleware checks that the azpacr claim has a value of 1 to make sure only a token which used a secret to get the access token can access this API. If using certificates, the value would be 2. The azp is used to validate that the correct Web API requested the access token.

public void ConfigureServices(IServiceCollection services)
{
	JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
	IdentityModelEventSource.ShowPII = true;
	JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

	services.AddSingleton<IAuthorizationHandler, HasServiceApiRoleHandler>();

	services.AddMicrosoftIdentityWebApiAuthentication(Configuration);

	services.AddControllers();

	services.AddAuthorization(options =>
	{
		options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy =>
		{
			validateAccessTokenPolicy.Requirements.Add(new HasServiceApiRoleRequirement());
			
			// Validate id of application for which the token was created
			// In this case the UI application 
			validateAccessTokenPolicy.RequireClaim("azp", "2b50a014-f353-4c10-aace-024f19a55569");

			// only allow tokens which used "Private key JWT Client authentication"
			// // https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
			// Indicates how the client was authenticated. For a public client, the value is "0". 
			// If client ID and client secret are used, the value is "1". 
			// If a client certificate was used for authentication, the value is "2".
			validateAccessTokenPolicy.RequireClaim("azpacr", "1");
		});
	});

	// add swagger ...

}

The AuthorizationHandler is used to fulfil the requirement HasServiceApiRoleRequirement which the API uses in its policy to authorize the access token. The authorization middlerware validates that the service-api scope claim is present in the access token.

using Microsoft.AspNetCore.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace ServiceApi
{
    public class HasServiceApiRoleHandler : AuthorizationHandler<HasServiceApiRoleRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasServiceApiRoleRequirement requirement)
        {
            if (context == null)
                throw new ArgumentNullException(nameof(context));
            if (requirement == null)
                throw new ArgumentNullException(nameof(requirement));

            var roleClaims = context.User.Claims.Where(t => t.Type == "roles");

            if (roleClaims != null && HasServiceApiRole(roleClaims))
            {
                context.Succeed(requirement);
            }

            return Task.CompletedTask;
        }

        private bool HasServiceApiRole(IEnumerable<Claim> roleClaims)
        {
            // we could also validate the "access_as_application" scope
            foreach(var role in roleClaims)
            {
                if("service-api" == role.Value)
                {
                    return true;
                }
            }

            return false;
        }
    }
}

The API uses the Policy ValidateAccessTokenPolicy to authorize the access token.

using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ServiceApi.Controllers
{
    [Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    [ApiController]
    [Route("[controller]")]
    public class ApiForServiceDataController : ControllerBase
    {
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new List<string> { "app-app Service API data 1", "service API data 2" };
        }
    }
}

User API for the delegated access

The API which uses the delegated access token which the frontend API got by using the OBO flow, is implemented like in this blog: Implement a Web APP and an ASP.NET Core Secure API using Azure AD which delegates to a second API. Again the azpacr claim is used to check that a client secret was used to get the access token requesting the API.

services.AddAuthorization(options =>
{
	options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy =>
	{
		validateAccessTokenPolicy.RequireClaim("azp", "2b50a014-f353-4c10-aace-024f19a55569");

		validateAccessTokenPolicy.RequireClaim("azpacr", "1");
	});
});

The Angular UI

Code: Angular CLI project

The UI part of the solution is implemented in Angular. The Angular SPA application which runs completely in the browser of the client needs to authenticate and store its tokens somewhere in the browser, usually in the session state. The Angular SPA cannot keep a secret, it is a public client. To authenticate, the application uses an Azure AD public client created using an Azure App Registration. The Azure App Registration is setup to support the OIDC Connect code flow with PKCE and uses a delegated access token for our backend. It has only access to the top API.

Only the single access token is moved around and stored in the public zone. This access token should have a short lifespan and be renewed or refreshed. There are two ways of renewing or refreshing access tokens in a SPA. One way is to silent renew in an Iframe but this is getting blocked now by Safari and Brave and soon other browsers. The second way is to use refresh tokens. This can lead to other security problems, but the risks can be reduced by using best practices like one-time usage and so on. Another way of reducing the risk would be to use the revocation endpoint to invalidate the refresh token, access token but this is not supported yet by Azure AD. Using reference tokens would also help but this is also not supported by Azure AD. For this reason, as little as possible should be implemented in the unsecure browser. Using multiple access tokens in your SPA is not a good idea. To get a second access token, a full UI authenticate is required (silent or in a popup, app redirect) and then the second access token would also be public. We want as few as possible public security parts.

The npm package angular-auth-oidc-client can be used to implement the security flows for the Angular app. Other Angular npm packages also work fine, you can choose the one you like or know best. Add the security lib configuration to the app.module which matches the Azure App Registration for this APP.

We will use an Auth Guard to protect the routes which must be protected. You MUST leave the default route and maybe an error or info route unprotected due to the constraints of the Open ID Connect code flow. The redirect steps of the flow CANNOT be protected with the auth guard. The auth guard is added to the routes.


export function configureAuth(oidcConfigService: OidcConfigService) {
  return () =>
    oidcConfigService.withConfig({
            stsServer: 'https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/v2.0',
            authWellknownEndpoint: 'https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/v2.0',
            redirectUrl: window.location.origin,
            clientId: 'ad6b0351-92b4-4ee9-ac8d-3e76e5fd1c67',
            scope: 'openid profile email api://2b50a014-f353-4c10-aace-024f19a55569/access_as_user offline_access',
            responseType: 'code',
            silentRenew: true,
            useRefreshToken: true,
            maxIdTokenIatOffsetAllowedInSeconds: 600,
            issValidationOff: false,
            autoUserinfo: false,
            logLevel: LogLevel.Debug
    });
}

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NavMenuComponent,
    UnauthorizedComponent,
    DirectApiCallComponent,
    GraphApiCallComponent,
    ApplicationApiCallComponent,
    DelegatedApiCallComponent
  ],
  imports: [
    BrowserModule,
    RouterModule.forRoot([
    { path: '', redirectTo: 'home', pathMatch: 'full' },
    { path: 'home', component: HomeComponent },
    { path: 'directApiCall', component: DirectApiCallComponent, canActivate: [AuthorizationGuard] },
    { path: 'graphApiCall', component: GraphApiCallComponent, canActivate: [AuthorizationGuard] },
    { path: 'applicationApiCall', component: ApplicationApiCallComponent, canActivate: [AuthorizationGuard] },
    { path: 'delegatedApiCall', component: DelegatedApiCallComponent, canActivate: [AuthorizationGuard] },
    { path: 'unauthorized', component: UnauthorizedComponent },
  ], { relativeLinkResolution: 'legacy' }),
    AuthModule.forRoot(),
    HttpClientModule,
  ],
  providers: [
    OidcConfigService,
    {
      provide: APP_INITIALIZER,
      useFactory: configureAuth,
      deps: [OidcConfigService],
      multi: true,
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true,
    },
    AuthorizationGuard
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

The AuthorizationGuard is implemented using the CanActivate. The oidcSecurityService.isAuthenticated$ pipe can be used to check.

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { OidcSecurityService } from 'angular-auth-oidc-client';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class AuthorizationGuard implements CanActivate {
    constructor(private oidcSecurityService: OidcSecurityService, private router: Router) {}

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
        return this.oidcSecurityService.isAuthenticated$.pipe(
            map((isAuthorized: boolean) => {
                console.log('AuthorizationGuard, canActivate isAuthorized: ' + isAuthorized);

                if (!isAuthorized) {
                    this.router.navigate(['/unauthorized']);
                    return false;
                }

                return true;
            })
        );
    }
}

The angular-auth-oidc-client this.authService.checkAuth() method is called once in the app.component class. This is part of the default route. When the redirect from the security flow calls back or the app is refreshed in the browser, the correct state will be initialized for the APP.

import { Component, OnInit } from '@angular/core';
import { AuthService } from './auth.service';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
})
export class AppComponent implements OnInit {
  constructor(public authService: AuthService) {}

  ngOnInit() {
    this.authService
      .checkAuth()
      .subscribe((isAuthenticated) =>
        console.log('app authenticated', isAuthenticated)
      );
  }
}

An AuthInterceptor is used to add the access token to the outgoing HTTP calls. The HttpInterceptor is for ALL HTTP requests, so care needs to be taken that the access token is only sent when making an HTTP request to one of the APIs for which the access token was intended for.

import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private secureRoutes = ['https://localhost:44390'];

  constructor(private authService: AuthService) {}

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ) {
    if (!this.secureRoutes.find((x) => request.url.startsWith(x))) {
      return next.handle(request);
    }

    const token = this.authService.token;

    if (!token) {
      return next.handle(request);
    }

    request = request.clone({
      headers: request.headers.set('Authorization', 'Bearer ' + token),
    });

    return next.handle(request);
  }
}

The DirectApiCallComponent implements the view uses the HttpClient to get the secure data from the API protected with Azure AD.

import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
import { AuthService } from '../auth.service';

@Component({
  selector: 'app-direct-api-call',
  templateUrl: 'directApiCall.component.html',
})
export class DirectApiCallComponent implements OnInit {
  userData$: Observable<any>;
  dataFromAzureProtectedApi$: Observable<any>;
  isAuthenticated$: Observable<boolean>;
  httpRequestRunning = false;

  constructor(
    private authService: AuthService,
    private httpClient: HttpClient
  ) {}

  ngOnInit() {
    this.userData$ = this.authService.userData$;
    this.isAuthenticated$ = this.authService.signedIn$;
  }

  callApi() {
    this.httpRequestRunning = true;
    this.dataFromAzureProtectedApi$ = this.httpClient
      .get('https://localhost:44390/DirectApi')
      .pipe(finalize(() => (this.httpRequestRunning = false)));
  }
}

The data is displayed in the template for the Angular component.


<div *ngIf="isAuthenticated$ | async as isAuthenticated">

  <button class="btn btn-primary" type="button" (click)="callApi()" [disabled]="httpRequestRunning">
    <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" [hidden]="!httpRequestRunning" ></span>
    Request Data
  </button>

  <br/><br/>

  Is Authenticated: {{ isAuthenticated }}

  <br/><br/>

  <div class="card">
    <div class="card-header">Data from direct API</div>
    <div class="card-body">
      <pre>{{ dataFromAzureProtectedApi$ | async | json }}</pre>
    </div>
  </div>

</div>


Now everything is working and the applications can be started and run.

By using ASP.NET Core as a gateway for further APIs or services, it is extremely easy to add further things like Databases, Storage, Azure Service Bus, IoT solutions, or any type of Azure / Cloud service as all have uncomplicated solutions for ASP.NET Core.

The solution could then be further improved by adding network security. A simple VNET could be created and the protected APIs can be made only available inside the VNET. This costs nothing and is simple to implement. You could use Cloudflare as a firewall or Azure Firewall.

In a follow up post to this, I plan to implement authorization using roles and groups.

Links

https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/howto-saml-token-encryption

Authentication and the Azure SDK

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-credential-flows

https://tools.ietf.org/html/rfc7523

https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication

https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-Assertions

https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow

https://github.com/AzureAD/microsoft-identity-web/wiki/Using-certificates#describing-client-certificates-to-use-by-configuration

API Security with OAuth2 and OpenID Connect in Depth with Kevin Dockx, August 2020

https://www.scottbrady91.com/OAuth/Removing-Shared-Secrets-for-OAuth-Client-Authentication

https://github.com/KevinDockx/ApiSecurityInDepth

https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki

https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-verification-scope-app-roles

8 comments

  1. […] Using multiple APIs in Angular and ASP.NET Core with Azure AD authentication (Damien Bowden) […]

    1. cjh398@nau.edu · · Reply

      Hi Damien! Love the article. This is exactly what I’ve been looking for. The only issue I’m having right now is being able to understand exactly what the configurations should be in the Azure Portal. Is it possible to see exactly what your configurations are for the Azure Portal? My guess, is that you just create a new App Registration for each Component in your first diagram, then either add a permission to access the specified component or Expose an API. Is this all that is required? Would be helpful (for a noobie like me) if there were a few more pictures/section(s) depicting exactly what that should look like!

      Thank you 🙂

  2. Knowing the OBO limitation with Azure AD B2C, would this somehow be possible with Azure AD B2C?

    1. B2C… maybe IdentityServer4, OpenIddict, Auth0 or Keyclock might make more sense. You would need to use the client credentials flow, if B2C is a must, which would mean that these APIs are admin APIs and require a secret. The user stuff would be passed as a parameter.

      Greetings Damien

  3. […] Using multiple APIs in Angular and ASP.NET Core with Azure AD authentication – Damien Bowden […]

  4. This is a great article. Thank you!

    You mention “This is very like the backend for frontend application architecture (BFF) which is more secure than this setup because the security for the UI is also implemented in the trusted backend for the UI, ie (no access tokens in the browser storage, no refresh/renew in the browser)”

    I’ve been looking for a sample or ref app that implements this with an Angular UI and backend for auth and and an API. Do you know of any samples on the web that demonstrate this?

  5. For anyone: make sure you have a V2.0 access token, or the azp claim is not available. Also, incremental consent doesn’t work for V1.0 tokens!

  6. While clicking “request data” on the localhost:4200(angular) the console shows “403:Forbidden” error while launching “localhost:44390/graphApiCalls”(ASP.Net). Please help

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

<span>%d</span> bloggers like this: