In some use cases, you might want your endpoints to be authorized using multiple schemes. In my case, I had to allow some of the endpoints for authorized clients (using Identity Server) as well as for requests with a custom token which is generated by a custom service for authorized users.
To achieve this, we need to create the custom AuthenticationScheme and configure a policy to use our custom scheme as well as JwtBearer.
Custom AuthenticationScheme
Creating a custom authentication scheme will validate the custom token using the [Authorize] attribute.
To create a custom authentication scheme, we need to define the following,
- CustomAuthenticationDefaults
- CustomAuthenticationHandler
- CustomAuthenticationOptions
Let’s start with the defaults, where we describe the name of the scheme.
public static class CustomAuthenticationDefaults
{
    public const string AuthenticationScheme = "Custom";
}Next AuthenticationSchemeOptions,
using Microsoft.AspNetCore.Authentication;
public class CustomAuthOptions : AuthenticationSchemeOptions
{
    public string UserInfoEndpoint { get; set; }
}
To validate the custom token, I need to send an HTTP request to an endpoint and the URL for that endpoint needs to be configurable.
By defining AuthenticationSchemeOptions, we can pass these values while setting up the scheme in the Startup.
Let’s move on to AuthenticationHandler, which validates the token.
using Flurl.Http;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
...
public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthOptions>
{
    public CustomAuthenticationHandler(
        IOptionsMonitor<CustomAuthOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder,
        ISystemClock clock
        )
        : base(options, logger, encoder, clock)
    {
    }
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        if (!Request.Headers.ContainsKey("Authorization"))
            return AuthenticateResult.Fail("Unauthorized");
        string authorizationHeader = Request.Headers["Authorization"];
        if (string.IsNullOrEmpty(authorizationHeader))
        {
            return AuthenticateResult.NoResult();
        }
        if (!authorizationHeader.StartsWith(CustomAuthenticationDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))
        {
            return AuthenticateResult.Fail("Unauthorized");
        }
        string token = authorizationHeader.Substring(CustomAuthenticationDefaults.AuthenticationScheme.Length).Trim();
        if (string.IsNullOrEmpty(token))
        {
            return AuthenticateResult.Fail("Unauthorized");
        }
        try
        {
            return await ValidateTokenAsync(token);
        }
        catch (Exception ex)
        {
            return AuthenticateResult.Fail(ex.Message);
        }
    }
    private async Task<AuthenticateResult> ValidateTokenAsync(string session)
    {
        // getting user info using HTTP request made using Flurl
        var user = await Options.UserInfoEndpoint
            .WithHeader("some-id", session)
            .GetJsonAsync<User>();
        if (user == null)
        {
            return AuthenticateResult.Fail("Unauthorized");
        }
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, $"{user.Name} {user.Surname}"),
            new Claim(ClaimTypes.GivenName, $"{user.Name}"),
            new Claim(ClaimTypes.Surname, surname),
            new Claim("scope", "orders:write"),
            new Claim(ClaimTypes.NameIdentifier, user.id)
            new Claim(ClaimTypes.Role, "User")
        };
        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);
        return AuthenticateResult.Success(ticket);
    }
}In the AuthenticationHandler, you can use your way to validate your tokens.
Now we need to configure our project to use the custom authentication, for that, in the ConfigureServices of startup.cs,
services
    .AddCustomAuthentication(Configuration);public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
{
    // Identity Server Configuration
    var identityUrl = configuration.GetValue<string>("Authentication:IdentityServerBaseUrl");
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    }).AddJwtBearer(options =>
    {
        options.Authority = identityUrl;
        options.RequireHttpsMetadata = false;
        options.Audience = "your_api";
    });
    // Custom Authentication configuration
    services.AddAuthentication(CustomAuthenticationDefaults.AuthenticationScheme)
        .AddScheme<CustomAuthOptions, CustomAuthenticationHandler>(CustomAuthenticationDefaults.AuthenticationScheme,
        o => o.UserInfoEndpoint = configuration.GetValue<string>("Authentication:Custom:UserInfoEndpoint"));
    // we define policies here where we configure which scheme or combinations we need for each of our policies.
    services.AddAuthorization(options =>
    {
        // authorize using custom auth scheme only
        options.AddPolicy("UserRole", policy =>
        {
            policy.AuthenticationSchemes.Add(CustomAuthenticationDefaults.AuthenticationScheme);
            policy.RequireRole("User");
        });
        // authorize using custom auth scheme as well as identity server
        options.AddPolicy("OrdersWrite", policy =>
        {
            policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
            policy.AuthenticationSchemes.Add(CustomAuthenticationDefaults.AuthenticationScheme);
            policy.RequireClaim("scope", "orders:write");
        });
    });
    return services;
}
You can have different combinations in the policy defined above like based on scheme, claim, etc.
The configuration in the appsettings.json look like,
"Authentication": {
    "Custom": {
      "UserInfoEndpoint": "https://yourcustomauthwebsite.com/user-info-path"
    },
    "IdentityServerBaseUrl": "https://url-of-idserver"
}Done, let’s enable the multi authorization to our endpoint. In the controller action,
[Authorize(Policy = "OrdersWrite")]
public async Task<ActionResult<OrderResult>> CreateOrder(OrderRequest orderRequest)
{
    var clientIdClaim = HttpContext.User.FindFirst("client_id"); // identity server client
    var userIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier); // user authenticated using custom auth handler.
    ...
}Now we can invoke our create order endpoint with valid bearer token as well as with our custom token.
The general format for authorization header is,
Authorization: <type> <credentials>So bearer token request header looks like,
Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cAnd our custom token request header looks like,
Authorization: custom abcasdjasdjlaksdjlasjdlasjd








