How do I setup multiple auth schemes in ASP.NET Core 2.0?

63,988

Solution 1

Navigating these changes has been difficult, but I'm guessing that I'm doing .AddScheme wrong.

Don't use the AddScheme: it's a low-level method designed for handlers writers.

How do I setup multiple auth schemes in ASP.NET Core 2.0?

To register the cookies handler, simply do:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "myauth1";
        })

       .AddCookie("myauth1");
       .AddCookie("myauth2");
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseAuthentication();

        // ...
    }
}

It's important to note that you can't register multiple default schemes like you could in 1.x (the whole point of this huge refactoring is to avoid having multiple automatic authentication middleware at the same time).

If you absolutely need to emulate this behavior in 2.0, you can write a custom middleware that manually calls AuthenticateAsync() and creates a ClaimsPrincipal containing all the identities you need:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "myauth1";
        })

       .AddCookie("myauth1");
       .AddCookie("myauth2");
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseAuthentication();

        app.Use(async (context, next) =>
        {
            var principal = new ClaimsPrincipal();

            var result1 = await context.AuthenticateAsync("myauth1");
            if (result1?.Principal != null)
            {
                principal.AddIdentities(result1.Principal.Identities);
            }

            var result2 = await context.AuthenticateAsync("myauth2");
            if (result2?.Principal != null)
            {
                principal.AddIdentities(result2.Principal.Identities);
            }

            context.User = principal;

            await next();
        });

        // ...
    }
}

Solution 2

Edit of December 2019: please consider this answer before anything else: Use multiple JWT Bearer Authentication

My old answer (that does not fit using multiple JWT but only JWT + API key, as a user commented):

Another possibility is to determine at runtime which authentication policy scheme to choose, I had the case where I could have an http authentication bearer token header or a cookie.

So, thanks to https://github.com/aspnet/Security/issues/1469

JWT token if any in request header, then OpenIdConnect (Azure AD) or anything else.

public void ConfigureServices(IServiceCollection services)
    {
        // Add CORS
        services.AddCors();

        // Add authentication before adding MVC
        // Add JWT and Azure AD (that uses OpenIdConnect) and cookies.
        // Use a smart policy scheme to choose the correct authentication scheme at runtime
        services
            .AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultScheme = "smart";
                sharedOptions.DefaultChallengeScheme = "smart";
            })
            .AddPolicyScheme("smart", "Authorization Bearer or OIDC", options =>
            {
                options.ForwardDefaultSelector = context =>
                {
                    var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
                    if (authHeader?.StartsWith("Bearer ") == true)
                    {
                        return JwtBearerDefaults.AuthenticationScheme;
                    }
                    return OpenIdConnectDefaults.AuthenticationScheme;
                };
            })
            .AddJwtBearer(o =>
            {
                o.Authority = Configuration["JWT:Authentication:Authority"];
                o.Audience = Configuration["JWT:Authentication:ClientId"];
                o.SaveToken = true;
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddAzureAd(options => Configuration.Bind("AzureAd", options));

        services
            .AddMvc(config =>
            {
                var policy = new AuthorizationPolicyBuilder()
                                 .RequireAuthenticatedUser()
                                 .Build();
                // Authentication is required by default
                config.Filters.Add(new AuthorizeFilter(policy));
                config.RespectBrowserAcceptHeader = true;
            });
            
            ...
            
            }

Edit of 07/2019: I must add a link to the following proposal, because it's very helpful too: you may not use parameters in AddAuthentication() as I did, because this would setup a default scheme. Everything is well explained here: Use multiple JWT Bearer Authentication. I really like this other approach!

Solution 3

https://stackoverflow.com/a/51897159/4425154's solution helps. Couple of items to consider on top the solution mentioned,

  1. Make sure you are using .net core run-time 2.1 or above
  2. Make sure you an authorization policy as mentioned below if you are using middleware

       services.AddMvc(options =>
        {
            var defaultPolicy = new AuthorizationPolicyBuilder(new[] { CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme })
                      .RequireAuthenticatedUser()
                      .Build();
            options.Filters.Add(new AuthorizeFilter(defaultPolicy));
        })
    

Solution 4

Extend @HotN solution If used Blazor server with AddDefaultIdentity and Blazor Wasm JwtBearer

    services.AddAuthentication(opt =>
    {
        opt.DefaultAuthenticateScheme = "smart";
        opt.DefaultChallengeScheme = "smart";
    })
    .AddPolicyScheme("smart", "Authorization Bearer or OIDC", options =>
    {
        options.ForwardDefaultSelector = context =>
        {
            var authHeader = context.Request.Headers["Authorization"].FirstOrDefault();
            if (authHeader?.ToLower().StartsWith("bearer ") == true)
            {
                return JwtBearerDefaults.AuthenticationScheme;
            }
            return IdentityConstants.ApplicationScheme;
        };
    })
    .AddCookie(cfg => cfg.SlidingExpiration = true)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new()
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,

            ValidIssuer = jwtSettings["ValidIssuer"],
            ValidAudience = jwtSettings["ValidAudience"],
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["securityKey"])),
        };

    });

Solution 5

In case someone needs the solution, this is what I have done:

services.AddMvc(options =>
{
            
     var defaultPolicy = new AuthorizationPolicyBuilder().AddAuthenticationSchemes(IdentityServerAuthenticationDefaults.AuthenticationScheme, BasicAuthenticationDefaults.AuthenticationScheme)
         .RequireAuthenticatedUser()
         .Build();

      options.Filters.Add(new AuthorizeFilter(defaultPolicy));
});

services.AddAuthentication()
    .AddIdentityServerAuthentication(option config here)
    .AddBasicAuthentication(setting);
Share:
63,988
Jeff Putz
Author by

Jeff Putz

Worked at Microsoft with data on the Azure platform, also in the group handling MSDN/TechNet. Maintain the open source POP Forums project: https://github.com/POPWorldMedia/POPForums Career stage: More manager than maker, teams 12 to 40+.

Updated on July 09, 2022

Comments

  • Jeff Putz
    Jeff Putz almost 2 years

    I'm trying to migrate my auth stuff to Core 2.0 and having an issue using my own authentication scheme. My service setup in startup looks like this:

    var authenticationBuilder = services.AddAuthentication(options =>
    {
        options.AddScheme("myauth", builder =>
        {
            builder.HandlerType = typeof(CookieAuthenticationHandler);
        });
    })
        .AddCookie();
    

    My login code in the controller looks like this:

    var claims = new List<Claim>
    {
        new Claim(ClaimTypes.Name, user.Name)
    };
    
    var props = new AuthenticationProperties
    {
        IsPersistent = persistCookie,
        ExpiresUtc = DateTime.UtcNow.AddYears(1)
    };
    
    var id = new ClaimsIdentity(claims);
    await HttpContext.SignInAsync("myauth", new ClaimsPrincipal(id), props);
    

    But when I'm in a controller or action filter, I only have one identity, and it's not an authenticated one:

    var identity = context.HttpContext.User.Identities.SingleOrDefault(x => x.AuthenticationType == "myauth");
    

    Navigating these changes has been difficult, but I'm guessing that I'm doing .AddScheme wrong. Any suggestions?

    EDIT: Here's (essentially) a clean app that results not in two sets of Identities on User.Identies:

    namespace WebApplication1.Controllers
    {
        public class Testy : Controller
        {
            public IActionResult Index()
            {
                var i = HttpContext.User.Identities;
                return Content("index");
            }
    
            public async Task<IActionResult> In1()
            {
                var claims = new List<Claim> { new Claim(ClaimTypes.Name, "In1 name") };
                var props = new AuthenticationProperties  { IsPersistent = true, ExpiresUtc = DateTime.UtcNow.AddYears(1) };
                var id = new ClaimsIdentity(claims);
                await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(id), props);
                return Content("In1");
            }
    
            public async Task<IActionResult> In2()
            {
                var claims = new List<Claim> { new Claim(ClaimTypes.Name, "a2 name") };
                var props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTime.UtcNow.AddYears(1) };
                var id = new ClaimsIdentity(claims);
                await HttpContext.SignInAsync("a2", new ClaimsPrincipal(id), props);
                return Content("In2");
            }
    
            public async Task<IActionResult> Out1()
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                return Content("Out1");
            }
    
            public async Task<IActionResult> Out2()
            {
                await HttpContext.SignOutAsync("a2");
                return Content("Out2");
            }
        }
    }
    

    And Startup:

    namespace WebApplication1
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }
    
            public IConfiguration Configuration { get; }
    
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddAuthentication(options =>
                {
                    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    })
                    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
                    .AddCookie("a2");
    
                services.AddMvc();
            }
    
            public void Configure(IApplicationBuilder app, IHostingEnvironment env)
            {
                app.UseAuthentication();
    
                app.UseMvc(routes =>
                {
                    routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
                });
            }
        }
    }
    
  • Jeff Putz
    Jeff Putz over 6 years
    I set up a clean project and used the code above, then created methods to sign in (similar to the code in my question), and it did not work. There is only one identity on User.Identities, and it's the one associated with the default. Also, strangely, its IsAuthenticated property is false.
  • Kévin Chalet
    Kévin Chalet over 6 years
    It's hard to say what's wrong without seeing the complete app. Consider updating your question to include the detailed ASP.NET Core logs. It would surely help.
  • Jeff Putz
    Jeff Putz over 6 years
    I added code above... that's quite literally the whole app. If I debug, and hit /testy/in1 then /testy/in2, then go back to /testy and put a breakpoint on the first line of Index(), there is only one identity. Not sure what logs you want to see.
  • Kévin Chalet
    Kévin Chalet over 6 years
    As I said, this is now the expected behavior in 2.0, where you can only have one default handler per app (and thus, one identity per request). Consider decorating your actions with [Authorize(AuthenticationSchemes = "myauth1")] or [Authorize(AuthenticationSchemes = "myauth2")] to pick the right identity per action.
  • Jeff Putz
    Jeff Putz over 6 years
    That won't work... the identity isn't there. If you could only have one identity per request, why would they retain the Identities property on User?
  • Jeff Putz
    Jeff Putz over 6 years
    Come to think of it, if you could only have one identity, I don't think the social logins would work, as they typically had their claims on their own identity.
  • Kévin Chalet
    Kévin Chalet over 6 years
    I tested your snippet and it works fine on my machine after fixing your In1/In2 actions: replace new ClaimsIdentity(claims) by new ClaimsIdentity(claims, "auth type") to ensure the IsAuthenticated property is not left to false.
  • Kévin Chalet
    Kévin Chalet over 6 years
    Concerning ClaimsPrincipal.Identities, it's a BCL type. It can't be changed just because the ASP.NET team decided that the built-in security stuff would no longer set multiple principals/identities per request. In both 1.x and 2.0 social providers use cookies handlers that are not configured to populate HttpContext.User so it's not a problem (the claims are retrieved via HttpContext.AuthenticateAsync("social provider scheme")).
  • Jeff Putz
    Jeff Putz over 6 years
    Weird that they would kind of disregard the base implementation. If this is really broken, that's unfortunate. If you can only have one identity, then it makes the whole stack of defining a default scheme, or any schemes at all, kind of strange.
  • Kévin Chalet
    Kévin Chalet over 6 years
    Strange but much less prone to bugs than the old stack, where multiple authentication middleware were able to handle the same request. If for some reasons, the [Authorize(AuthenticationSchemes = "myauth1")] approach doesn't work for you (if so, please explain why), I can edit my answer to include a custom middleware "emulating" the old behavior using multiple HttpContext.AuthenticateAsync("scheme") calls.
  • Jeff Putz
    Jeff Putz over 6 years
    In my case it's not ideal because I'm not looking to bind the auth just to controller actions, which is why I'm looking at identities from a global action filter. In my use case, I'm then deferring to some logic to cache the user data for the rest of the request and record some telemetry related to the user. Line 49 of this file is similar to what I need in a work project: github.com/POPWorldMedia/POPForums/blob/…
  • Kévin Chalet
    Kévin Chalet over 6 years
    Answer updated to include a custom middleware sample.
  • norekhov
    norekhov over 6 years
    Am I get it right that currently when I access HttpContext.User.Identity.Name in Middleware I ALWAYS get default authentication scheme user if any? But when I access it in MVC I will get user of a corresponding scheme which allowed me access?
  • user1754675
    user1754675 almost 5 years
    I tried above code but getting error Process is terminating due to StackOverflowException (DotNet Core 2.2). I would like to use JWT if token is provided else openid connect.
  • Israel Garcia
    Israel Garcia over 4 years
    This approach is not bad, what happen is that both authentication mechanism are triggered always, in fact the url you cited face this in a better way, actually I couldn't find a way to selectively authenticate by specifying the Scheme in Authorize attribute, seems to be ignored, the only way I achieved it is by specifying a policy in [Authorize] and in the policy definition is where a specify the Scheme.
  • bech
    bech over 4 years
    Are you positive that both handlers are run with this configuration? I've tried implementing the above and only the relevant handler runs in my case. On the flipside, the linked solution runs both every time. Odd. Maybe it's because they're both JWT, whereas I used it to solve the issue of using either Bearer or API key.
  • John Reilly
    John Reilly about 4 years
    Thanks @barbara.post!
  • thargenediad
    thargenediad almost 4 years
    Barbara, your technique is fine, even with the parameters. @john-reilly explains how, in his "Dual boot authentication with ASP.NET Core" blog post: blog.johnnyreilly.com/2020/03/…
  • John Reilly
    John Reilly almost 4 years
    Props where they are due: my blog post was inspired by the answer provided by @barbara.post - thanks again!
  • Teoman shipahi
    Teoman shipahi over 2 years
    What a complex way to setup on dotnet core. Gosh.. Thanks for the answer.
  • Pieter
    Pieter over 2 years
    Absolutely nothing wrong with .AddScheme it gives you the possibility to override the HandleAuthentication(), also multiple schemes are perfectly possible without all the code you are presenting... cfr: oliviervaillancourt.com/posts/…
  • Phil Huhn
    Phil Huhn about 2 years
    IdentityConstants.ApplicationScheme was the scheme I was missing. Then I had to set my [Authorize(AuthenticationSchemes = "Identity.Application"] and it started to work.
  • Emanuel Gianico
    Emanuel Gianico about 2 years
    I don't understand why this is not the way asp.net core authenthication works from scratch!