AzureAD / microsoft-identity-web

Helps creating protected web apps and web APIs with Microsoft identity platform and Azure AD B2C

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException` does not redirect to correct challenge endpoint for alternate authentication schemes

thebarrettlo opened this issue · comments

Microsoft.Identity.Web Library

Microsoft.Identity.Web

Microsoft.Identity.Web version

2.19.0

Web app

Sign-in users and call web APIs

Web API

Protected web APIs (validating tokens)

Token cache serialization

In-memory caches

Description

Context

In creating a Blazor server-side web app (.NET 8), my team is supporting two authentication schemes according to the IDWeb wiki (https://github.com/AzureAD/microsoft-identity-web/wiki/multiple-authentication-schemes). The default authentication scheme (using the default name, OpenIdConectDefaults.AuthenticationScheme/"OpenIdConnect") is standard Entra (https://login.microsoftonline.com), while the second authentication scheme (also OAuth2.0/OIDC) defines a different authentication scheme name. The user is authenticated against either authentication scheme using TokenAcquisition.GetAuthenticationResultForUserAsync, and exceptions are caught with MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException(Exception) according to https://github.com/AzureAD/microsoft-identity-web/wiki/Managing-incremental-consent-and-conditional-access.

When the second (non-default named) authentication scheme is used and MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException(Exception) is called, the user is routed to the challenge path without specifying the authentication scheme. This results in the default OpenIdConectDefaults.AuthenticationScheme to be used for the challenge, and the "One client credential type required" error is thrown.

Root cause

I have traced this to the use of Constants.BlazorChallengeUri in the redirect URI (MicrosoftIdentityConsentAndConditionalAccessHandler.cs:198), which does not respect the authentication scheme attempted to be used. Therefore, the AccountController.Challenge method is passed a null scheme (from the route parameterization) and the scheme used is set to OpenIdConectDefaults.AuthenticationScheme (line 85). As confirmation, making our own NavigationManager call with the same redirect URI but with the authentication scheme specified in the challenge path succeeds (i.e., /MicrosoftIdentity/Account/Challenge/<second auth scheme name>).

Reproduction steps

  1. In a Blazor Server (.NET 8) web app, add two authentication schemes. One scheme should be Entra and use the default scheme name ("OpenIdConnect") while the other should be an OAuth2.0/OpenID Connect scheme but use an arbitrary custom authentication scheme name.
  2. Call TokenAcquisition.GetAuthenticationResultForUserAsync within a try/catch block, passing in the custom authentication scheme name
  3. Handle exceptions with MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException(Exception)

Error message

  Exception occurred while processing message.
  MSAL.NetCore.4.60.3.0.MsalClientException:
    ErrorCode: Client_Credentials_Required_In_Confidential_Client_Application
  Microsoft.Identity.Client.MsalClientException: One client credential type required either: ClientSecret, Certificate, ClientAssertion or AppTokenProvider must be defined when creating a Confidential Client. Only specify one. See https://aka.ms/msal-net-client-credentials.
     at Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder`1.Validate()
     at Microsoft.Identity.Client.AcquireTokenByAuthorizationCodeParameterBuilder.Validate()
     at Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder`1.ValidateAndCalculateApiId()
     at Microsoft.Identity.Client.AbstractConfidentialClientAcquireTokenParameterBuilder`1.ExecuteAsync(CancellationToken cancellationToken)
     at Microsoft.Identity.Client.BaseAbstractAcquireTokenParameterBuilder`1.ExecuteAsync()
     at Microsoft.Identity.Web.TokenAcquisition.AddAccountToCacheFromAuthorizationCodeAsync(AuthCodeRedemptionParameters authCodeRedemptionParameters)
     at Microsoft.Identity.Web.TokenAcquisitionAspNetCore.AddAccountToCacheFromAuthorizationCodeAsync(AuthorizationCodeReceivedContext context, IEnumerable`1 scopes, String authenticationScheme)
     at Microsoft.Identity.Web.MicrosoftIdentityWebAppAuthenticationBuilder.<>c__DisplayClass11_1.<<WebAppCallsWebApiImplementation>b__1>d.MoveNext()
  --- End of stack trace from previous location ---
     at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, ClaimsPrincipal user, AuthenticationProperties properties, JwtSecurityToken jwt)
     at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync()

Id Web logs

No response

Relevant code snippets

public class Program
{
    public static void Main(string[] args)
    {
        WebApplicationBuilder? builder = WebApplication.CreateBuilder(args);
        // ...
        builder.Services
            .AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
            .AddMicrosoftIdentityWebApp(builder.Configuration.GetAzureAdSection())
            .EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetFirstScopes())
            .AddInMemoryTokenCaches();

        builder.Services
            .AddAuthentication()
            .AddMicrosoftIdentityWebApp(builder.Configuration.GetOtherSection(), openIdConnectScheme: Constants.CustomAuthenticationScheme, cookieScheme: Constants.CustomCookieScheme, displayName: Constants.CustomAuthenticationScheme)
            .EnableTokenAcquisitionToCallDownstreamApi(builder.Configuration.GetSecondScopes())
            .AddInMemoryTokenCaches();
        // ...
    }
}

/*
 * Token acquisition
 */
try
{
    AuthenticationResult res = await _tokenAcquisition.GetAuthenticationResultForUserAsync(_scopes, authenticationScheme: CurrentAuthenticationScheme);
    // ...
}
catch (Exception ex)
{
    _idAccessHandler.HandleException(ex);
}

Regression

No response

Expected behavior

MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException(Exception) should be able to work with non-default authentication schemes. This could be supported by adding a parameter to the HandleException and ChallengeUser method signatures for specifying the authentication scheme.

public void HandleException(Exception exception, string? authenticationScheme = null)
{
    // ...
    ChallengeUser(
        scopes.ToArray(),
        claims,
        userflow,
        authenticationScheme);
}

public void ChallengeUser(
    string[]? scopes,
    string? claims = null,
    string? userflow = null,
    string? authenticationScheme = null)
{
    // ...
    string scheme = authenticationScheme ?? OpenIdConnectDefaults.AuthenticationScheme;
    // ...
    string url = $"{BaseUri}/{Constants.BlazorChallengeUri}/{scheme}{redirectUri}"
                + ...;
    // ...
}