Using multiple APIs in Angular and ASP.NET Core with Microsoft Entra ID 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/MicrosoftEntraIDAuthMicrosoftIdentityWeb

Posts in this series

History

  • 2023-11-29 Updated to .NET 8

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 Microsoft Entra ID.

The APIs

The API which was created for the UI uses Microsoft.Identity.Web to implement the Microsoft Entra ID 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 Microsoft Entra ID 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 static WebApplication ConfigureServices(this WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;

services.AddHttpClient();
services.AddOptions();

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(builder.Configuration)
	 .EnableTokenAcquisitionToCallDownstreamApi()
	 .AddMicrosoftGraph()
	 .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");
	});
});

services.AddSwaggerGen(c =>
{
	// add JWT Authentication
	var securityScheme = new OpenApiSecurityScheme
	{
		Name = "JWT Authentication",
		Description = "Enter JWT Bearer token **_only_**",
		In = ParameterLocation.Header,
		Type = SecuritySchemeType.Http,
		Scheme = "bearer", // must be lower case
		BearerFormat = "JWT",
		Reference = new OpenApiReference
		{
			Id = JwtBearerDefaults.AuthenticationScheme,
			Type = ReferenceType.SecurityScheme
		}
	};
	c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);
	c.AddSecurityRequirement(new OpenApiSecurityRequirement
	{
		{securityScheme, new string[] { }}
	});

	c.SwaggerDoc("v1", new OpenApiInfo { Title = "ApiWithMutlipleApis", Version = "v1" });
});

return builder.Build();
}

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 Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;

namespace ApiWithMutlipleApis.Controllers;

[Authorize(Policy = "ValidateAccessTokenPolicy",
    AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AuthorizeForScopes(Scopes = new string[] { "api://2b50a014-f353-4c10-aace-024f19a55569/access_as_user" })]
[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 ApiWithMutlipleApis.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;

namespace ApiWithMutlipleApis.Controllers;

[Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AuthorizeForScopes(Scopes = new string[] { "api://2b50a014-f353-4c10-aace-024f19a55569/access_as_user" })]
[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.Net.Http.Headers;
using System.Text.Json;

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)
            .ConfigureAwait(false);

        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());

            if(data != null)
                return data;

            return Array.Empty<string>();
        }

        throw new ApplicationException("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 Microsoft Entra ID. The UserApiClientService is used to access the API.

using ApiWithMutlipleApis.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;

namespace ApiWithMutlipleApis.Controllers;

[Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AuthorizeForScopes(Scopes = new string[] { "api://2b50a014-f353-4c10-aace-024f19a55569/access_as_user" })]
[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.Net.Http.Headers;
using System.Text.Json;

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 stream = await response.Content.ReadAsStreamAsync();

            var data = await JsonSerializer.DeserializeAsync<List<string>>(stream);

            if (data != null)
                return data;

            return Array.Empty<string>();
        }

        throw new ApplicationException("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 ApiWithMutlipleApis.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Web;

namespace ApiWithMutlipleApis.Controllers;

[Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[AuthorizeForScopes(Scopes = new string[] { "User.ReadBasic.All", "user.read" })]
[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 ImageMagick;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Identity.Web;
using Microsoft.IdentityModel.Tokens;

namespace ApiWithMutlipleApis.Services;

public class GraphApiClientService
{
    private readonly GraphServiceClient _graphServiceClient;

    public GraphApiClientService(GraphServiceClient graphServiceClient)
    {
        _graphServiceClient = graphServiceClient;
    }

    public async Task<User?> GetGraphApiUser()
    {
        var user = await _graphServiceClient.Me
            .GetAsync(b => b.Options.WithScopes("User.ReadBasic.All", "user.read"));

        return user;
    }

    public async Task<string> GetGraphApiProfilePhoto(string oid)
    {
        var photo = string.Empty;
        byte[] photoByte;

        using (var photoStream = await _graphServiceClient.Users[oid]
            .Photo
            .Content
            .GetAsync(b => b.Options.WithScopes("User.ReadBasic.All", "user.read")))
        {
            photoByte = ((MemoryStream)photoStream!).ToArray();
        }

        using var imageFromFile = new MagickImage(photoByte);
        // Sets the output format to jpeg
        imageFromFile.Format = MagickFormat.Jpeg;
        var size = new MagickGeometry(400, 400);

        // This will resize the image to a fixed size without maintaining the aspect ratio.
        // Normally an image will be resized to fit inside the specified size.
        //size.IgnoreAspectRatio = true;

        imageFromFile.Resize(size);

        // Create byte array that contains a jpeg file
        var data = imageFromFile.ToByteArray();
        photo = Base64UrlEncoder.Encode(data);

        return photo;
    }
}

In the app.settings.json file, add the Microsoft Entra ID App registration settings to match the the configuration for this application.

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "damienbodhotmail.onmicrosoft.com",
    "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
    "ClientId": "2b50a014-f353-4c10-aace-024f19a55569"
  },

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 Microsoft Entra ID 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 static WebApplication ConfigureServices(this WebApplicationBuilder builder)
{
var services = builder.Services;
var configuration = builder.Configuration;

services.AddSingleton<IAuthorizationHandler, HasServiceApiRoleHandler>();

services.AddMicrosoftIdentityWebApiAuthentication(builder.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 CC client 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");
	});
});

services.AddSwaggerGen(c =>
{
	c.EnableAnnotations();

	// add JWT Authentication
	var securityScheme = new OpenApiSecurityScheme
	{
		Name = "JWT Authentication",
		Description = "Enter JWT Bearer token **_only_**",
		In = ParameterLocation.Header,
		Type = SecuritySchemeType.Http,
		Scheme = "bearer", // must be lower case
		BearerFormat = "JWT",
		Reference = new OpenApiReference
		{
			Id = JwtBearerDefaults.AuthenticationScheme,
			Type = ReferenceType.SecurityScheme
		}
	};
	c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);
	c.AddSecurityRequirement(new OpenApiSecurityRequirement
	{
		{securityScheme, Array.Empty<string>()}
	});

	c.SwaggerDoc("v1", new OpenApiInfo
	{
		Title = "Service API One",
		Version = "v1",
		Description = "Service API One",
		Contact = new OpenApiContact
		{
			Name = "damienbod",
			Email = string.Empty,
			Url = new Uri("https://damienbod.com/"),
		},
	});
});

return builder.Build();
}

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 static 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.Http;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;

namespace ServiceApi.Controllers;

[Authorize(Policy = "ValidateAccessTokenPolicy", AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
[Route("[controller]")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[Produces("application/json")]
[SwaggerTag("Service API for demo service data")]
public class ApiForServiceDataController : ControllerBase
{
    [HttpGet]
    [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable<string>))]
    [SwaggerOperation(OperationId = "Get", Summary = "Gets service data")]
    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 Microsoft Entra ID 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.

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy =>
    {
        // 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");
    });
});

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 AMicrosoft Entra ID 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 Microsoft Entra ID. Using reference tokens would also help but this is also not supported by Microsoft Entra ID. 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.

See the auth implementation in the Angular code.

Note: The BFF security architecture should now be used when implementing web security.

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

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.

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.

The DirectApiCallComponent implements the view uses the HttpClient to get the secure data from the API protected with Microsoft Entra ID.

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 to damienbod Cancel reply

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