Search code examples
c#azure-active-directoryjwtasp.net-core-webapiazure-ad-msal

.NET API can't recognize/authorize Azure AD user through authorized client's JWT


I am trying to authorize Azure Active Directory users logged into a NextJS client application (using msal-react) through my .NET 8 API so they can use [Authorize] endpoints. However, upon sending a client JWT to my API, I receive a 401 response and my API logs a DenyAnonymousAuthorizationRequirement.

I have registered both applications through Azure's App registrations. In my API registration, I have exposed my API to authorize my client app through a single scope, access_as_user. I have confirmed this in my client's registration as a Delegated permission. Azure AD App registration screen showing the Expose an API page

API permissions for my API app

API permissions for my client app

Showing I'm granting access and ID tokens

In my .NET API, I'm setting my AzureAd configuration in appsettings.json

"AzureAd": {
  "Instance": "https://login.microsoftonline.com/",
  "Domain": "qualified.domain.name",
  "TenantId": "API-TENANT-ID",
  "ClientId": "API-CLIENT-ID",
  "Scopes": "access_as_user",
  "CallbackPath": "/signin-oidc"
},

I then add authentication to my services in my Program.cs

...
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(config.GetSection("AzureAd"));
...
app.UseAuthorization();
app.UseAuthentication();
...

Finally, I've set up a simple controller with a single [Authorize] endpoint.

[Controller]
[Route("api")]
public class GeneralController : Controller
{
    [Authorize]
    [HttpGet("get")]
    public Task<IActionResult> Test()
    {
        return Task.FromResult<IActionResult>(Ok());
    }
}

In my client application, I retrieve my token after a user logs in with Azure AD.

const { instance, accounts } = useMsal();
...
instance.acquireTokenSilent({
   scopes: [
     "api://API-CLIENT-ID/access_as_user",
   ],
     account: account,
})

I'm able to log the token retrieved by the client app succesfully and, when using https://jwt.io/, I can decode the JWT to find the following relevant properties:

{
  "aud": "API-CLIENT-ID",
  "iss": "https://login.microsoftonline.com/CLIENT-TENANT-ID/v2.0",
  "azp": "CLIENT-CLIENT-ID",
  "name": "LOGGED-IN-USER-NAME",
  "scp": "access_as_user",
  "tid": "CLIENT-TENANT-ID",
  "ver": "2.0"
}

However, when I include this token in a request's Authorization header to the general endpoint I set up, I receive a 401 response and the following logs from my API:

info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
      Authorization failed. These requirements were not met:
      DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
      AuthenticationScheme: Bearer was challenged.

Solution

  • General speaking, by default, the web API protect by AAD requires a token which aud claim value should match the value we defined in the appsettings.json, so that it's a valid token. If we didn't define Audience property in appsetting, the default value for it is the ClientId in appsetting.

    According to your screenshot, you exposed API api://my_api_client_id/access_as_user in your My API app. And you already added this API permission into My client, that should be ok. Therefore, in your client app, you should have configurations for MSAL that all use My Client, and my_api_client_id should only appear once when you define the scope you used to generate access token. Please make sure that you can obtain the access token, and decode it to see the value for aud and scp. If value of aud is my_api_client_id, then we don't need to change anything in your web api appsettings.json. If value of aud is api://my_api_client_id, pls add Audience property like below.

    "AzureAd": {
      "Instance": "https://login.microsoftonline.com/",
      "Domain": "qualified.domain.name",
      "TenantId": "My-TENANT-ID",
      "ClientId": "backend_aad_app-CLIENT-ID",
      "Scopes": "access_as_user",
      "Audience": "api://clientid_of_the_app_exposed_api_which_should_be_id_of_backend_aad_app"
    },
    

    Comparing the information you shared above, all codes look good, so that could you pls checking the app ids you used in each place to make sure they didn't mix up? In the mean time, we might use only one app which is the My API both in the client and the backend api.

    I followed the official sample and had a test in my side, it worked for me.

    enter image description here