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