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

C#asp.netasp.net Mvcasp.net Coreasp.net Core-2.0

C# Problem Overview


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

C# Solutions


Solution 1 - C#

Edit of December 2019: please consider this answer before anything else: https://stackoverflow.com/questions/49694383/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: https://stackoverflow.com/questions/49694383/use-multiple-jwt-bearer-authentication. I really like this other approach!

Solution 2 - C#

> 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 3 - C#

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 - C#

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

Solution 5 - C#

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 6 - C#

//******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
{

}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJeff PutzView Question on Stackoverflow
Solution 1 - C#barbara.postView Answer on Stackoverflow
Solution 2 - C#Kévin ChaletView Answer on Stackoverflow
Solution 3 - C#Lakshmana Prabhu VenkatesanView Answer on Stackoverflow
Solution 4 - C#Phong NguyenView Answer on Stackoverflow
Solution 5 - C#mdimai666View Answer on Stackoverflow
Solution 6 - C#ahmad rahimiView Answer on Stackoverflow