57

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?}");
            });
        }
    }
}
ArunPratap
  • 4,816
  • 7
  • 25
  • 43
Jeff Putz
  • 14,504
  • 11
  • 41
  • 52
  • We registered our schemes following this example from the IdentityServer4 quickstarts: https://github.com/IdentityServer/IdentityServer4.Samples/blob/release/Quickstarts/4_ImplicitFlowAuthenticationWithExternal/src/QuickstartIdentityServer/Startup.cs – nurdyguy Aug 15 '17 at 15:32
  • That's not relevant to Core 2.0. – Jeff Putz Aug 15 '17 at 16:27

6 Answers6

83

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!

HotN
  • 4,216
  • 3
  • 40
  • 51
barbara.post
  • 1,581
  • 16
  • 27
  • 1
    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. – user1754675 Jul 13 '19 at 18:29
  • 1
    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. – Israel Garcia Oct 25 '19 at 19:35
  • 1
    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. – bech Dec 18 '19 at 12:26
  • 2
    Thanks @barbara.post! – John Reilly Mar 03 '20 at 19:01
  • 3
    Barbara, your technique is fine, even with the parameters. @john-reilly explains how, in his "Dual boot authentication with ASP.NET Core" blog post: https://blog.johnnyreilly.com/2020/03/dual-boot-authentication-with-aspnetcore.html – thargenediad Jul 10 '20 at 00:15
  • 1
    Props where they are due: my blog post was inspired by the answer provided by @barbara.post - thanks again! – John Reilly Jul 26 '20 at 16:03
  • 1
    I don't understand why this is not the way asp.net core authenthication works from scratch! – Emanuel Gianico Mar 28 '22 at 19:31
  • 1
    Thanks @barbara.post, this saved us after a sprint of spiking the issue when moving to ASP.net core – Ahmad Hajou Apr 17 '23 at 07:14
46

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();
        });

        // ...
    }
}
Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131
  • 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. – Jeff Putz Aug 16 '17 at 01:57
  • 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. – Kévin Chalet Aug 16 '17 at 02:07
  • 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. – Jeff Putz Aug 16 '17 at 02:16
  • 1
    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. – Kévin Chalet Aug 16 '17 at 11:23
  • 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 Aug 16 '17 at 12:26
  • 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. – Jeff Putz Aug 16 '17 at 12:28
  • 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 Aug 16 '17 at 12:51
  • 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")`). – Kévin Chalet Aug 16 '17 at 12:54
  • 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. – Jeff Putz Aug 16 '17 at 13:17
  • 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. – Kévin Chalet Aug 16 '17 at 13:20
  • 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: https://github.com/POPWorldMedia/POPForums/blob/6f0f6b8f7698839e230da6ba59b3b2b6f1afc5d1/src/PopForums.Mvc/Areas/Forums/Authorization/PopForumsUserAttribute.cs – Jeff Putz Aug 16 '17 at 13:43
  • 1
    Answer updated to include a custom middleware sample. – Kévin Chalet Aug 16 '17 at 13:56
  • 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? – norekhov Nov 08 '17 at 12:24
  • 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: https://oliviervaillancourt.com/posts/Fixing-IDX10501-MultipleAuthScheme – Pieter Feb 03 '22 at 16:37
7

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));
        })
    
3

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"])),
        };

    });
mdimai666
  • 699
  • 8
  • 14
  • IdentityConstants.ApplicationScheme was the scheme I was missing. Then I had to set my [Authorize(AuthenticationSchemes = "Identity.Application"] and it started to work. – Phil Huhn Mar 24 '22 at 18:46
2

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);
Vaccano
  • 78,325
  • 149
  • 468
  • 850
2
//******Startup=>ConfigureServices******

services.AddAuthentication(option =>
{
    option.DefaultScheme = "AdministratorAuth";
})
.AddCookie("AdministratorAuth", "AdministratorAuth", option =>
{
    option.Cookie.Name = "AdministratorAuth";
    option.LoginPath = new PathString("/AdminPanel/Login");
    option.ExpireTimeSpan = TimeSpan.FromMinutes(14400);
    option.AccessDeniedPath = "/Error/UnAuthorized";
    option.LogoutPath = "/Security/Logout";
})
.AddCookie("UsersAuth", "UsersAuth", option =>
{
    option.Cookie.Name = "UsersAuth";
    option.LoginPath = new PathString("/Security/LoginUser/");
    option.ExpireTimeSpan = TimeSpan.FromMinutes(144000);
    option.AccessDeniedPath = "/Error/UnAuthorized";
    option.LogoutPath = "/Security/LogoutUser";
});
    
//______________________________________________________________
    
//******Startup=> Configure******
    app.UseAuthentication();
    app.UseCookiePolicy();

//______________________________________________________________
    
//******Admin Login******
    var status = HttpContext.SignInAsync("AdministratorAuth", new ClaimsPrincipal(principal), properties)IsCompleted;
    
//******OtherUsers Login******
    var status = HttpContext.SignInAsync("UsersAuth", new ClaimsPrincipal(principal), properties)IsCompleted;
    
//______________________________________________________________
    
[Authorize(AuthenticationSchemes = "AdministratorAuth")]
public class DashboardController : BaseController
{

}