`MicrosoftIdentityConsentAndConditionalAccessHandler.HandleException` does not redirect to correct challenge endpoint for alternate authentication schemes
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
- 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.
- Call
TokenAcquisition.GetAuthenticationResultForUserAsyncwithin a try/catch block, passing in the custom authentication scheme name - 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}"
+ ...;
// ...
}
Constants.BlazorChallengeUri this one also should not be hardcoded. It prevents users to provide a custom challenge endpoint.
Suggested Fix
modify the HandleException and ChallengeUser methods to accept an optional authenticationScheme parameter. Here’s a proposed solution:
Modify the HandleException method:
public void HandleException(Exception exception, string? authenticationScheme = null)
{
// ...
ChallengeUser(
scopes.ToArray(),
claims,
userflow,
authenticationScheme);
}
Modify the ChallengeUser method:
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}"
+ ...;
// ...
}
Next Steps
Update the MicrosoftIdentityConsentAndConditionalAccessHandler class with the proposed changes.
Test the changes in a Blazor Server (.NET 8) web app with multiple authentication schemes to ensure the issue is resolved, see the dev app mvcwebapp-graph for a multi-auth scheme set-up.
Constants.BlazorChallengeUrithis one also should not be hardcoded. It prevents users to provide a custom challenge endpoint.
This looks to be a separate issue, right @Kumima ?
It should also be possible to find the authentication scheme from the metadata of the HttpContext.Request.
Constants.BlazorChallengeUrithis one also should not be hardcoded. It prevents users to provide a custom challenge endpoint.This looks to be a separate issue, right @Kumima ?
I'll post a new one for this, I thought they are similar since they are both about the extensibility for MicrosoftIdentityConsentAndConditionalAccessHandler.