microsoft-identity-web
microsoft-identity-web copied to clipboard
`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.GetAuthenticationResultForUserAsync
within 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}"
+ ...;
// ...
}