OAuth 2.0 authorization code flow with a React SPA, ASP.NET Core Web API, RBAC roles, and MSAL

Earlier this year the Microsoft Identity Platform team shared new guidance that recommends using the OAuth 2.0 Authorization Code flow for browser based web applications. The reason for this is that new browser security changes are going to cause problems for the commonly used implicit grant flow pattern.

Although I found plenty of great code samples and quickstart material for using the authorization code flow with graph API, it took me a while to figure out how to use it against an ASP.NET Core Web API. The goal of this post is to provide an end-to-end setup guide with source code for the protected web API resource scenario that uses RBAC roles.

Complete source code

If you want to jump straight to the complete source code: https://github.com/keithbabinec/SampleMsalAuthorizationCodeFlow

End-to-end setup guide

Part 1: Azure AD configuration

Scripted solutions are linked when available. Some steps are manual due lack of Azure tooling support for the operation or information is highly specific to your own environment.

1. Create security groups in Azure AD that will be used for your sample application.

I would recommend creating one group per role. For example if I want to have application roles defined for standard users and admin users, I would make two new AAD security groups called MyAppUsersGroup and MyAppAdministratorsGroup.

Script solution: new-rbac-groups.ps1.

2. Assign at least one user to each application role security group created in step 1.

Manual step / no script.

3. Create and configure the Azure AD app registration.

  • Create a new Azure AD app registration with the following options:
    • Supported account types: Accounts in this organizational directory only (Single tenant).
    • Redirect URI:
  • Define custom app roles in your application manifest (instructions). These roles should reflect the same roles you created groups for in step 1.
  • Under the Expose an API section of the app registration:
    • Set the Application ID URI (using default format: api://<appid>)
    • Define one application scope (name doesn’t matter, but make it so both admins and users can consent).

Script solution: new-app-registration.ps1.

4. Create and configure the service principal (enterprise application).

  • From the app registration’s Overview page, click on the link below menu item Managed application in local directory.
  • From the enterprise application’s Properties page, set the User assignment required property to Yes. This ensures that only pre-authorized users can sign in.
    • Manual step / no script.
  • From the enterprise application’s Users and groups page, assign the AAD groups from step 1 to the corresponding application roles defined in step 3.
    • Manual step / no script.

Part 2: ASP.NET Web API setup

1. Create the dotnet core project.

Create a new ASP.NET Core 3.x Web API project. If you are creating the project from the Visual Studio template, you don’t need to specify any authentication mechanisms. Just select the Web API template type. You will need to add a nuget package reference to Microsoft.Identity.Web.

2. Startup.cs configuration.

Under the ConfigureServices() method of Startup.cs, you will need to configure both Authentication and Authorization services:

// add authentication support (bearer token validation).
// configuration values are pulled from "AzureAD" section of app settings config.
// this uses Microsoft Identity platform (AAD v2.0).
services.AddMicrosoftIdentityWebApiAuthentication(Configuration, "AzureAd");

// add authorization support.
services.AddAuthorization();

Under the Configure() method of Startup.cs, you will need to add those services to the request pipeline:

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

3. Controller configurations.

Add an Authorize attribute for each controller or route that you want to enable authorization for. In the roles string, specify the app manifest defined role that should have access to the route. Example:

[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
   [HttpGet()]
   [Route("standard")]
   [Authorize(Roles = "MyAppUsersRole")]
   public async Task<ActionResult<string>> StandardRoleGet()
   {
       // implementation code
   }
}

4. App settings configuration

Finally we want to add an AzureAd section to the appsettings.json (and any environment specific configs) that provides tenant authority information and app registration details. Example:

"AzureAd": {
  "Instance": "https://login.windows.net",
  "ClientId": "-- use client ID of app registration --",
  "TenantId": "-- use Azure tenant (directory) ID --",
  "Audience": "api://[-- use client ID of app registration --]"
}

Part 3: React client setup

1. Create the react project and add dependencies.

I like to bootstrap things with create-react-app. Here is a command to generate a React-Typescript project:

npx create-react-app web-client --template typescript

At a minimum you will need the MSAL client. An http client like Axios is also helpful since we are making web calls.

npm install @azure/msal-browser
npm install axios

2. Configure MSAL objects and call MSAL APIs.

The following is a high-level overview. For full context, view the sample application source that provides wrapper classes around these core ideas.

  • Create a wrapper class that holds your MSAL PublicClientApplicationObject and the AccountInfo of the currently logged in user.
    • The public client app object constructor will require a config object where you can specify your Azure AD client app ID, the tenant URI, and the redirect URL (usually app home/index).
  • Have a UI button that calls <msal-app>.loginRedirect().
    • The user is immediately redirected to the Microsoft Identity Platform so they can sign in.
    • In the login request payload, specify the exact scope created in your app registration.
      • Example: “api://<app-client-id-guid>/Scope.Name”
  • In your main index.(js|ts|tsx) file, ensure that you call <msal-app>.handleRedirectPromise().
    • This is invoked on all page loads to handle either (1) a redirect back from the identity platform, or (2) pull cached user details.

3. Call a protected web API resource.

  • Before each call to a protected web API resource, call <msal-app>.acquireTokenSilent() to fetch a token.
    • If the current access token is still valid it will be used, otherwise MSAL will attempt to fetch a new one silently.
    • The scope specified in the payload must match the scope created in your app registration.
  • Pass in the access token from MSAL into the authorization header for API calls to the protected web API resource.

Q&A

1. Why does my call to the /Authorize endpoint fail with an error message saying that the client_assertion or client_secret is missing?

A: Authorization code flow only works if your app registration’s redirect URI is of type “SPA”. If it is incorrectly set to type “Web” (which is the default), then the /Authorize call will fail.

2. Why can’t I assign users or groups to my app registration roles?

A: Make sure that your app registration wasn’t registered as a public client, because public clients do not support RBAC. Also note that assigning Groups to App Roles is only available with Azure AD Premium. If you have standard Azure AD then you will need to assign users directly to the app roles.

3. I asked for multiple scopes in a token request but only received some of the scopes, why is that?

A: Each call to MSAL’s loginRedirect or acquireToken operations must only specify scopes for a single resource. If you specify scopes for multiple resources (for example one scope for Graph and one for your ASP.NET web API), only the scopes from the first resource are returned. More information here.

4. Why did the example instructions create one App registration scope if we had multiple app roles defined?

Scopes are linked with the app registration itself and not with specific user roles. All authorized users (regardless of role) are given the application scopes defined in the app registration. As a result, our API is concerned with making sure that users have the correct client/audience and that they are authorized via roles.

5. Why is there only one app registration instead of two (split for client app and web api app)?

This flow should work fine in either a single-app or multi-app registration setup. I choose single app here for simplicity because:

  • I only have one web client now and I don’t plan on adding multiple clients for this same API later.
  • The API itself isn’t acting as a client to another resource.

6. How can I examine the contents of a token returned from the identity platform?

https://jwt.ms is a great tool for decoding the token to examine claims for debugging purposes.

Screenshots

App before signing in
Logging in at the Microsoft Identity Platform
Redirect after login
Result after calling an endpoint
Fiddler trace showing successful API call with bearer token.

7 thoughts on “OAuth 2.0 authorization code flow with a React SPA, ASP.NET Core Web API, RBAC roles, and MSAL

  1. srini October 10, 2020 / 6:17 am

    Very nice article. How do we handle CORS issue when the call originates from https://localhost:3000 (react) to asp.net core webapi running under IIS 10. I seem to have the problem even after enabling CORS in IIS and allowing specific sites

    Like

    • keithbabinec October 10, 2020 / 5:33 pm

      Generally speaking I would verify the following:

      1. You have added a valid CORS config to the services.AddCors() method in Startup.ConfigureServices()

      2. You are actually using the CORS config by calling app.UseCors() in Startup.Configure().

      3. The order you the call the Use() methods in Startup.Configure() is correct. Adding things out of order may cause them not to work.

      If none of those things are the issue, I would recommend opening up a new question on stackoverflow where you can post your code samples.

      Like

  2. Mohammad Shadmehr October 21, 2020 / 4:46 pm

    I have setup AD as per the steps above. However, at the moment only the client gets authenticated and I am getting error (code 500) when trying to call the “AdminRoleGet” end point. I have enabled the “IdentityModelEventSource.ShowPII” for more info and I am getting the following:
    “System.InvalidOperationException: IDX20803: Unable to obtain configuration from: ‘https://login.windows.net/– use Azure tenant….”
    “HttpResponseMessage.Content: ‘{“error”:”invalid_tenant”,”error_description”:”AADSTS90002: Tenant ‘– use azure tenant (directory) id –‘ not found. This may happen if there are no active subscriptions for the tenant…”

    Below is my appSettings.json:
    “AzureAd”: {
    “Instance”: “https://login.windows.net”,
    “ClientId”: “b29e4a17-26d8-48d2-a904-80gt547bcab4”,
    “TenantId”: “ed1f9778-fef2-4985-9c5f-d4detyyy349a”,
    “Audience”: “api://b29e4a17-26d8-48d2-a904-80gt547bcab4”
    }

    and below is the values I set in the React evironment:
    REACT_APP_MSAL_CLIENT_SCOPE = “https://sab2cp.onmicrosoft.com/b29e4a17-26d8-48d2-a904-80gt547bcab4/Admin.Authorization”
    REACT_APP_MSAL_TENANT_AUTHORITY_URI = “https://login.windows.net/ed1f9778-fef2-4985-9c5f-d4detyyy349a”
    REACT_APP_MSAL_CLIENT_ID = “b29e4a17-26d8-48d2-a904-80gt547bcab4”

    BTW, I had to change the value for REACT_APP_MSAL_CLIENT_SCOPE from “api://{clientId}” to the one above as it is what I can read in my Azure scope. Have already changed the “Audience” from “api://…” to my scope, but still same error!

    Any help is much appreciated

    Like

    • keithbabinec October 22, 2020 / 1:27 pm

      The failure you are seeing is because its trying to query tenant details for the unsubstituted placeholder values. The tenant configuration that you applied in appsettings.json would also needs to be applied to appsettings.development.json. Visual Studio launching the Web API in debug mode uses the development config file, not the regular config file (for the deployed service).

      Like

      • Mohammad Shadmehr November 10, 2020 / 4:41 pm

        thanks for your input. all is working fine. Just a quick question on the client side configuration. I can see a comment in the env.development as “IMPORTANT: do not store secret values in this file.”. Where should I exactly store the clientId, tenantId and other bits for the client to be secure?

        Like

    • keithbabinec November 10, 2020 / 8:44 pm

      Regarding the client config and secrets: your Azure AD tenant ID and client ID are not actually considered secrets. These are public details that an unauthenticated client requires before they can even log in– otherwise they wouldn’t know which AAD tenant/client to authenticate against. You can also see these public details when you redirect to the Microsoft identity platform. The tenant ID and the client ID are directly in the URL. These values are certainly safe to check into source control and store in the .env files.

      A better example of an actual secret that you should NOT store in the .env files would be something like an API key, or an account password.

      Like

Leave a Reply to keithbabinec Cancel 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