[Bug] Configuring multiple authentication schemes using Microsoft Identity Web
Which version of Microsoft Identity Web are you using? Microsoft Identity Web 1.16.0
Where is the issue?
- Web app
- [ ] Sign-in users
- [ ] Sign-in users and call web APIs
- Web API
- [x] Protected web APIs (validating tokens)
- [ ] Protected web APIs (validating scopes)
- [ ] Protected web APIs call downstream web APIs
- Token cache serialization
- [ ] In-memory caches
- [ ] Session caches
- [ ] Distributed caches
- Other (please describe)
Is this a new or an existing app?
This is a new app or an experiment.
Problem Description
We are trying out to setup multiple Azure AD authentication schemes with our project. The AddAuthentication and AddAuthorization calls are configured as follows:
services.AddAuthentication("AzureAd1")
.AddJwtBearer("AzureAd1", options =>
{
options.Authority = "https://login.microsoftonline.com/tenantId1";
options.Audience = "clientId1";
})
.AddMicrosoftIdentityWebApi(configuration, "AzureAd2", "AzureAd2");
...
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("AzureAd1", "AzureAd2")
.Build();
});
...
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizeFilter());
});
In the Startup.cs for the project we are configuring the app as follows:
...
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthentication();
app.UseMiddleware<TestMiddleware>();
app.UseAuthorization();
...
On the controller we have the following attribute set: [Authorize]. Now, when a request is received for the token corresponding to AzureAd1 scheme, the authentication works correctly. However, when we send a request with token issued with respect to AzureAd2 scheme, then the authentication fails.
We try to add debug the code by traversing the ASP .NET Core code base.
- The call from
Microsoft.Identity.Webfor adding the authentication scheme using the method:AddMicrosoftIdentityWebApican be found here. Implicitly, it calls the add JWT bearer method to add the scheme for protecting the Web API. - The
AddJwtBearercall adds the scheme to the authentication builder as visible here. - Now the
AddSchemeimplementation expects that the handler type for the scheme is assignable toIAuthenticationRequestHandleras available here. However, if one attempts to achieve that withJwtBearerHandlerthen it fails. Therefore, the property_requestHandlersCopyremains empty always. - Now, with the help of
UseAuthenticationfunctionality used for configuring the app, theAuthenticationMiddlewareis invoked. In the authentication middleware it uses theSchemesproperty to fetch the schemes using:GetRequestHandlerSchemesAsync(as suggested here). However, the methodGetRequestHandlerSchemesAsyncin theAuthenticationSchemeProvidersimply returns the value for_requestHandlersCopy(as shown here). Now because the_requestHandlersCopyis empty array, theAuthenticationMiddlewarereceives the same and does not iterate over any of the added schemes. Because of this reason, it will continue to get the default scheme and try to parse using theAzureAd1scheme (based on our example above) - which leads to failure for an access token issued forAzureAd2scheme. - Given the identity as not authenticated simply by using the
UseAuthenticationcall, it results into an issue where, let us say if we have a custom middleware immediately after the authentication middleware, here invoked by:UseMiddleware<TestMiddleware>()- then in this middleware, the Invoke function's (Invoke(HttpContext httpContext)) HTTP contexthttpContextdoes not populate theUser.Identitycorrectly andhttpContext.User.IsAuthenticatedremainsfalse.
Now, looking at the implementation I wanted to identify if this is truly achievable using the ASP .NET core authentication middleware or if I missing some configuration change that is leading to this behaviour.
Repro We can reproduce the error using the sample published by the Microsoft Identity Web library as available here. We can extend the project to include the custom middleware as follows:
TestMiddleware.cs
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace mvcwebapp_graph.Middlewares
{
public class TestMiddleware
{
private readonly RequestDelegate next;
public TestMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext httpContext)
{
var userIdentity = httpContext.User.Identity;
if (userIdentity?.IsAuthenticated == true && userIdentity is ClaimsIdentity @identity)
{
// do something.
}
await this.next(httpContext);
}
}
}
Expected behavior IMO, let us say, if we have 1 default scheme and other additional schemes are also configured for the app, then the implementation should try out all the schemes in some order until one of them succeeds.
Actual behavior
Because the JWT Bearer Handler is not assignable to IAuthenticationRequestHandler, the JWT bearer schemes are never added to the _requestHandlersCopy. Now, I am not sure, if that is expected, but then this enforces the AuthenticationMiddleware to always fallback for the default scheme (because it did not find any configured scheme that was assignable to IAuthenticationRequestHandler).
NOTE: To get better clarity on the issue, it has been raised on aspnetcore GitHub repository as well as available here.
Thanks @chintanr97 This is indeed more a question for the ASP.NET core team.
Meanwhile, if you use [Authorize(AuthenticationSchemes="AzureAd2")} in your controller, this should work?
Hi @jmprieur,
Thanks a lot for your response. While I am waiting for the response from ASP.NET core team (issue linked in previous comment), I tried out the change you recommended.
As a requirement, we need to allow authentication using both the schemes along with some other requirement settings on the authorization policy. Therefore, we use our AddAuthorization call as follows:
serviceCollection.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes("AzureAd1", "AzureAd2")
.Build();
options.AddPolicy("MyCustomPolicy", policy =>
{
// configure some additional properties
policy.AddAuthenticationSchemes("AzureAd1", "AzureAd2");
});
});
Followed by this, the controllers use the policy as: [Authorize(Policy = "MyCustomPolicy")]. However, when the API request is received for one of the supported paths over this controllers, it does not authenticate using AzureAd2.
And I think the reason this will not populate the identity correctly in the HTTP context of a middleware prior to UseAuthorization (in this case the TestMiddleware), is because upto then the configured policies are not triggered.
Hi @chintanr97 , as the Issue is closed on asp.net side, did you ever get this to work just by policy? Or did you stick with the custom handler?