In this article, we are going to learn how to use multiple authentication schemes in .NET.
Sometimes our web applications need to have multiple authentication methods. One example can be a Single Page Application (SPA) which uses cookie-based authentication to log in and JWT bearer authentication for the XHR requests. Another example can be an application that authenticates users from Azure Active Directory and users database.
To see how to achieve that, we are going to create an ASP.NET Web API project with multiple endpoints. An endpoint that accepts both cookies and tokens, an endpoint that accepts only tokens, an endpoint that accepts only cookies, and one that accepts only tokens with specific schemes.
Let’s start.
VIDEO: How to Use Multiple Authentication Schemes in .NET Applications.
How to Setup Multiple Authentication Schemes in .NET
To demonstrate how multiple schemes can work together, we are going to implement an API that uses cookie-based authentication with the default scheme and two JWT bearer authentications with two different schemes.
Let’s create a project in the Visual Studio with the ASP.NET Core Empty project template and then change the Program.cs
class:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication() .AddCookie(options => { options.LoginPath = "/auth/unauthorized"; options.AccessDeniedPath = "/auth/forbidden"; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateIssuerSigningKey = true, ValidIssuer = "https://localhost:7208/", ValidAudience = "https://localhost:7208/", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey@1")) }; }) .AddJwtBearer("SecondJwtScheme", options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateIssuerSigningKey = true, ValidIssuer = "https://localhost:7209/", ValidAudience = "https://localhost:7208/", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey@2")) }; });
We register authentication services required by authentication services with the AddAuthentication
method. Then we add cookie authentication to AuthenticationBuilder
using the default CookieAuthenticationDefaults.AuthenticationScheme
scheme. After that, we add two JWT bearer authentications using default JwtBearerDefaults.AuthenticationScheme
and SecondJwtScheme
schemes. They have different ValidIssuer
properties to simulate different issuers, just like we would if we were working with, e.g., Active Directory Federation Services or Azure Active Directory B2C.
To use authentication services in our application, we have to add authentication to the request pipeline:
app.UseAuthentication(); app.UseAuthorization();
Since we are going to implement our authentication partially for demonstration purposes, you can read how to do it properly in our JWT Authentication in ASP.NET Core Web API and Authentication With ASP.NET Core Identity articles.
Adding Endpoints That Will Use Multiple Authentication Schemes
We need some endpoints that will use different schemes later in the article. We are going to create three controllers: AuthController
, ApiController
, and ContentController
.
Let’s first create AuthController
:
[ApiController] [Route("[controller]")] public class AuthController : ControllerBase { }
Once we have our controller, let’s add a first POST action:
[HttpPost("loginDefaultJwt")] public IActionResult LoginDefaultJwt([FromBody] User user) { var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey@1")); var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); var tokenOptions = new JwtSecurityToken( issuer: "https://localhost:7208/", audience: "https://localhost:7208/", claims: new List<Claim>() { new Claim(ClaimTypes.Name, user.Username ?? string.Empty) }, expires: DateTime.Now.AddMinutes(30), signingCredentials: signinCredentials ); var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions); return Ok(new { Token = tokenString }); }
We implement this endpoint for demonstration purposes (we’ll do the same for all the other endpoints), and for simplicity, we don’t check the user’s existence or password. Of course, we encourage you to read how to implement them fully in the already mentioned articles.
Then we need another one for the login with the second JWT scheme:
[HttpPost("loginSecondJwt")] public IActionResult LoginSecondJwt([FromBody] User user) { var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey@2")); var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); var tokenOptions = new JwtSecurityToken( issuer: "https://localhost:7209/", audience: "https://localhost:7208/", claims: new List<Claim>() { new Claim(ClaimTypes.Name, user.Username ?? string.Empty) }, expires: DateTime.Now.AddMinutes(30), signingCredentials: signinCredentials ); var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions); return Ok(new { Token = tokenString }); }
Since we also want to support login with the cookie scheme, let’s add a third endpoint:
[HttpPost("loginCookie")] public async Task<IActionResult> LoginCookie([FromBody] User user) { var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme, ClaimTypes.Name, ClaimTypes.Role); identity.AddClaim(new Claim(ClaimTypes.Name, user.Username ?? string.Empty)); var principal = new ClaimsPrincipal(identity); await HttpContext.SignInAsync( CookieAuthenticationDefaults.AuthenticationScheme, principal, new AuthenticationProperties { IsPersistent = true, AllowRefresh = true, ExpiresUtc = DateTime.UtcNow.AddDays(1) }); return Ok(); }
Finally, let’s add endpoints for the unauthorized and forbidden requests that we set in the AddCookie
method:
[HttpGet("unauthorized")] public IActionResult GetUnauthorized() { return Unauthorized(); } [HttpGet("forbidden")] public IActionResult GetForbidden() { return Forbid(); }
This is the implementation of the User
class that the first three endpoints accept:
public class User { public string? Username { get; set; } }
Now, after the AuthController
implementation, let’s implement ApiController
:
[ApiController] [Route("[controller]")] public class ApiController : ControllerBase { [HttpGet("getWithAny")] public IActionResult GetWithAny() { return Ok(new { Message = $"Hello to Code Maze {GetUsername()}"}); } [HttpGet("getWithSecondJwt")] public IActionResult GetWithSecondJwt() { return Ok(new { Message = $"Hello to Code Maze {GetUsername()}" }); } private string? GetUsername() { return HttpContext.User.Claims .Where(x => x.Type == ClaimTypes.Name) .Select(x => x.Value) .FirstOrDefault(); } }
At this moment, we allow anyone to access endpoints. Later on in the article, we will use authorization with different authentication schemes for our endpoints.
Finally, let’s implement ContentController
:
[ApiController] [Route("[controller]")] public class ContentController : ControllerBase { [HttpGet("getWithCookie")] public IActionResult GetWithCookie() { var userName = HttpContext.User.Claims .Where(x => x.Type == ClaimTypes.Name) .Select(x => x.Value) .FirstOrDefault(); return Content($"<p>Hello to Code Maze {userName}</p>"); } }
The idea for the ContentController
is to (later) authorize only cookie-based requests.
How to Specify Authentication Scheme
Now that we have all example endpoints in place, let’s add authorization that will authorize against all three authentication schemes at the same time.
Using Multiple Authentication Schemes at the Same Time
Since we want that /api/getWithAny
endpoint works with any scheme; let’s first add [Authorize]
attribute to it:
[Authorize] [HttpGet("getWithAny")] public IActionResult GetWithAny() { return Ok(new { Message = $"Hello to Code Maze {GetUsername()}"}); }
If we use [Authorize]
attribute without any parameters, it is going to use the default authorization policy. Let’s define it in the Program.cs
:
builder.Services.AddAuthorization(options => { var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder( JwtBearerDefaults.AuthenticationScheme, CookieAuthenticationDefaults.AuthenticationScheme, "SecondJwtScheme"); defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser(); options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build(); });
We define that the default policy is going to use all three authentication schemes.
Let’s test what we have at this moment. We are going to send a POST request to /auth/loginDefaultJwt
via Postman with the body:
{ "Username": "Author with default jwt scheme" }
And one more POST request to /auth/loginSecondJwt
:
{ "Username": "Author with second jwt scheme" }
As a result, we should get two tokens, each per POST request.
After that, to get a cookie, we are going to send a POST request to /auth/loginCookie
:
{ "Username": "Author with cookie scheme" }
Postman saves the cookie for later usage.
Now that we have tokens and the cookie, let’s send some GET requests to the /api/getWithAny
endpoint.
This is the response we get after sending a request with the cookie:
{ "message": "Hello to Code Maze Author with cookie scheme" }
After we remove the cookie from the Postman and send a GET request with the first token in the Authorization header, we receive the response:
{ "message": "Hello to Code Maze Author with default jwt scheme" }
And finally, the response with the second token to the same endpoint:
{ "message": "Hello to Code Maze Author with second jwt scheme" }
Excellent, we have an endpoint that works with any token and a cookie. But what if we want to specify that /api/getWithSecondJwt
endpoint only accepts SecondJwtScheme
scheme. Along with that, we wish that /content/getWithCookie
only works with CookieAuthenticationDefaults.AuthenticationScheme
. With all of that, we need the /api/getWithAny
to continue to work as before.
Lucky for us, we can achieve desired behavior in multiple ways.
How to Specify Authorization Policy to Work With One Authentication Scheme
To be able to specify the authorization policy, let’s add two policies:
builder.Services.AddAuthorization(options => { var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder( JwtBearerDefaults.AuthenticationScheme, CookieAuthenticationDefaults.AuthenticationScheme, "SecondJwtScheme"); defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser(); options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build(); var onlySecondJwtSchemePolicyBuilder = new AuthorizationPolicyBuilder("SecondJwtScheme"); options.AddPolicy("OnlySecondJwtScheme", onlySecondJwtSchemePolicyBuilder .RequireAuthenticatedUser() .Build()); var onlyCookieSchemePolicyBuilder = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme); options.AddPolicy("OnlyCookieScheme", onlyCookieSchemePolicyBuilder .RequireAuthenticatedUser() .Build()); });
This allows us to specify the policy in the [Authorize]
attribute. Since we want that the endpoint /api/getWithSecondJwt
works only with SecondJwtScheme
scheme, let’s add the OnlySecondJwtScheme
policy to its [Authorize]
attribute:
[Authorize(Policy = "OnlySecondJwtScheme")] [HttpGet("getWithSecondJwt")] public IActionResult GetWithSecondJwt() { return Ok(new { Message = $"Hello to Code Maze {GetUsername()}" }); }
Similarly, let’s add the OnlyCookieScheme
policy to /content/getWithCookie
endpoint:
[Authorize(Policy = "OnlyCookieScheme")] [HttpGet("getWithCookie")] public IActionResult GetWithCookie() { var userName = HttpContext.User.Claims .Where(x => x.Type == ClaimTypes.Name) .Select(x => x.Value) .FirstOrDefault(); return Content($"<p>Hello to Code Maze {userName}</p>"); }
Now, if we send the first token to the /api/getWithSecondJwt
endpoint, we will get a 401 status response. Yet, with the second token, we get the expected response:
{ "message": "Hello to Code Maze Author with second jwt scheme" }
When we send a GET request without the cookie to the /content/getWithCookie
we are going to get a 401 status response. But with the cookie, we get:
<p>Hello to Code Maze Author with cookie scheme</p>
How to Specify Only One Authentication Scheme
Let’s achieve the same behavior by specifying the AuthenticationScheme
in the [Authorize]
attribute.
First, we need to change Authorize
attribute on the endpoint:
[Authorize(AuthenticationSchemes = "SecondJwtScheme")] [HttpGet("getWithSecondJwt")] public IActionResult GetWithSecondJwt() { return Ok(new { Message = $"Hello to Code Maze {GetUsername()}" }); }
After that, we need to specify the DefaultScheme
and DefaultChallengeScheme
properties in the AddAuthentication
options argument and add a policy scheme for it:
builder.Services.AddAuthentication(options => { options.DefaultScheme = "MultiAuthSchemes"; options.DefaultChallengeScheme = "MultiAuthSchemes"; }) .AddCookie(...) .AddJwtBearer(...) .AddJwtBearer(...) .AddPolicyScheme("MultiAuthSchemes", JwtBearerDefaults.AuthenticationScheme, options => { options.ForwardDefaultSelector = context => { string authorization = context.Request.Headers[HeaderNames.Authorization]; if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer ")) { var token = authorization.Substring("Bearer ".Length).Trim(); var jwtHandler = new JwtSecurityTokenHandler(); return (jwtHandler.CanReadToken(token) && jwtHandler.ReadJwtToken(token).Issuer.Equals("https://localhost:7208/")) ? JwtBearerDefaults.AuthenticationScheme : "SecondJwtScheme"; } return CookieAuthenticationDefaults.AuthenticationScheme; }; });
After sending some requests to the /api/getWithSecondJwt
endpoint, we find out that AuthenticationSchemes
property is not respected. It turns out that at this moment [Authorize(AuthenticationSchemes = "SecondJwtScheme")]
acts the same as [Authorize]
. That’s because we still have the default policy defined in the AddAuthorization
call, and the default policy is used instead of the default scheme.
Let’s comment out the default policy part in the AddAuthorization
call:
builder.Services.AddAuthorization(options => { //var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder( // JwtBearerDefaults.AuthenticationScheme, // CookieAuthenticationDefaults.AuthenticationScheme, // "SecondJwtScheme"); //defaultAuthorizationPolicyBuilder = // defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser(); //options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build(); var onlySecondJwtSchemePolicyBuilder = new AuthorizationPolicyBuilder("SecondJwtScheme"); options.AddPolicy("OnlySecondJwtScheme", onlySecondJwtSchemePolicyBuilder .RequireAuthenticatedUser() .Build()); var onlyCookieSchemePolicyBuilder = new AuthorizationPolicyBuilder(CookieAuthenticationDefaults.AuthenticationScheme); options.AddPolicy("OnlyCookieScheme", onlyCookieSchemePolicyBuilder .RequireAuthenticatedUser() .Build()); });
Now the endpoint /api/getWithSecondJwt
with the specified scheme works as expected, as well as /content/getWithCookie
with the specified policy.
Conclusion
In this article, we learned how to use multiple authentication schemes when adding authorization to our endpoints. We saw that we could have one endpoint that uses multiple schemes and the other that uses only one scheme at the same time. Also, we achieved that in two different ways: by specifying policies and specifying schemes.