11
votes

I'm trying to create a simple example of Azure AD authentication using this sample except for my client is JQuery. I am not sure why I get the 401 error about the audience is invalid when the token shows the audience is https://myportal.onmicrosoft.com/test_core_web_api_spa. This matches the API definition in Azure. The only missing piece is the custom scope of user_impersonation but when I make the call using MSAL clientApplication.acquireTokenSilent(tokenRequest2) to acquire the token it my scopes matches the full URL of the API with scope:

const tokenRequest2 = {
    scopes: ["https://myportal.onmicrosoft.com/test_core_web_api_spa/user_impersonation"]
};

In the API to establish authentication I am using this code (I noticed not many examples use this method)

services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
   .AddAzureADBearer(options => Configuration.Bind("AzureAd", options));

And the configuration for the API is

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "myportal.onmicrosoft.com",
    "TenantId": "my-tenant-guid",
    "ClientId": "my-api-client-guid"
  },

I've noticed many examples showing a different format for the API (I assume these are older version) but the exposed API scope is listed in Azure as https://myportal.onmicrosoft.com/test_core_web_api_spa/user_impersonation. I have also added the guid of the client using the Azure dashboard to access this exposed API scope.

Any ideas where I have gone wrong? Alternately, any simple examples using MSAL, JQuery for the client, and a simple .Net Core Web Api? Seems like all the examples I find are out of date or use a different client or a different authentication method.

Update to show expose api settings in Azure for web api app. I've added an image from Azure showing the settings for the "expose an api" screen. I've added the custom scope user_impersonation then added the client and granted it access to that scope. As you can see my Azure subscription does not have the api://guid format that is seen by others. When I try to use that api://guid format I get the error The resource principal named api://guid was not found in the tenant.

I also added image of the token. The aud tag matches my web api app name in Azure. And the scp lists the scope that I attached to my scopes request. I just cannot see what else to try.

enter image description here

expose api settings for web api

5
One thing you should check is what is the aud claim in the token? Is it your API app ID URI or the API client id?juunas
@juunas The aud claim it the token matches the API as exposed in Azure portal. I've listed it in the second sentence then reference the definition near the end.pretzelb

5 Answers

23
votes

The problem was the configuration data for the Web API. When they say the ClientId what they really want is the value under the "expose an API" option where it says "Application ID URI". What I was putting in there was the guid for the Web Api application registration. Below is how it should look.

  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "myportal.onmicrosoft.com",
    "TenantId": "mytenant-guid",
    "ClientId": "https://myportal.onmicrosoft.com/test_core_web_api_spa"
  },

Note on the API format. It appears that when you register the application directly in Azure the format for the exposed API will be api://app-guid. But if you first create your application using Visual Studio then the format will default to something like https:///myportal.onmicrosoft.com/project-name-in-visual-studio.

3
votes

Like most people here I was stuck on this for a little while and the documentation seriously lets it down.

While following the MS guide it populates appsettings file in the Server project like so

"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "contoso.onmicrosoft.com",
"TenantId": "e86c78e2-8bb4-4c41-aefd-918e0565a45e",
"ClientId": "41451fa7-82d9-4673-8fa5-69eff5a761fd",
 }

Frustratingly this fix is as simple as pre-fixing the client ID with api:// so that it matches both the audience in the JWT and the Application ID URI on the Expose an API section of your server app in AAD

"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "contoso.onmicrosoft.com",
"TenantId": "e86c78e2-8bb4-4c41-aefd-918e0565a45e",
"ClientId": "api://41451fa7-82d9-4673-8fa5-69eff5a761fd",
} 

enter image description here

1
votes

Don't just look at the below code, PLEASE READ!

Building on what pretzelb said, don't worry so much about how your client is configured (as long as your client is working). Assuming the error you are getting is related to invalid issuer, or invalid audience then try the following.

Debug your api and set a debug point somewhere after your client has tried to connect and look at HttpContext - Request - Headers - Values, in there you will see your token so drop that in jwt io website and you should find your issuer and your audience which may be completely different than you expected. At this point change your TokenValidationParameters to agree with what you found in the token, then it should work.

Using .Net Core 3.1 and Micorosft.Identity.Web (currently in preview), I'll show you what worked for me:

Here is my config with fake guids. Do not try to use the same instance, you need to look at the issuer in the token your client is sending.

"AzureAD": {
    "Instance": "https://sts.windows.net/",
    "ClientId": "28e36e14-5191-4987-bcdf-982d958de2b3",
    "Domain": "funco.com",
    "TenantId": "744ce43f-a10f-499f-29f3-7je6ef439787"
  }

create a model for the AzureAd config section somewhere:

public class AzureModel
    {
        public String Instance { get; set; }
        public String ClientId { get; set; }
        public String Domain { get; set; }
        public String TenantId { get; set; }
    }
}

then...

services.AddProtectedWebApi(Configuration, AzureADDefaults.AuthenticationScheme,  AzureADDefaults.JwtBearerAuthenticationScheme);
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
    var azureSettings = Configuration.GetSection(AzureADDefaults.AuthenticationScheme).Get<AzureModel>();
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        ValidateIssuer = true,
        ValidIssuer = $"{azureSettings.Instance}{azureSettings.TenantId}/",
        ValidateAudience = true,
        ValidAudience = $"https://{azureSettings.Domain}/{azureSettings.ClientId}",
        //ValidateLifetime = true,
        //ClockSkew = TimeSpan.Zero
    };

    options.Events = new JwtBearerEvents();

    options.Events.OnTokenValidated = async context =>
    {
            await Task.FromResult(0);
    };
    options.Events.OnAuthenticationFailed = async context =>
    {
        await Task.CompletedTask;
    };
    options.Events.OnMessageReceived = async context =>
    {
        await Task.CompletedTask;
    };
});
0
votes

The scope is not correct, that's why you got the audience is invalid.

The scope should be something like api://web_api_clientId/read.

enter image description here

0
votes
const tokenRequest2 = {
    scopes: ["test_core_web_api_spa/user_impersonation"]
};

or

const tokenRequest2 = {
    scopes: ["app-guid/user_impersonation"]
};

When your application needs to request an access token with specific permissions for a resource API, pass the scopes containing the app ID URI of the API in the format /.

Some example scope values for different resources:

Microsoft Graph API: https://graph.microsoft.com/User.Read Custom web API: api://11111111-1111-1111-1111-111111111111/api.read The format of the scope value varies depending on the resource (the API) receiving the access token and the aud claim values it accepts.

For Microsoft Graph only, the user.read scope maps to https://graph.microsoft.com/User.Read, and both scope formats can be used interchangeably.

Certain web APIs such as the Azure Resource Manager API (https://management.core.windows.net/) expect a trailing forward slash ('/') in the audience claim (aud) of the access token. In this case, pass the scope as https://management.core.windows.net//user_impersonation, including the double forward slash ('//').

Other APIs might require that no scheme or host is included in the scope value, and expect only the app ID (a GUID) and the scope name, for example:

11111111-1111-1111-1111-111111111111/api.read

REF: https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-acquire-cache-tokens

Use https://jwt.ms/ to decode your token