Search code examples
c#asp.net-coreazure-active-directoryblazordata-api-builder

How to get proper JWT token using IdentityModel and Azure AD to authenticate with my API?


I have Blazor Server App which is getting data from Microsoft's Data Api Builder (DAB). According to the docs, if I want to secure my API, I have to use Azure AD. I have set up two apps in Azure AD, one for my client and one for my API based on these docs:

https://github.com/Azure/data-api-builder/blob/main/docs/authentication-azure-ad.md https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps

The first doc, also explains how to set up Azure CLI and use it login and get a token. If I use the token it generates, my app can access the API just fine.

However, when trying to find a solution to generate and manage JWT tokens without using the CLI, I came across IdentityModel, which attaches itself to the HttpClient and requests the JWT token from Azure for you, Nice!

The problem is, when using the token requested by IdentityServer, I get 403 forbidden.

The token that worked using the AZ CLI, included the scope and roles I expected to see:

Cmd is:

az account get-access-token --scope api://bba-8a1e-e6d16fa4f99f/Endpoint.Access

This returns a token:

{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "-KI3Q9nNR7ZGew"
  }.{
  "aud": "bba4057e-67d4-4f99f",
  "iss": "https://login.microsoftonline.com/51d526da-a780bffe07/v2.0",
  "iat": 1682877238,
  "nbf": 1682877238,
  "exp": 1682882388,
  "aio": "AYQAe/8TfUYSCvGWRrrOdUN8=",
  "azp": "04b07795-8ddb-461a-bbee-02f9e1bf7b46",
  "azpacr": "0",
  "idp": "live.com",
  "name": "Admin User",
  "oid": "c83386fc-b7bc-448a5-801525",
  "preferred_username": "[email protected]",
  "rh": "0.AWIA2ibVUT2nl0ic0aHngL_-B34FpLvUZ09Eih7m0W-k-Z9jAFE.",
 "roles": [
    "Test.Role"
  ],
  "scp": "Endpoint.Access",
  "sub": "LvXOKlWQikSGa53IniRQE",
  "tid": "51d526da-a73d9cd1-a1e7",
  "uti": "C8oLb6qhRUKnAA",
  "ver": "2.0"
 }.[Signature]

When using the following approach as outlined here: https://identitymodel.readthedocs.io/en/latest/aspnetcore/worker.html

using var client = new HttpClient();
                var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                {
                    Address = "https://login.microsoftonline.com/51da-a73d-4897-9cd1-07/oauth2/v2.0/token",
                    ClientId = "5285be6e-0134-d824",
                    ClientSecret = "H768Q~J.cQmhNVAmIBaZU", //SPA secret
                    Scope = "api://bba4057e-67df99f/.default",
                    GrantType = "client_credentials"
                });

I get token:

 {
 "typ": "JWT",
 "alg": "RS256",
 "kid": "-KI3Q9nNR7bRofxmeZoXqbHZGew"
 }.{
 "aud": "bba4057e-67d4-444f-8a1e-e6d16fa4f99f",
 "iss": "https://login.microsoftonline.com/51d52cd1-a1e780bffe07/v2.0",
  "iat": 1682877623,
  "nbf": 1682877623,
  "exp": 1682881523,
  "aio": "E2ZgYAhMqo9fcejA5cz26hm8d//cBQA=",
  "azp": "bba4057e-67d4-444f-8a1e-e6d16fa4f99f",
  "azpacr": "1",
  "oid": "9d235ad2-4432-4983-b1e9-8726d68ddb84",
  "rh": "0.AWIA2ibVUT2nl0ic0aHngL_-B34FpLvUZ09Eih7m0W-k-Z9jAAA.",
  "sub": "9d235ad2-4432-4983-b1e9-8726d68ddb84",
  "tid": "51d526da-a73d-4897-9cd1-a1e780bffe07",
  "uti": "cb7ttHUn5kCvHllSI7m6AA",
  "ver": "2.0"
}.[Signature]

Notice how the token is missing the scope and roles. I'm not 100% sure if this is why I am getting rejected, but I assume so.

According to this statement found here: https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-add-app-roles-in-azure-ad-apps

If you're implementing app role business logic in an app-calling-API scenario, you have two app registrations. One app registration is for the app, and a second app registration is for the API. In this case, define the app roles and assign them to the user or group in the app registration of the API. When the user authenticates with the app and requests an ID token to call the API, a roles claim is included in the ID token. Your next step is to add code to your web API to check for those roles when the API is called.

It seems that that the roles are not returned unless I request an ID token instead of an Access token, however I cannot see a clear way to do so using the IdentityModel. Some documentation says I need to use OpenID for this but since I the consumer of my API is my client app and not a specific user, this doesn't seem like the proper flow. In fact the docs here, https://identitymodel.readthedocs.io/en/latest/aspnetcore/web.html say the following,

In web applications you might either want to call APIs using the client identity or the user identity. The client identity scenario is exactly the same as the previous section that covered service workers.

And the "worker" approach led me to the sample code used to request the token above.

The following shows my registered apps in Azure AD (second and fourth): Registered apps

The following image shows my API app's permissions: API app permissions

The following shows the API is exposed and my client app is authorized to use it. Expose API

The following shows the Role assigned to the API app: Roles

I do appreciate any help I can get. I have spent a couple of days on this so far and have run out of ideas. Thanks in advance.


Solution

  • To give you a more precise answer, I have the following question:

    • Are you intending to have your app make requests to Data Api Builder in the context of/impersonating a user? e.g. are the roles you create intended to be assigned to a user or to be assigned directly to the client application (Blazer Server app)?

    App Should Impersonate a signed in user

    If intending to have your app make requests impersonating a signed in user, you need to validate that the role is assigned to a user. Then in IdentityModel code, you'll need to use the authorization grant flow instead of the client credentials flow.Identity Model Docs because client credentials flow is "permits a web service (confidential client) to use its own credentials, instead of impersonating a user, to authenticate when calling another web service."Microsoft Docs.

    • I say this because I notice that your example token which excludes roles, has the azp claim (The application ID of the client using the token. The application can act as itself or on behalf of a user Microsoft Docs) set as the same GUID value as your Data Api Builder app ID (aud/ audience claim value in your token). This indicates to me that

    Once you create an app role within your Application Registration in Azure AD, you'll need to assign the role to a user with which you are testing authentication. It wasn't immediately clear in your question whether you did this, because your last bullet point just states you created the role for the app.

    • To assign a user role, you can follow the "How do I assign App roles" link in the App Roles blade (your last screenshot) which will take you to the "Enterprise Applications" blade for your app. From there, find the "Users and Roles" blade and assign a role to user.

    Last thing which has resulted in not receiving roles in the Access Token for me in the past: Navigate to the app registration blade for your app Data Api Builder. From there, on the left column, select the "Application Manifest" blade, and find the property accessTokenAcceptedVersion. It will be null unless you have manually changed it, where null resolves to v1.0 access tokens per Azure AD documentation.

    • The property should be set to 2 which tells Azure AD to issue v2.0 access tokens. Your ID tokens are v2 by default, and unfortunately requires this change in the app manifest to apply to access tokens. After you save this change, you'll have to wait a few minutes for the change to be recognized.

    App Should make requests to the API as itself, the app.

    If you intend for the app to make requests to the API and have the app assigned a role, you'll have to assign "Application Permissions" to your client app.

    To do that:

    1. Go to your client app's app registration blade
    2. Select "API Permissions"
    3. Select "Add Permissions"
    4. Select "Application Permissions", then check the box for your role, since you configured it to be a "User, group, or application role."
    5. Select "Add Permission"

    Once this is done, your existing client credential flow config should result in your app getting an access token with the desired role. Do note that this would mean that all requests to Data Api builder with this client would be using the client app's identity which has membership to that role.