C#

How to Use Multiple Authentication Schemes in .NET

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 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.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

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.

Domagoj Beti

View Comments

  • Good material. I just lost a weekend trying to get similar thing to work. I have OpenIdConnect as the main method, but I wanted to add a shared secret "backdoor" method. A few times it almost worked. I used various combinations of things you showed above.

    What I missed in ms docs is a proper description of logic behind all this gear like schemes, options (per scheme and global), what is the intended use cases for all these "ForwardSignIn" or "ForwardDefaultSelector". What calls "HandleAuthenticationAsync" and what calls "HandleChallengeAsync" and how the destination schema is chosen? Can this make AuthenticationService to call several schemes until successful authentication? I think it cannot, but I am not sure. Maybe something can be subclassed e.g. AuthorizationService, but where are examples of doing this? ASPNetCore sources on GitHub can only tell you that if you have a lot of time.

Share
Published by
Domagoj Beti

Recent Posts

Code Maze Weekly #134

Issue #134 of the Code Maze weekly. Check out what's new this week and enjoy…

Updated Date Aug 12, 2022

Heap Sort in C#

In this article, we'll look at the heap sort algorithm in C#. We'll discuss what…

Updated Date Aug 11, 2022

Flags Attribute For Enum in C#

In this article, we are going to learn about the Flags attribute for enum in…

Aug 10, 2022

Code Maze Weekly #133

Issue #133 of the Code Maze weekly. Check out what's new this week and enjoy…

Updated Date Aug 5, 2022

Type Checking and Type Casting in C#

In this article, we are going to learn various ways of converting a value from…

Updated Date Aug 5, 2022

Sort Dictionary by Value in .NET

In this article, we are going to learn how to sort the values in the…

Updated Date Aug 4, 2022