asp.net-web-apiazure-active-directoryazure-ad-msalmicrosoft.identity.web

MsalClientException IDW10104 from GetAccessTokenForAppAsync


I have an ASP.NET Core Web API set up as App Service in Azure with an App Registration in our AzureAd

In appsettings.json I have (anonimized)

 "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "ourdomain.co.uk",
    "TenantId": "n9n999n9-9999-nnnn-9n9n9-9n9n9n9n9n9",
    "ClientId": "81933a15-157f-45b0-bc32-3d7d6d62f4a7",
    "Audience": "https://ourdomain.co.uk/breathe.notifications-service",
    "ClientSecret": "a6a6a6a~EEizqWNa8itAAAjcrycxnCtxaVgKTFx"
  },

That app has an API permission in Azure Ad that allows me to call another app service, Audit. The audit service does not have any specific scopes defined but it does have an app role called Audit.Write

In the calling API i need to get a token to call audit so I run this code

var accessToken = await this.tokenAcquisition.GetAccessTokenForAppAsync(this.auditApiScope);
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

Note the call to GetAccessTokenForAppAsync rather than the more common GetAccessTokenForUserAsync

The scope string that I am passing is

https://ourdomain.co.uk/us.audit-service/.default

When I call GetAccessTokenForAppAsync it is failing with MSALException

IDW10104: Both client secret and client certificate cannot be null or whitespace, and only ONE must be included in the configuration of the web app when calling a web API. For instance, in the appsettings.json file.

The client secret is in the AzureAd config, I am not specifying a certificate.


Solution

  • I now have this working and have two options but before I outline those I need to offer some extra background.

    This Web Api and others we have created offer functionality to Azure Ad users and Azure B2C users. This functionality was first possible with Microsoft.Identity.Web 1.11.0 and we hjave been using 1.11.0 since it was released. However we always had an issue where we would generate thousands of exceptions because MSAL was getting confused ny which scheme to use.

    We came across this blog post, Removing misleading IDX10501 logs when using multiple authentication schemes in ASP.NET Core 3.1 there is more detail in this github thread, https://github.com/oliviervaillancourt/blog/issues/3.

    Our Startup.cs Configure Services looks like this

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMicrosoftIdentityWebApiAuthentication(this.configuration)
                                                                .EnableTokenAcquisitionToCallDownstreamApi()
                                                                .AddInMemoryTokenCaches();
    
        services.AddAuthentication()
                .AddMicrosoftIdentityWebApi(this.configuration, "AzureAdB2C", "B2CScheme", true);
    
        services.AddAuthentication("AzureAD_OR_AzureAdB2C")
                    .AddMicrosoftIdentityWebApi(
                                                jwtBearerOptions =>
                                                {
                                                    var azureAdB2CConfig = this.configuration.GetSection("AzureAdB2C");
                                                    jwtBearerOptions.ForwardDefaultSelector = context =>
                                                    {
                                                        var token = string.Empty;
    
                                                        if (context.Request.Headers.TryGetValue("Authorization", out var value))
                                                        {
                                                            string authorization = value;
                                                            if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                                                            {
                                                                token = authorization.Substring("Bearer ".Length).Trim();
                                                            }
                                                        }
    
                                                        if (token == null)
                                                        {
                                                            this.logger.LogInformation($"Cannot get the Token out of the Authorization header");
                                                        }
    
                                                        var jwtHandler = new JwtSecurityTokenHandler();
    
                                                        if (jwtHandler.CanReadToken(token))
                                                        {
                                                            var jwtToken = jwtHandler.ReadJwtToken(token);
                                                            var expectedB2CIssuer = $"{azureAdB2CConfig.GetValue<string>("Instance")}/{azureAdB2CConfig.GetValue<string>("TenantId")}/v2.0/";
    
                                                            if (string.Compare(jwtToken.Issuer, expectedB2CIssuer, true) == 0)
                                                            {
                                                                // Claim is from B2C so this request should be validated against the B2C scheme.
                                                                this.logger.LogInformation($"Request is with a B2C issued token so refer to B2CScheme. Token issuer: {jwtToken.Issuer} B2C Issuer: {expectedB2CIssuer}");
                                                                return "B2CScheme";
                                                            }
                                                            else
                                                            {
                                                                this.logger.LogInformation($"Request is not with a B2C issued token so refer to Bearer scheme. Token issuer: {jwtToken.Issuer} B2C Issuer: {expectedB2CIssuer}");
                                                            }
                                                        }
                                                        else
                                                        {
                                                            this.logger.LogInformation("Request token could not be read so refer to Bearer scheme");
                                                        }
    
                                                        return "Bearer";
                                                    };
                                                },
                                                identityOptions =>
                                                {
                                                    var azureAdB2CConfig = this.configuration.GetSection("AzureAdB2C");
                                                    identityOptions.Instance = azureAdB2CConfig.GetValue<string>("Instance");
                                                    identityOptions.TenantId = "AzureAD_OR_AzureAdB2C";
                                                    identityOptions.ClientId = "AzureAD_OR_AzureAdB2C";
                                                },
                                                "AzureAD_OR_AzureAdB2C",
                                                false);
    
        services.AddControllers()
                .AddNewtonsoftJson();
    
        services.AddLogging(options =>
        {
            // hook the Console Log Provider
            options.AddConsole();
            options.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
    
            // hook the Application Insights Provider
            options.AddFilter<ApplicationInsightsLoggerProvider>(string.Empty, Microsoft.Extensions.Logging.LogLevel.Trace);
    
            // pass the InstrumentationKey provided under the appsettings
            options.AddApplicationInsights(this.configuration["APPINSIGHTS_INSTRUMENTATIONKEY"]);
        });
      
    }
    

    The logic used by the ForwardDefaultSelector is what helps us work with multiple schemes and forward ASP.NET to the right scheme.

    Now back to the answer.

    If I remove the ForwardDefaultSelector I no longer get the IDW10104 however that is what we use to remopve all the extraneous exceptions schemes so that is not really going to be workable.

    The only viable option is to move the Web Api from the latest version of Microsoft.Identity.Web 1.21.1 to 1.16.0. The issue that is causing us to get the exception was introduced in 1.16.1. I will raise an issue on the MSAL github for 1.16.1. We were previously using 1.11.0.