aspnet-core

ASP.NET Core - Using Mutliple Authentication Schemes

One endpoint, authorize using Identity Server or using a custom authentication handler.

Abhith Rajan
Abhith RajanJuly 11, 2020 · 4 min read · Last Updated:

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_adQssw5c

And our custom token request header looks like,

Authorization: custom abcasdjasdjlaksdjlasjdlasjd

Additional Resources

This page is open source. Noticed a typo? Or something unclear?
Improve this page on GitHub


Abhith Rajan

Written byAbhith Rajan
Abhith Rajan is a software engineer by day and a full-stack developer by night. He's coding for almost a decade now. He codes 🧑‍💻, write ✍️, learn 📖 and advocate 👍.
Connect

Is this page helpful?

Related ArticlesView All

Related VideosView All

Why I DON'T use MediatR in ASP.NET Core

Don't Use AutoMapper in C#! Do THIS Instead!

Exploring Source Generation for Logging

Related StoriesView All

Related Tools & ServicesView All

flurl.dev

Flurl

Flurl is a modern, fluent, asynchronous, testable, portable, buzzword-laden URL builder and HTTP client library for .NET.