dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.21k stars 9.95k forks source link

Multiple Authentication Schemes are mutually exclusive #40820

Open Exagram opened 2 years ago

Exagram commented 2 years ago

Is there an existing issue for this?

Describe the bug

I have a React SPA and a Mobile App that calls a Web API protected by Azure AD OIDC.

Goal: If AT LEAST one of the schemes succeeds then the user should be authenticated. Unfortunately the two schemes are mutually exclusive:

Expected Behavior

Both React SPA and Mobile App should be able to authenticate using their separate authentication schemes.

Steps To Reproduce

startup.cs example with JWT default:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddScheme<MobileAuthenticationSchemeOptions, MobileAuthenticationHandler>(MobileAuthenticationDefaults.AuthenticationScheme, null)
                .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAD:LycheeWebAPI"));
services.AddAuthorization(options =>
            {
                options.DefaultPolicy = new AuthorizationPolicyBuilder(
                    JwtBearerDefaults.AuthenticationScheme,
                    MobileAuthenticationDefaults.AuthenticationScheme)
                    .RequireAuthenticatedUser()
                    .Build();
            });
services.AddScoped<IAuthenticationHandler, MobileAuthenticationHandler>();
...
var policy = new AuthorizationPolicyBuilder()
                .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme, MobileAuthenticationDefaults.AuthenticationScheme)
                .RequireAuthenticatedUser()
                .Build();
mvcOptions.Filters.Add(new AuthorizeFilter(policy));
...
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });

MobileAuthenticationHandler:

public class MobileAuthenticationHandler : AuthenticationHandler<MobileAuthenticationSchemeOptions>
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            // validation comes in here
            if (!Request.Headers.ContainsKey(ApiConstants.MobileApiHttpHeader))
            {
                return Task.FromResult(AuthenticateResult.NoResult());
            }
            ...
            var claimsIdentity = new ClaimsIdentity(claims, nameof(MobileAuthenticationHandler));
            var ticket = new AuthenticationTicket(
                new ClaimsPrincipal(claimsIdentity), this.Scheme.Name);
            return Task.FromResult(AuthenticateResult.Success(ticket));
}

MobileAuthenticationOptions.cs:

public class MobileAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
}

MobileAuthenticationDefaults.cs:

public static class MobileAuthenticationDefaults
{
    public const string AuthenticationScheme = "MobileAuthenticationScheme";
}

Exceptions (if any)

N/A

.NET Version

6.0.101

Anything else?

.NET SDK (reflecting any global.json):
 Version:   6.0.101
 Commit:    ef49f6213a

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.19043
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\6.0.101\

Host (useful for support):
  Version: 6.0.3
  Commit:  c24d9a9c91

.NET SDKs installed:
  6.0.101 [C:\Program Files\dotnet\sdk]

.NET runtimes installed:
  Microsoft.AspNetCore.App 5.0.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 5.0.15 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.1 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 6.0.3 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 5.0.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 5.0.15 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.1 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 6.0.3 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 5.0.13 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 5.0.15 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
  Microsoft.WindowsDesktop.App 6.0.1 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]
davidfowl commented 2 years ago

They can't both be active so that's not the right approach. I think you have 2 options. Add a policy scheme that selects the right scheme based on the incoming request. See https://docs.microsoft.com/en-us/aspnet/core/security/authentication/policyschemes?view=aspnetcore-6.0

cc @HaoK Do we have this in the box (pass N schemes to an uber scheme and it'll pick the first one that succeeds).

HaoK commented 2 years ago

No we don't have any additional logic in our policy (uber) schemes, the only sugar we have is the set of ForwardXyz properties on the default auth options for schemes (Authenticate/Challenge/Forbid) that will make it easy to forward to another scheme. You have to implement your own handler today to try N schemes and stop on the first one.

HaoK commented 2 years ago

Certainly easy to add some built in policy schemes that do some common scenarios though, but it seems like people haven't had too much trouble rolling there own

HaoK commented 2 years ago

Or maybe we should just add a ne example of this pick first scheme that succeeds Authenticate to the policyschemes docs

HaoK commented 2 years ago

The ForwardDefaultSelector is an implementation detail of our auth handlers, there's no concept of this in the authentication/authorization stacks themselves. So its not exposed at the AddAuthentication level because not all auth schemes will support that concept

davidfowl commented 2 years ago

Let’s update the docs for this scenario

HaoK commented 2 years ago

Is it good enough to document the common case of having the default scheme fallback to another scheme if it doesn't succeed? I don't think the N scheme case is really that common, its usually just between 2 schemes right?

davidfowl commented 2 years ago

Is 2 -> 3 much harder?

HaoK commented 2 years ago

Depends on exactly what we want to illustrate, something like logic in the ForwardDefaultSelector doesn't really care as that's just a big switch. Are we looking to document that pattern? Alternatively I could just show something generic like a FallbackPolicyScheme, which you could then chain up like a linked list, i.e.

  services.AddAuthentication("Primary")
    .AddScheme<FallbackPolicyScheme>("Primary", o => o.FallbackScheme("Secondary"); o.ForwardAuthenticate("Cookies"))
    .AddScheme<FallbackPolicyScheme>("Secondary", o => o.FallbackScheme("Third"); o.ForwardAuthenticate("Bearer"));
    .AddScheme<FallbackPolicyScheme>("Third", o => o.FallbackScheme("Fourth"; o.ForwardAuthenticate("Cookie2"))
    .AddCookie("Fourth")

And show how to implement the FallbackPolicyScheme to do this kind of chaining

HaoK commented 2 years ago

I guess if we wanted to make this in the box, we could also just add the concept of FallbackAuthenticateScheme to our default implementation so this kind of thing just works for authenticate (check yourself for authentication, if you don't have anything, then fallback to the FallbackAuthenticateScheme), its basically a second Foward, but instead of always forwarding, you only fallback

ghost commented 2 years ago

Thanks for contacting us.

We're moving this issue to the .NET 7 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

gjmoyer commented 2 years ago

I have to support 3 schemes (ADFS2012, ADFS2016, and Azure AD). Azure AD is new I am adding. When I added Azure AD as teh 3rd scheme via AddMicrosoftIdentityWebApi where my .NET6 WebAPI is deployed to Azure App Service. The Trace log started filling up with IDX10503, IDX10223, IDX10516, IDX10205 errors. Not sure why these classified as errors and then go to Trace and not Exceptions. I removed AddMicrosoftIdentityWebApi and used AddJwtBearer instead to remove the errors.

Can someone please make it more clear how to implement the forward selector? In my case I wish to authorize all 3 to every controller. I currently have the following which always tries all 3.

builder.Services .AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .AddAuthenticationSchemes(AzureAd, Adfs2012, Adfs2016) .RequireRole(User_Roles) .Build(); });

maxandriani commented 2 years ago

Depends on exactly what we want to illustrate, something like logic in the ForwardDefaultSelector doesn't really care as that's just a big switch. Are we looking to document that pattern? Alternatively I could just show something generic like a FallbackPolicyScheme, which you could then chain up like a linked list, i.e.

  services.AddAuthentication("Primary")
    .AddScheme<FallbackPolicyScheme>("Primary", o => o.FallbackScheme("Secondary"); o.ForwardAuthenticate("Cookies"))
    .AddScheme<FallbackPolicyScheme>("Secondary", o => o.FallbackScheme("Third"); o.ForwardAuthenticate("Bearer"));
    .AddScheme<FallbackPolicyScheme>("Third", o => o.FallbackScheme("Fourth"; o.ForwardAuthenticate("Cookie2"))
    .AddCookie("Fourth")

And show how to implement the FallbackPolicyScheme to do this kind of chaining

@HaoK That is interesting. I'm strugling w/ one use case I have on my company, and I hope this ForwardAuthenticate pattern will help me to provide an elegant solution.

We actually solve the use case described by @exagran by delegate the responsibility to handle multiple ISP providers to a SSO Gateway. This gateway generates authorization tokens to all my applications. But my aplications has a very diverse authorization strategies. Some of then use rbac, some use permission/claims, and other a mix of rbac at high level and permissions at application level. In short, I have a JWT Bearer as Authentication Scheme with provides the user identity and some enterprise (SSO) level rbac. But I also have a custom scheme that loads application level claims and join those to the ClaimsPrincipal.

Initially, we wrote an Middleware to grant the local claims to the authenticated identity. Then we migrate this code to authorization scheme level because it seems the right place to put this code. So we have now the AppSchemeHandler that uses JwtBearerHandler as dependency by composition that validates the JWT and create the Principal, then AppScheme attach additional claims and officially return AuthenticateResult.Success.

So, my question is, can this proposal pattern of ForwardAuthentication fit my use case instead of build an AuthorizationSchema w/ dependencies by composition, and chain then as an Authorization workflow?

Or i'm messing up the authorization schema purpose?

HaoK commented 2 years ago

Hey @maxandriani let me see if I understand your scenario properly before trying to answer anything:

It sounds like you have a jwt bearer scheme which has your basic user / roles claims. And then you want to also have additional application level claims that you want included in the ClaimsPrincipal.

There are indeed lots of different ways you can do this:

  1. Using an AuthorizationPolicy that specifies both your JwtBearer + AppSchemes as AuthenticationSchemes, this will merge them and give you a combined ClaimsPrincipal
  2. Implement a JwtAppScheme that takes care of the combining inside of its authenticate, it could do something simple like check for the jwt, if it had that, then get the app schemes and return the unified claims principle. If no jwt, respond with a challenge to the jwt scheme.
  3. ForwardAuthenticate would be helpful if you wanted to explicitly handle the delegation of the authentication in schemes instead, i.e. where you mention having an assortment of ISP providers, RBAC or claims etc, you could implement logic that returns the right scheme via a Selector on a single authentication scheme(i.e. AppSchemeForwarder), and then register an actual authentication scheme for each of your ISPs that hopefully would give you exactly what you want, in terms of a ClaimsPrincipal, and the AppSchemeForwarder is just forwarding to the appropriate ISP (each of which could also have similar logic if needed to continue forwarding/combining)
maxandriani commented 2 years ago

Thanks to clarify @HaoK ! We choose the option 2 strategy. It worked like a charm.

ghost commented 2 years ago

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

devich2 commented 2 years ago

Hi, also met the same issue, RequireAuthenticated() method forces all authentication schemes to be tried. If i properly understood, that was designed to collect as much information about user as possible. But in our case we have different api clients, that are supposed to authenticate via different schemas, and it's a bit annoying to see a lot of error logs, when in fact user pas authentication.

maxandriani commented 2 years ago

@devich2 Hi! As far as I understood the thread and your use case, I think you should write you own proxy scheme and properly evaluate wich schema to challenge based some information about the origin of the request (client id registration table, …).

muj-beg commented 6 months ago

We're using multiple Azure AD and Azure AD B2C schemes in various applications. What we would like to do is create a generic policy scheme selector based on issuer matching. Ideally, instead of using a static list of issuers, we want to match the HTTP request's token issuer to a configured jwtOptions.ClaimsIssuer or jwtOptions.Configuration.Issuer. However, MSAL retrieves issuer information using a ConfigurationManager, and the configuration hasn't been loaded by the time ForwardDefaultSelector() method is called. We can try to manually load the configuration using jwtOptions.ConfigurationManager.GetConfigurationAsync() in order to retrieve the issuer value. However, that requires a blocking wait, since that's an async method, but ForwardDefaultSelector() isn't!

The other (minor) downside to the above approach is that the token had to be decoded twice -- once by the policy scheme handler, and then again by the real scheme handler.