TL;DR If you want to skip the blurbs, just go straight to steps to reproduce or checkout the code on GitHub.

Authentication in SPAs using OAuth 2 authorisation code flow with MSAL.js 2.x and consuming a .NET Core 3.1 API

This post provides the steps to secure your ASP.NET core API using OAuth2, authenticating a Single Page App against Azure AD using Microsoft Authentication Library for JavaScript (MSAL.js) 2.x and the authorisation code flow with PKCE and calling the API with an access token.

MSAL 1.x, Implicit Flow and Security Considerations

Authentication in single page apps using v1.x involves the use of the implicit flow. The OAuth security best practices have been updated and advise against using the implicit flow due to a few vulnerabilities most of which result from the fact that the access token is part of the return URL (and therefore could be intercepted before arriving in the app it is intended for).

Instead, the authorisation code flow is recommended.

MSAL 2, Authorisation Code Flow and Azure AD

Using MSAL.js 2.x, SPAs can now authenticate against Azure AD using the authorisation code flow (Azure AD B2C was not supported at the time this blog was written). Let’s get started…

Steps to reproduce

  • Prerequisites
    • An app registration in AzureAD, set up for authentication with the platform ‘Single-page Application’ as described here.
  • What I used:
    • Windows 10
    • Visual Studio 2019
    • dotnet sdk v3.1.401
  • Commit 1: Creating an ASP.NET Core 3.1 web app with React
    • File->New project
    • ASP.NET Core Web Application (name it i.e. “Msal2”) -> Create
    • Choose .NET Core 3.1, React.js (the auth code won’t be React-specific)
    • Choose ‘No Authentication’
    • Create (the app will be scaffolded)
  • Commit 2: Let’s modify Home.js to fetch and display today’s weather forecast. Authetication is not yet enabled in the API so we can get away with an unauthenticated call - here is the link to the complete Home.js file for this step. The App will now display tomorrow’s weather.
    //...
      fetchWeather() {
        fetch("/weatherforecast").then(response => {
          response.json().then(result => {
            this.setState(result);
          })
        });
      }
    //...
    
  • Commit 3: Secure the API by adding middleware to handle OpenID Connect Bearer token.
    • Install nuget package Microsoft.AspNetCore.Authentication.JwtBearer
    • Add the following lines to the beginning of the Startup.ConfigureServices method:

      services.AddAuthentication(options =>
      {
        options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
      })
      .AddJwtBearer(jwtOptions =>
      {
        var instance = Configuration["AzureAd:Instance"];
        var domain = Configuration["AzureAd:Domain"];
      
        jwtOptions.Authority = $"{instance}/{domain}/v2.0/";
        jwtOptions.Audience = Configuration["AzureAd:ClientId"];
      });
      
    • Add the following lines to the Startup.Configure method to configure the pipeline to use the auth middleware:
      app.UseAuthentication();
      app.UseAuthorization();
      
    • Open appsettings.json and replace the values for TenantId, ClientId and Domain with valid values from your Azure AD app registration.
    • Finally, we add the [Authorize] attribute to the weather forecast endpoint.
    • Now the fetch('/weatherforecast') call will result in a 401 (Unauthorized) response due to a missing valid JWT.
  • Commit 4: Finally, we add the package @azure/msal-browser to our client code and acquire an access token from Azure AD that we then use to call the API.
    • Install msal-browser by typing npm install --save @azure/msal-browser
    • The call to myMSALObj.loginRedirect(loginRequest) will request an authorisation code (response_type: code) which then will be used to request an access token.
    • We then use that access token to call the API:
      // ...
      auth.getToken().then(accessToken => {
        fetch("/weatherforecast", { headers: { Authorization: `Bearer ${accessToken}` } }).then(response => {
          response.json().then(result => {
            this.setState(result);
          })
        });
      });
      // ...
      

Conclusion

MSAL.js 2.x supports the authorisation code flow with PKCE as opposed to MSAL.js 1.x (and ADAL.js), which uses the implicit flow. With the updates OAuth security best practices advising against using the implicit flow it’s a good idea to choose MSAL.js 2.x over version 1.x for new projects and update existing projects to use the most recent version.