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

[Bug] Configuring multiple authentication schemes using Microsoft Identity Web

Open chintanr97 opened this issue 4 years ago • 3 comments

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.

  1. The call from Microsoft.Identity.Web for adding the authentication scheme using the method: AddMicrosoftIdentityWebApi can be found here. Implicitly, it calls the add JWT bearer method to add the scheme for protecting the Web API.
  2. The AddJwtBearer call adds the scheme to the authentication builder as visible here.
  3. Now the AddScheme implementation expects that the handler type for the scheme is assignable to IAuthenticationRequestHandler as available here. However, if one attempts to achieve that with JwtBearerHandler then it fails. Therefore, the property _requestHandlersCopy remains empty always.
  4. Now, with the help of UseAuthentication functionality used for configuring the app, the AuthenticationMiddleware is invoked. In the authentication middleware it uses the Schemes property to fetch the schemes using: GetRequestHandlerSchemesAsync (as suggested here). However, the method GetRequestHandlerSchemesAsync in the AuthenticationSchemeProvider simply returns the value for _requestHandlersCopy (as shown here). Now because the _requestHandlersCopy is empty array, the AuthenticationMiddleware receives 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 the AzureAd1 scheme (based on our example above) - which leads to failure for an access token issued for AzureAd2 scheme.
  5. Given the identity as not authenticated simply by using the UseAuthentication call, 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 context httpContext does not populate the User.Identity correctly and httpContext.User.IsAuthenticated remains false.

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.

chintanr97 avatar Dec 01 '21 05:12 chintanr97

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?

jmprieur avatar Dec 01 '21 06:12 jmprieur

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.

chintanr97 avatar Dec 01 '21 07:12 chintanr97

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?

PatGet avatar Jul 06 '22 05:07 PatGet