microsoft-identity-web icon indicating copy to clipboard operation
microsoft-identity-web copied to clipboard

IDX40001 raised when using multiple Authentication Schemes

Open munkii opened this issue 2 years ago • 8 comments

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

2.16.1

Web app

Sign-in users and call web APIs

Web API

Protected web APIs call downstream web APIs

Token cache serialization

In-memory caches

Description

ASPNET Core Web API using .NET8 We have multiple controllers, some using an Azure Ad authentication scheme and some Azure Ad B2C. This is not an new issue for us but one that I have rercently removed the hack for hoping that it would be no longer needed.

If we attempt to configure both Authentication Schemes

services
    .AddMicrosoftIdentityWebApiAuthentication(this.Configuration)
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

services.AddAuthentication()
       .AddMicrosoftIdentityWebApi(this.Configuration, "AzureAdB2C", "B2CScheme", true);

We will get a warning written to the logs whenver the client app (secured with Azure Ad B2C) call the app service

IDX40001: Issuer: 'https://ourTenanatName.b2clogin.com/ourTenantId/v2.0/', does not match any of the valid issuers provided for this application.

This is an incorrect statement. That is a configured issuer. Furthermore if I remove the Azure Ad configuration

services
    .AddMicrosoftIdentityWebApiAuthentication(this.Configuration)
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

The warning goes away. Obviously I cannot do that as this Api needs to support Azure Ad consumers.

I have tried directly setting the ValidIssuers array but that does not stop the warning

services.AddAuthentication()
        .AddMicrosoftIdentityWebApi(
            jwtBearerOptions =>
            {
                this.Configuration.Bind("AzureAdB2C", jwtBearerOptions);
                jwtBearerOptions.TokenValidationParameters.ValidIssuers = new List<string>
                                                                            {
                                                                                "https://ourDevTenantName.b2clogin.com/DevTenantId/v2.0/",
                                                                                "https://outTestTenantName.b2clogin.com/TesttenantId/v2.0/"
                                                                                // Add other valid issuers here
                                                                            };
            },
            microsoftIdentityOptions =>
            {
                this.Configuration.Bind("AzureAdB2C", microsoftIdentityOptions);
            },
            "B2CScheme",
            true);

Our previous hack was use a third fake scheme and a ForwardDefeaultSelector

services.AddAuthentication("AzureAD_OR_AzureAdB2C")
                        .AddMicrosoftIdentityWebApi(
                                                    jwtBearerOptions =>
                                                    {
                                                        jwtBearerOptions.ForwardDefaultSelector = this.GetDefaultAuthenticationSchemeFromToken;
                                                    },
                                                    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);

I am trying to remove any hacks.

Surely what we are trying to do is relatively normal and should not cause these issues

Reproduction steps

Configure both Azure Ad and Azure Ad B2C schemes

services
    .AddMicrosoftIdentityWebApiAuthentication(this.Configuration)
    .EnableTokenAcquisitionToCallDownstreamApi()
    .AddInMemoryTokenCaches();

services.AddAuthentication()
       .AddMicrosoftIdentityWebApi(this.Configuration, "AzureAdB2C", "B2CScheme", true);

Error message

IDX40001: Issuer: 'https://ourTenanatName.b2clogin.com/ourTenantId/v2.0/', does not match any of the valid issuers provided for this application

Id Web logs

No response

Relevant code snippets

[HttpPut]
[Authorize(AuthenticationSchemes = "B2CScheme")]
[Route("installations")]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest)]
[ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
public async Task<IActionResult> UpdateInstallation([Required] DeviceIdentity deviceIdentity)
{
    IncludeUserIdAsATag(this.ControllerContext.HttpContext.User, deviceIdentity);
    IncludeClinicsAsTags(this.ControllerContext.HttpContext.User, deviceIdentity);

    if (string.IsNullOrWhiteSpace(deviceIdentity.InstallationId) ||
        string.IsNullOrWhiteSpace(deviceIdentity.Platform) ||
        string.IsNullOrWhiteSpace(deviceIdentity.PushChannel))
    {
        this.logger.LogWarning("UpdateInstallation: Body missing mandatory values");
        return new UnprocessableEntityObjectResult("Body missing mandatory values");
    }

    if (notificationService.IsPlatformSupported(deviceIdentity.Platform) == false)
    {
        this.logger.LogWarning("UpdateInstallation: Platform {DevicePlatform} not supported", deviceIdentity.Platform);
        return new UnprocessableEntityObjectResult("Platform not supported");
    }

    var errorMessage = await this.notificationService.CreateOrUpdateInstallationAsync(deviceIdentity, HttpContext.RequestAborted);

    if (errorMessage != null)
    {
        return new ObjectResult(errorMessage)
        {
            StatusCode = (int)HttpStatusCode.InternalServerError,
        };
    }
    else
    {
        return new OkResult();
    }
}

Regression

No response

Expected behavior

I would expect the Warning message to not be written to the logs.

munkii avatar Feb 27 '24 20:02 munkii

I am getting the same issue. When I comment either Azure Ad or B2C configuration, the application accepts token from the clients in that uncommented tenant. I am using .NET 8. Any suggestions would be great help!

akjal avatar Feb 29 '24 10:02 akjal

Hi @munkii , Would you mind sharing the code for the hack GetDefaultAuthenticationSchemeFromToken . I am planning to implement something similar until we have a permanent fix. Thanks

akjal avatar Mar 01 '24 10:03 akjal

Sorry for the delay in replying...

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";
	};

munkii avatar Mar 05 '24 20:03 munkii

Hi, I have been struggling with this for a while myself and thanks to your code stumbled upon this documentation from Microsoft: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-8.0#use-multiple-authentication-schemes

Here they do something very similar, except using AddPolicyScheme instead of AddMicrosoftIdentityWebApi, allowing you to specify the ForwardDefaultSelector without having to pass foobar arguments for the Azure AD (B2C) configuration. So it seems the 'hack' is actually the correct way to implement support for multiple authentication schemes.

wouterSeyen avatar Mar 25 '24 12:03 wouterSeyen

It seems pretty dumb to me that this “hack” is how it is supposed to work.

munkii avatar Mar 27 '24 07:03 munkii

@westin-m - can we add a configuration switch, or something in the appsettings.json, which developers can turn on if they want the full logs from IdentityModel, otherwise, we don't let IdentityModel log, only the logs from aspnetcore.

jennyf19 avatar Mar 28 '24 01:03 jennyf19

I have this issue but is just being logged as trace. If I understand, it is basically getting the correct policy as you mentioned in ForwardDefaultSelector, however when you set [Authorize(AuthenticationSchemes = "B2CScheme")], it will try to validate the issuer for "B2CScheme" using "Bearer" then, it logs the trace.

IDX40001: Issuer: 'https://{domain}.b2clogin.com/{ourTenantId}/v2.0/', does not match any of the valid issuers provided for this application.

In my case, to fix it, I just have to remove the schemas from Authorize attribute and works well. In your case, you cannot. My question is, why it is switching the validations?

smileyiori avatar May 23 '24 02:05 smileyiori

#2816 enables turning off logs from IdWeb/ using a null logger for the same effect.

westin-m avatar Jun 03 '24 20:06 westin-m