aspnet-core

ASP.NET Core - Using Mutliple Authentication Schemes

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

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.

1public static class CustomAuthenticationDefaults
2{
3 public const string AuthenticationScheme = "Custom";
4}

Next AuthenticationSchemeOptions,

1using Microsoft.AspNetCore.Authentication;
2
3public class CustomAuthOptions : AuthenticationSchemeOptions
4{
5 public string UserInfoEndpoint { get; set; }
6}

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.

1using Flurl.Http;
2using Microsoft.AspNetCore.Authentication;
3using Microsoft.Extensions.Logging;
4using Microsoft.Extensions.Options;
5using System;
6using System.Collections.Generic;
7using System.Security.Claims;
8using System.Text.Encodings.Web;
9using System.Threading.Tasks;
10...
11
12
13public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthOptions>
14{
15 public CustomAuthenticationHandler(
16 IOptionsMonitor<CustomAuthOptions> options,
17 ILoggerFactory logger,
18 UrlEncoder encoder,
19 ISystemClock clock
20 )
21 : base(options, logger, encoder, clock)
22 {
23 }
24
25 protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
26 {
27 if (!Request.Headers.ContainsKey("Authorization"))
28 return AuthenticateResult.Fail("Unauthorized");
29
30 string authorizationHeader = Request.Headers["Authorization"];
31 if (string.IsNullOrEmpty(authorizationHeader))
32 {
33 return AuthenticateResult.NoResult();
34 }
35
36 if (!authorizationHeader.StartsWith(CustomAuthenticationDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))
37 {
38 return AuthenticateResult.Fail("Unauthorized");
39 }
40
41 string token = authorizationHeader.Substring(CustomAuthenticationDefaults.AuthenticationScheme.Length).Trim();
42
43 if (string.IsNullOrEmpty(token))
44 {
45 return AuthenticateResult.Fail("Unauthorized");
46 }
47
48 try
49 {
50 return await ValidateTokenAsync(token);
51 }
52 catch (Exception ex)
53 {
54 return AuthenticateResult.Fail(ex.Message);
55 }
56 }
57
58 private async Task<AuthenticateResult> ValidateTokenAsync(string session)
59 {
60 // getting user info using HTTP request made using Flurl
61 var user = await Options.UserInfoEndpoint
62 .WithHeader("some-id", session)
63 .GetJsonAsync<User>();
64
65 if (user == null)
66 {
67 return AuthenticateResult.Fail("Unauthorized");
68 }
69
70 var claims = new List<Claim>
71 {
72 new Claim(ClaimTypes.Name, $"{user.Name} {user.Surname}"),
73 new Claim(ClaimTypes.GivenName, $"{user.Name}"),
74 new Claim(ClaimTypes.Surname, surname),
75
76 new Claim("scope", "orders:write"),
77 new Claim(ClaimTypes.NameIdentifier, user.id)
78 new Claim(ClaimTypes.Role, "User")
79 };
80
81 var identity = new ClaimsIdentity(claims, Scheme.Name);
82 var principal = new ClaimsPrincipal(identity);
83 var ticket = new AuthenticationTicket(principal, Scheme.Name);
84 return AuthenticateResult.Success(ticket);
85 }
86}

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,

1services
2 .AddCustomAuthentication(Configuration);
1public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)
2{
3
4 // Identity Server Configuration
5 var identityUrl = configuration.GetValue<string>("Authentication:IdentityServerBaseUrl");
6 services.AddAuthentication(options =>
7 {
8 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
9 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
10 }).AddJwtBearer(options =>
11 {
12 options.Authority = identityUrl;
13 options.RequireHttpsMetadata = false;
14 options.Audience = "your_api";
15 });
16
17 // Custom Authentication configuration
18 services.AddAuthentication(CustomAuthenticationDefaults.AuthenticationScheme)
19 .AddScheme<CustomAuthOptions, CustomAuthenticationHandler>(CustomAuthenticationDefaults.AuthenticationScheme,
20 o => o.UserInfoEndpoint = configuration.GetValue<string>("Authentication:Custom:UserInfoEndpoint"));
21
22 // we define policies here where we configure which scheme or combinations we need for each of our policies.
23 services.AddAuthorization(options =>
24 {
25 // authorize using custom auth scheme only
26 options.AddPolicy("UserRole", policy =>
27 {
28 policy.AuthenticationSchemes.Add(CustomAuthenticationDefaults.AuthenticationScheme);
29 policy.RequireRole("User");
30 });
31
32 // authorize using custom auth scheme as well as identity server
33 options.AddPolicy("OrdersWrite", policy =>
34 {
35 policy.AuthenticationSchemes.Add(JwtBearerDefaults.AuthenticationScheme);
36 policy.AuthenticationSchemes.Add(CustomAuthenticationDefaults.AuthenticationScheme);
37 policy.RequireClaim("scope", "orders:write");
38 });
39 });
40
41 return services;
42}

You can have different combinations in the policy defined above like based on scheme, claim, etc.

The configuration in the appsettings.json look like,

1"Authentication": {
2 "Custom": {
3 "UserInfoEndpoint": "https://yourcustomauthwebsite.com/user-info-path"
4 },
5 "IdentityServerBaseUrl": "https://url-of-idserver"
6}

Done, let’s enable the multi authorization to our endpoint. In the controller action,

1[Authorize(Policy = "OrdersWrite")]
2public async Task<ActionResult<OrderResult>> CreateOrder(OrderRequest orderRequest)
3{
4 var clientIdClaim = HttpContext.User.FindFirst("client_id"); // identity server client
5 var userIdClaim = HttpContext.User.FindFirst(ClaimTypes.NameIdentifier); // user authenticated using custom auth handler.
6 ...
7}

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,

1Authorization: <type> <credentials>

So bearer token request header looks like,

1Authorization: bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

And our custom token request header looks like,

1Authorization: custom abcasdjasdjlaksdjlasjdlasjd

Additional Resources

Written by Abhith Rajan
Abhith Rajan is an aspiring software engineer with more than 8 years of experience and proven successful track record of delivering technology-based products and services.
Buy me a coffee

Was this helpful?

👈 This is a live react editor.

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

Related ArticlesView All

Related VideosView All

A Journey into .NET Microservices with Steeltoe

.NET Microservices with Steeltoe

Single Page Architectures with VueJS and ASP.NET Core - Kevin Griffin

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.