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 CustomAuthenticationDefaults2{3 public const string AuthenticationScheme = "Custom";4}
Next AuthenticationSchemeOptions
,
1using Microsoft.AspNetCore.Authentication;23public class CustomAuthOptions : AuthenticationSchemeOptions4{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...111213public class CustomAuthenticationHandler : AuthenticationHandler<CustomAuthOptions>14{15 public CustomAuthenticationHandler(16 IOptionsMonitor<CustomAuthOptions> options,17 ILoggerFactory logger,18 UrlEncoder encoder,19 ISystemClock clock20 )21 : base(options, logger, encoder, clock)22 {23 }2425 protected override async Task<AuthenticateResult> HandleAuthenticateAsync()26 {27 if (!Request.Headers.ContainsKey("Authorization"))28 return AuthenticateResult.Fail("Unauthorized");2930 string authorizationHeader = Request.Headers["Authorization"];31 if (string.IsNullOrEmpty(authorizationHeader))32 {33 return AuthenticateResult.NoResult();34 }3536 if (!authorizationHeader.StartsWith(CustomAuthenticationDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))37 {38 return AuthenticateResult.Fail("Unauthorized");39 }4041 string token = authorizationHeader.Substring(CustomAuthenticationDefaults.AuthenticationScheme.Length).Trim();4243 if (string.IsNullOrEmpty(token))44 {45 return AuthenticateResult.Fail("Unauthorized");46 }4748 try49 {50 return await ValidateTokenAsync(token);51 }52 catch (Exception ex)53 {54 return AuthenticateResult.Fail(ex.Message);55 }56 }5758 private async Task<AuthenticateResult> ValidateTokenAsync(string session)59 {60 // getting user info using HTTP request made using Flurl61 var user = await Options.UserInfoEndpoint62 .WithHeader("some-id", session)63 .GetJsonAsync<User>();6465 if (user == null)66 {67 return AuthenticateResult.Fail("Unauthorized");68 }6970 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),7576 new Claim("scope", "orders:write"),77 new Claim(ClaimTypes.NameIdentifier, user.id)78 new Claim(ClaimTypes.Role, "User")79 };8081 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
,
1services2 .AddCustomAuthentication(Configuration);
1public static IServiceCollection AddCustomAuthentication(this IServiceCollection services, IConfiguration configuration)2{34 // Identity Server Configuration5 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 });1617 // Custom Authentication configuration18 services.AddAuthentication(CustomAuthenticationDefaults.AuthenticationScheme)19 .AddScheme<CustomAuthOptions, CustomAuthenticationHandler>(CustomAuthenticationDefaults.AuthenticationScheme,20 o => o.UserInfoEndpoint = configuration.GetValue<string>("Authentication:Custom:UserInfoEndpoint"));2122 // 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 only26 options.AddPolicy("UserRole", policy =>27 {28 policy.AuthenticationSchemes.Add(CustomAuthenticationDefaults.AuthenticationScheme);29 policy.RequireRole("User");30 });3132 // authorize using custom auth scheme as well as identity server33 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 });4041 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 client5 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