aspnet / Security

[Archived] Middleware for security and authorization of web apps. Project moved to https://github.com/aspnet/AspNetCore
Apache License 2.0
1.27k stars 599 forks source link

Support for multiple authorities for the same JWT bearer scheme #1847

Closed blowdart closed 6 years ago

blowdart commented 6 years ago

From @jimmy3912msncom on August 24, 2018 16:8

We recently started moving our applications from our legacy ASP.NET owin web hosted web API to ASP.NET Core. Our application supported JWT Bearer authentication for various clouds: the Azure global cloud, Azure China cloud, the Azure German cloud, and the Azure US government cloud.

The way we did this was having multiple configurations for each cloud and calling foreach (var tokenConfiguration in this.config.TokenAuthenticationConfigurations) { var aadTokenOptions = new WindowsAzureActiveDirectoryBearerAuthenticationOptions() { Tenant = tokenConfiguration.TenantId, MetadataAddress = $"{tokenConfiguration.OpenIdConnectDiscoveryMetadataEndpoint}/{tokenConfiguration.TenantId}/{tokenConfiguration.FederationMetadataUrlSuffix}", TokenHandler = new JwtSecurityTokenHandler(), TokenValidationParameters = new TokenValidationParameters() { ValidAudiences = tokenConfiguration.AadTokenValidAudiences, ValidateIssuer = false, // can't set to true due to multi-tenant access enabled apps and different metadata address for each ValidateAudience = tokenConfiguration.ValidateAudience, ValidateIssuerSigningKey = tokenConfiguration.ValidateIssuerSigningKey, ValidateLifetime = tokenConfiguration.ValidateLifetime } }; app.UseWindowsAzureActiveDirectoryBearerAuthentication(aadTokenOptions); }

By doing this we could have the same authentication scheme support any of our users from any cloud since the authority to use when making open id connect metadata discovery calls were different for each cloud (and the corresponding metadata itself was different e.g. signing keys, etc...).

This way all of our callers can use the same authorization bearer scheme when calling our APIs, regardless of cloud.

With ASP.NET Core 2.1, I tried to do this as well as below:

var authenticationConfigurations = this.configuration.GetSection("AuthenticationConfigurations").Get<IEnumerable>(); var authenticationBuilder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme); foreach (var authenticationConfiguration in authenticationConfigurations) { authenticationBuilder.AddJwtBearer(options => { options.Authority = authenticationConfiguration.Authority; options.TokenValidationParameters = new TokenValidationParameters { RequireExpirationTime = authenticationConfiguration.RequireExpirationTime, ValidateLifetime = authenticationConfiguration.ValidateLifetime, ValidateIssuer = authenticationConfiguration.ValidateIssuer, ValidateAudience = authenticationConfiguration.ValidateAudience, ValidAudiences = authenticationConfiguration.ValidAudiences }; }); }

but it fails with: System.InvalidOperationException: 'Scheme already exists: Bearer' at Microsoft.AspNetCore.Authentication.AuthenticationOptions.AddScheme(String name, Action1 configureBuilder) at Microsoft.AspNetCore.Authentication.AuthenticationBuilder.<>c__DisplayClass4_02.b0(AuthenticationOptions o) at Microsoft.Extensions.Options.ConfigureNamedOptions1.Configure(String name, TOptions options) at Microsoft.Extensions.Options.OptionsFactory1.Create(String name) at Microsoft.Extensions.Options.OptionsManager`1.<>cDisplayClass5_0.b0() at System.Lazy1.ViaFactory(LazyThreadSafetyMode mode) at System.Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor) at System.Lazy1.CreateValue() at Microsoft.Extensions.Options.OptionsCache1.GetOrAdd(String name, Func1 createOptions) at Microsoft.Extensions.Options.OptionsManager1.Get(String name) at Microsoft.Extensions.Options.OptionsManager1.get_Value() at Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider..ctor(IOptions1 options, IDictionary2 schemes) at Microsoft.AspNetCore.Authentication.AuthenticationSchemeProvider..ctor(IOptions1 options) --- End of stack trace from previous location where exception was thrown --- at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitSingleton(SingletonCallSite singletonCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass1_0.b0(ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType) at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType) at Microsoft.Extensions.Internal.ActivatorUtilities.ConstructorMatcher.CreateInstance(IServiceProvider provider) at Microsoft.Extensions.Internal.ActivatorUtilities.CreateInstance(IServiceProvider provider, Type instanceType, Object[] parameters) at Microsoft.AspNetCore.Builder.UseMiddlewareExtensions.<>c__DisplayClass4_0.b__0(RequestDelegate next) at Microsoft.AspNetCore.Builder.Internal.ApplicationBuilder.Build() at Microsoft.AspNetCore.Hosting.Internal.WebHost.BuildApplication() at Microsoft.AspNetCore.Hosting.Internal.WebHost.StartAsync(CancellationToken cancellationToken) at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token, String shutdownMessage) at Microsoft.AspNetCore.Hosting.WebHostExtensions.RunAsync(IWebHost host, CancellationToken token) at Microsoft.AspNetCore.Hosting.WebHostExtensions.Run(IWebHost host)

Is there a way to enable this? I want to avoid having to make our users change call patterns if it is unnecessary to.

Microsoft.NETCore.App version 2.1.0 Microsoft.AspNetCore.All 2.1.2

Copied from original issue: aspnet/Home#3455

Tratcher commented 6 years ago

"Bearer" as specified in the request headers is independent of "Bearer" used internally to identify different authentication providers. Start by calling the overload of AddJwtBearer that lets you specify a provider name: https://github.com/aspnet/Security/blob/beaa2b443d46ef8adaf5c2a89eb475e1893037c2/src/Microsoft.AspNetCore.Authentication.JwtBearer/JwtBearerExtensions.cs#L20

The next challenge you'll run into is deciding which to run on a given request. The Microsoft.Owin stack would try every Jwt provider on every request, but in AspNetCore the design is based around only running the one you need to. How do you distinguish which credentials you expect for a request?

jimmy3912msncom commented 6 years ago

@Tratcher When you say provider name I assume you mean authentication scheme. I've tried calling the overload you mentioned before but since each config option ties to the same scheme it fails with the same error: System.InvalidOperationException: 'Scheme already exists: Bearer' e.g.

        var authenticationConfiguration = this.configuration.GetSection("AuthenticationConfiguration").Get<AuthenticationConfiguration>();
        services.AddAuthentication()
            .AddJwtBearer(
                "Bearer",
                options =>
                {
                    options.Authority = "https://login.microsoftonline.de/common";
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        RequireExpirationTime = authenticationConfiguration.RequireExpirationTime,
                        ValidateLifetime = authenticationConfiguration.ValidateLifetime,
                        ValidateIssuer = authenticationConfiguration.ValidateIssuer,
                        ValidateAudience = authenticationConfiguration.ValidateAudience,
                        ValidAudiences = authenticationConfiguration.ValidAudiences
                    };
                })
            .AddJwtBearer(
                "Bearer",
                options =>
                {
                    options.Authority = "https://login.microsoftonline.com/common";
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        RequireExpirationTime = authenticationConfiguration.RequireExpirationTime,
                        ValidateLifetime = authenticationConfiguration.ValidateLifetime,
                        ValidateIssuer = authenticationConfiguration.ValidateIssuer,
                        ValidateAudience = authenticationConfiguration.ValidateAudience,
                        ValidAudiences = authenticationConfiguration.ValidAudiences
                    };
                });

The only way I could get this to work was to provide different schemes.

For our service we support both app and app+user token requests and as a multi-tenant application supporting multiple clouds our consumers can provide us with their bearer auth credentials and we only expect this layer to do the basic authN while authZ is done separately. We support any credentials that meet any of our authentication configurations to identify the caller but we may block requests who aren't authorized to perform certain actions and this worked well for us using the owin stack as you mentioned.

To your question we don't really distinguish between credentials we expect for a request. We just clients to come in with a token where we can validate with any one of our configured authorities whether it is the Azure global cloud, Azure China cloud, the Azure German cloud, or the Azure US government cloud.

Tratcher commented 6 years ago
.AddJwtBearer(
                "Bearer",

This does not change the expected header value, it's only an internal unique identifier. Change it to "BearerDe" and "BearerCom" for example.

As for authenticating any given provider, you'd have to have an additional middleware that looped through the available providers and called Authenticate for each of them, or at least until it got a hit.

jimmy3912msncom commented 6 years ago

@Tratcher If I understand what you are saying correctly, out of box there isn't support to do this so I will need to introduce custom logic to do what I want?

Tratcher commented 6 years ago

To authenticate multiple providers? No, but it's a common question and there are several examples in this issue tracker. I'll see if I can find one.

jimmy3912msncom commented 6 years ago

@Tratcher Thanks. It would be great if you can share 👍

Tratcher commented 6 years ago

See https://github.com/aspnet/Security/issues/1708#issuecomment-376567491

ggirard07 commented 6 years ago

@Tratcher here a use case where you could have 2 providers relying in fact on the same Azure AD B2C https://stackoverflow.com/questions/35072371/headless-authentication-azure-ad-b2c/49036907#49036907

Tratcher commented 6 years ago

Comments on closed issues are not tracked, please open a new issue with the details for your scenario.