ASP.NET Core 2.0 authentication middleware - c#

With Core 1.1 followed #blowdart's advice and implemented a custom middleware:
https://stackoverflow.com/a/31465227/29821
It worked like this:
Middleware ran. Picked up a token from the request headers.
Verified the token and if valid built an identity (ClaimsIdentity) that contained multiple claims which then it added via HttpContext.User.AddIdentity();
In ConfigureServices using services.AddAuthorization I've added a policy to require the claim that is provided by the middleware.
In the controllers/actions I would then use [Authorize(Roles = "some role that the middleware added")]
This somewhat works with 2.0, except that if the token is not valid (step 2 above) and the claim is never added I get "No authenticationScheme was specified, and there was no DefaultChallengeScheme found."
So now I'm reading that auth changed in 2.0:
https://learn.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x
What's the right path for me to do the same thing in ASP.NET Core 2.0? I don't see an example to do truly custom authentication.

So, after a long day of trying to solve this problem, I've finally figured out how Microsoft wants us to make custom authentication handlers for their new single-middleware setup in core 2.0.
After looking through some of the documentation on MSDN, I found a class called AuthenticationHandler<TOption> that implements the IAuthenticationHandler interface.
From there, I found an entire codebase with the existing authentication schemes located at https://github.com/aspnet/Security
Inside of one of these, it shows how Microsoft implements the JwtBearer authentication scheme. (https://github.com/aspnet/Security/tree/master/src/Microsoft.AspNetCore.Authentication.JwtBearer)
I copied most of that code over into a new folder, and cleared out all the things having to do with JwtBearer.
In the JwtBearerHandler class (which extends AuthenticationHandler<>), there's an override for Task<AuthenticateResult> HandleAuthenticateAsync()
I added in our old middleware for setting up claims through a custom token server, and was still encountering some issues with permissions, just spitting out a 200 OK instead of a 401 Unauthorized when a token was invalid and no claims were set up.
I realized that I had overridden Task HandleChallengeAsync(AuthenticationProperties properties) which for whatever reason is used to set permissions via [Authorize(Roles="")] in a controller.
After removing this override, the code had worked, and had successfully thrown a 401 when the permissions didn't match up.
The main takeaway from this is that now you can't use a custom middleware, you have to implement it via AuthenticationHandler<> and you have to set the DefaultAuthenticateScheme and DefaultChallengeScheme when using services.AddAuthentication(...).
Here's an example of what this should all look like:
In Startup.cs / ConfigureServices() add:
services.AddAuthentication(options =>
{
// the scheme name has to match the value we're going to use in AuthenticationBuilder.AddScheme(...)
options.DefaultAuthenticateScheme = "Custom Scheme";
options.DefaultChallengeScheme = "Custom Scheme";
})
.AddCustomAuth(o => { });
In Startup.cs / Configure() add:
app.UseAuthentication();
Create a new file CustomAuthExtensions.cs
public static class CustomAuthExtensions
{
public static AuthenticationBuilder AddCustomAuth(this AuthenticationBuilder builder, Action<CustomAuthOptions> configureOptions)
{
return builder.AddScheme<CustomAuthOptions, CustomAuthHandler>("Custom Scheme", "Custom Auth", configureOptions);
}
}
Create a new file CustomAuthOptions.cs
public class CustomAuthOptions: AuthenticationSchemeOptions
{
public CustomAuthOptions()
{
}
}
Create a new file CustomAuthHandler.cs
internal class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
public CustomAuthHandler(IOptionsMonitor<CustomAuthOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
// store custom services here...
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
// build the claims and put them in "Context"; you need to import the Microsoft.AspNetCore.Authentication package
return AuthenticateResult.NoResult();
}
}

There are considerable changes in Identity from Core 1.x to Core 2.0 as the article you reference points out. The major change is getting away from the middleware approach and using dependency injection to configure custom services. This provides a lot more flexibility in customizing Identity for more complex implementations. So you want to get away from the middleware approach you mention above and move towards services. Follow the migration steps in the referenced article to achieve this goal. Start by replacing app.UseIdentity with app.UseAuthentication. UseIdentity is depreciated and will not be supported in future versions. For a complete example of how to insert a custom claims transformation and perform authorization on the claim view this blog post.

Related

AllowAnonymous not working with ASP.NET Core 6.0 Web API

I have the following authentication configuration in ASP.NET Core 6.0, with a custom authentication scheme:
// Enable authentication, add a custom scheme and set it as the default
builder.Services.AddAuthentication(opts =>
opts.DefaultAuthenticateScheme = "Custom")
.AddScheme<CustomAuthSchemeOptions, CustomAuthSchemeHandler>("Custom", null);
// stuff...
app.UseAuthentication();
app.UseAuthorization();
// Use attribute routing for the Web API
app.MapControllers();
Options are empty, while the CustomAuthSchemeHandler can be something as simple as this:
public class CustomAuthSchemeHandler: AuthenticationHandler<CustomAuthSchemeOptions>
{
public CustomAuthSchemeHandler(
IOptionsMonitor<CustomAuthSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected async override Task<AuthenticateResult> HandleAuthenticateAsync()
{
Console.WriteLine("Authenticating..."); // the logger, yeah yeah...
var principal = new ClaimsPrincipal(new ClaimsIdentity("Test"));
var ticket = new AuthenticationTicket(principal, "Custom");
return AuthenticateResult.Success(ticket);
}
}
Now, what I understand (although apparently undocumented) is that if I set the default authentication scheme as in the code above, authentication is automatically enforced on all controllers and actions.
Now what if I wanted to have a controller/action with no authentication?
My understanding is that there are two options:
Remove the default authentication scheme and use the [Authorize] attribute explicitly where needed
Use the [AllowAnonymous] attribute to disable authentication in specific contexts
The first option works, but I can't get the [AllowAnonymous] approach to work.
The following code behaves as if the [AllowAnonymous] wasn't there, i.e. the authentication scheme handler is called anyway and the user is authenticated.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet("test")]
[AllowAnonymous]
// ^^^^^^^^^^^^^
public string Get()
{
return HttpContext.User.Identity.IsAuthenticated ? "YES" : "NO";
}
}
What am I missing?
Thanks!
The AllowAnonymous attribute "allows anonymous" as its name implies, but also allows authorization, that's why your handler is called... it should work even when your handler can't authenticate (that's what the attribute does on the middleware)
PS: It also makes the middleware ignore the Authorize attribute if both are present, but not the default authentication scheme
Adding to Jcl's answer, that pointed me in the right direction, here's a summary of how to use authentication schemes and authorization in ASP.NET Core 6.0.
This is an excerpt from the blog post I've written after this question.
you can define many authentication schemes, whose handlers contain the actual authentication code
if you choose a default authentication scheme...
the authentication handler will be called at each request, no matter what
to actually enforce authorization, use RequireAuthorization() or [Require]
to selectively disable authorization where already enabled, use [AllowAnonymous]
if you don't set a default scheme...
you must explicitly choose an authentication scheme when enabling authorization, with [Require(AuthenticationSchemes = "...")]
the authentication handler will then be run only where you enabled authorization
to simplify the syntax and simply use [Authorize], define a default authorization policy at startup

How to configure JWT for ASP.NET API containing public razor pages and health checks

I have a hard time configuring the API authentication and authorization. There is definitely something I do not understand. Ideas appreciated.
I have NET Core 3.1 API. It contains Razor Pages (documentation and Swagger UI).
There is no sign-in endpoint. The token is validated using the secret signing key.
Requirements:
Use JWT
Make all endpoints protected by default.
Razor pages are public.
Enable anonymous access to health checks.
Enable anonymous access to several endpoints.
Support roles.
I am not sure how to combine all the settings to make it work properly.
I've implemented a custom authentication handler.
I've tried many combinations, add/removing parts of the config.
The main problem is the handler is called every time (AllowAnonymous attribute is ignored) or I'm getting 401 instead of 403.
Playing with:
// in controllers
[Authorize(Roles="Role")]
[AllowAnonymous]
// in configure services
services.AddRazorPages(options => options.Conventions.AllowAnonymousToFolder("/"));
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddScheme<JwtBearerOptions, JwtBearerAuthenticationHandler>(JwtBearerDefaults.AuthenticationScheme, options => { });
services.AddAuthorization(options =>
{
// expected to add the default schema for the [Authorize] attribute (so I do not need to write it explicitly)
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
// tried to resolve the health checks call the authentication handler
options.AddPolicy("PublicPolicy", new AuthorizationPolicyBuilder()
.RequireAssertion(_ => true)
.Build());
});
// in configure
app.UseAuthentication(); // will cause all actions like decoreted with [Authorize]
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages().RequireAuthorization(SecurityRegistration.PublicPolicyName);
// map health checks
endpoints.MapApiHealthChecks("/health")
.RequireAuthorization("PublicPolicy");
}
I have implemented a custom authentication handler. Later on, I refactored it and inherited the JwtBearerHandler.
public class JwtBearerAuthenticationHandler: JwtBearerHandler
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
}
}
Why not using the AddJwtBearer()? I am generating/validating the tokes and I need to implement the OptionsMonitor pattern (reading the security settings from JSON, namely signing key, issuer, audience and expiration).
I expected that when I decorate a controller or an action with [AllowAnonymous] attribute, the handler will not be invoked. But it is called every time.
When I remove the UseAuthentication() I am getting issues with the default authentication schema and I'm getting 401 instead of 403.
I've also tried to add/remove the authorize attribute to controllers using a filter with/without a policy parameter.
configure.Filters.Add(new AuthorizeFilter());

Multiple Authentication Schemes ASPNET Core 3

See Update below
I'm using Azure AD B2C and I'd like my users to be able to log in thru my web app as well as be able to utilize JWT bearer tokens and call Web API methods from a mobile app.
I can get either authentication scheme to work by itself. For example, in my startup.cs I can do the following:
services
.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
which works as expected (a user can login on the web site, but JWT doesn't work).
Alternatively, I can instead use the following and then only JWT bearer tokens will work:
services
.AddAuthentication(AzureADB2CDefaults.JwtBearerAuthenticationScheme)
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
If I want either to work, I can do the following (with the help of https://stackoverflow.com/a/49706390)
services
.AddAuthentication()
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(AzureADB2CDefaults.AuthenticationScheme, AzureADB2CDefaults.JwtBearerAuthenticationScheme)
.Build();
}
And now either will work. (edit: actually, they don't work completely)
HOWEVER, I also have this code:
app.UseAuthentication();
app.UseMiddleware<MyAfterAuthenticatedMiddleware>();
app.UseAuthorization();
The problem is that when I use the combination of either authentication, when my middleware code runs, my user is not authenticated (in the middleware code) and has no claims, etc. but obtains them later in the pipeline.
What's happening here? And how can this be fixed?
It seems that when I don't specify a default authentication scheme--in order to have multiple schemes--the authentication is not happening until the authorization step in the pipeline.
I need my middleware to run after authentication and before authorization.
How can I make that happen with the multiple authentication schemes?
UPDATE -- Solved! But there must be a better way!??
First of all, to the people who have created the .NET security stuff, I say kudos. It's important and it's difficult. However I do think there may be a lot of room for improvement.
Most developers dabble in security when they have to, and then go back to their "regular" job". Unless you work with it every day, it's tough to keep on top of. Every time you go back to it, everything's changed yet again.
It must be a common scenario: I want my users to be able to log in to my web site and interact with various web API methods. I would like them to also be able to access those same API methods via another means, such as a mobile app--where I'd be using JWT tokens, or equivalent.
This shouldn't be hard to make work.
However, I was tying myself into knots creating handlers for this and policies for that. One thing would work, but another thing wouldn't. Then when I thought things were right--I discovered some of the challenge and forbid logic didn't work as expected.
The built-in Authorization middleware has the ability to do authentication -- this was one of the early roads I went down, only to discover that it didn't fully work -- and it caused other problems for me, as described above.
In my opinion, authentication should not happen during authorization. Authentication should happen where it is expected--in authentication middleware. (My guess is that it was added in authorization in order to work around some other problem that presented itself years ago -- and perhaps still exists today)
Anyway--here is how I finally got things to work. It could be a lot cleaner and slicker and more flexible, but it works for my needs. And it is less of a hack than anything else I have seen. But is there a nicer, built-in class that could have done this for me?
My new question is this: is there a better way to get this done than how I've done it as described below?. It's hard to believe this is the best way.
In Startup.ConfigureServices I have now have the following:
services
.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("AzureAdB2C", options));
I then also have:
services.AddHttpContextAccessor();
services.AddSingleton<IAuthenticationSchemeProvider, MyAuthenticationSchemeProvider>();
And finally, I have a new class:
public class MyAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
public MyAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IHttpContextAccessor httpContextAccessor) : base(options)
{
HttpContextAccessor = httpContextAccessor;
}
protected MyAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes, IHttpContextAccessor httpContextAccessor) : base(options, schemes)
{
HttpContextAccessor = httpContextAccessor;
}
private IHttpContextAccessor HttpContextAccessor { get; }
private bool IsBearerRequest()
{
var httpContext = HttpContextAccessor.HttpContext;
return httpContext.Request.Headers.ContainsKey("Authorization")
&& httpContext.Request.Headers["Authorization"].Any(x => x.ToLower().Contains("bearer"));
}
public async Task<AuthenticationScheme> GetMySchemeAsync()
{
return IsBearerRequest()
? await GetSchemeAsync(AzureADB2CDefaults.BearerAuthenticationScheme)
: await base.GetDefaultAuthenticateSchemeAsync();
}
public override async Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
{
return await GetMySchemeAsync();
}
public override Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
{
return GetMySchemeAsync();
}
public override Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
{
return GetMySchemeAsync();
}
}
Now I can use both kinds of authentication, the challenge & forbid work as expected. Why isn't there a built-in class that allows for switching between authentication schemes? Why does the authorization middleware attempt to authenticate with multiple schemes (I say it shouldn't do it at all), but not the authentication middleware?
Now this is here for anyone else who struggles with a similar issue.

How do I disable/enable authentication at runtime in Asp.Net Core 2.2?

A website is per default anonymous access only.
The admin has a button to switch the site into maintenance mode, which should enable authorization using the built-in CookieAuthentication (flip a bit in a database, not relevant for this post).
In order to make that work, I first configured cookie authentication (in startup.cs):
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/auth/login");
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
}
Then on relevant controllers, I put an [Authorize] attribute.
[Authorize]
public class HomeController : Controller
{
//removed
}
This works perfectly - cookie auth kicks in when authorize-attribute is present. So far so good.
Now I want to disable authorization at runtime when maintenance mode is off.
Attempted Solution
This is what I ended up with after a lot of trial and error and research.
public void OnAuthorization(AuthorizationFilterContext context)
{
IMaintenanceModeDataService ds = context.HttpContext.RequestServices.GetService<IMaintenanceModeDataService>();
if (!ds.IsMaintenanceModeEnabled)
{
//Maintenance mode is off, no need for authorization
return;
}
else
{
ClaimsPrincipal user = context.HttpContext.User;
if (user.Identity.IsAuthenticated)
{
//when the user is authenticated, we don't need to do anything else.
return;
}
else
{
//we're in maintenance mode AND the user is not
//It is outside the scope of this to redirect to a login
//We just want to display maintenancemode.html
context.Result = new RedirectResult("/maintenancemode.html");
return;
}
}
}
[MaintenanceModeAwareAuthorize]
public class HomeController : Controller
{
//removed
}
This works great when the site is in maintenance mode.
When the site is NOT in maintenance mode, the cookie authentication still kicks in and requires auth. I could remove that and try to implement my own auth, but that would be stupid, when we already have perfectly well-crafted solutions built-in.
How do I disable authorization when the site is NOT in maintenance mode (at runtime)?
Notes:
Q: Why not handle this by doing x (which requires serverside access to config, environment vars, server or similar)?
A: Because this needs to be immediately accessible to non-technical admin-users by clicking a button in the backend.
Yes you can!
The authorization system in ASP.NET Core is extensible and you can implement your scenario easily with poliy-based authorization.
Two main things to know to get going:
an authorization policy is made of one or more requirements
all of the requirements must be satisfied for a policy to succeed
Our goal is then to create a requirement which is satisfied if any of the following statements is true:
the maintenance mode is not enabled, or
the user is authenticated
Let's see the code!
The first step is to create our requirement:
public class MaintenanceModeDisabledOrAuthenticatedUserRequirement : IAuthorizationRequirement
{
}
We then have to implement the handler for this requirement, which will determine if it's satisfied or not. The good news is handlers support dependency injection:
public class MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler : AuthorizationHandler<MaintenanceModeDisabledOrAuthenticatedUserRequirement>
{
private readonly IMaintenanceModeDataService _maintenanceModeService;
public MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler(IMaintenanceModeDataService maintenanceModeService)
{
_maintenanceModeService = maintenanceModeService;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MaintenanceModeDisabledOrAuthenticatedUserRequirement requirement)
{
if (!_maintenanceModeService.IsMaintenanceModeEnabled || context.User.Identities.Any(x => x.IsAuthenticated))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Next, we need to create an authorization policy that uses this requirement, and you have 2 choices here:
you can redefine the default policy, used when "empty" [Authorize] attributes are used, or
create an explicit policy that you'll have to reference in your attributes, like [Authorize(Policy = "<your-policy-name>")]
There's no right or wrong answer; I'd pick the first option is my application had only one authorization policy, and the second one if it had several of them. We'll see how to do both:
services
.AddAuthorization(options =>
{
// 1. This is how you redefine the default policy
// By default, it requires the user to be authenticated
//
// See https://github.com/dotnet/aspnetcore/blob/30eec7d2ae99ad86cfd9fca8759bac0214de7b12/src/Security/Authorization/Core/src/AuthorizationOptions.cs#L22-L28
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddRequirements(new MaintenanceModeDisabledOrAuthenticatedUserRequirement())
.Build();
// 2. Define a specific, named policy that you can reference from your [Authorize] attributes
options.AddPolicy("MaintenanceModeDisabledOrAuthenticatedUser", builder => builder
.AddRequirements(new MaintenanceModeDisabledOrAuthenticatedUserRequirement()));
});
Next, you need to register the requirement handler as an IAuthorizationHandler, as indicated in the official docs
// The lifetime you pick is up to you
// You just need to remember that it's got a dependency on IMaintenanceModeDataService, so if you
// registered the implementation of IMaintenanceModeDataService as a scoped service, you shouldn't
// register the handler as a singleton
// See this captive dependency article from Mark Seeman: https://blog.ploeh.dk/2014/06/02/captive-dependency/
services.AddScoped<IAuthorizationHandler, MaintenanceModeDisabledOrAuthenticatedUserRequirementHandler>();
The final step is to apply the [Authorize] attributes on your controllers/actions as needed.
// 1. If you redefined the default policy
[Authorize]
public IActionResult Index()
{
return View();
}
// 2. If you defined an explicit policy
[Authorize(Policy = "MaintenanceModeDisabledOrAuthenticatedUser")]
public IActionResult Index()
{
return View();
}
I am afraid that could not be done .The accept of authorization is different from authentication, when context.HttpContext.User.Identity.IsAuthenticated is false, it will always redirect to login page.
It's better to have actions that must or may require authorization in a controller together, and unauthorized actions in a separate controller with [AllowAnonymous].
if (!user.IsMaintenanceModeEnabled)
{
context.Result = new RedirectResult("Another controller with [AllowAnonymous]");
return;
}
Since current pages need work perfectly with anonymous mode, then authentication should NOT be in Controller level.
I think your requests are:
If a Maintancer login system,
run extra code to show maintance elements(switch button or others) on page, so Maintancer can switch page with different mode, and do maintancer actions
If user visit site anonymously, anonymous-mode elements will render to browser
If user login but not an Maintancer, normal-user-mode elements will render to browser
To resolve those, The key is to block unauthorized user to visit Maintancer ACTIONS, instead of controller.
my suggestions are:
in _Layout.cshtml page, check if Maintancer Login, then enject switch button
in the actions or pages that could visit anornymously, check if "Maintancer Login" && IsMaintenanceMode, then show Maintancer-authorized elements, like Delete Post, Edit Content, ...
in Controller.Actions that works only for Maintancer(like Delete Post), add [Authorize(Roles="Maintancer")] or [Authorize(Policy="Maintancer")] or you customized authorize.

How to correctly setup Policy Authorization for WEB API in .NET Core

I have this Web API project, with no UI. My appsettings.json file has a section listing tokens and which client they belong to. So the client will need to just present a matching token in the header. If no token is presented or an invalid one, then it should be returning a 401.
In ConfigureServices I setup authorization
.AddTransient<IAuthorizationRequirement, ClientTokenRequirement>()
.AddAuthorization(opts => opts.AddPolicy(SecurityTokenPolicy, policy =>
{
var sp = services.BuildServiceProvider();
policy.Requirements.Add(sp.GetService<IAuthorizationRequirement>());
}))
This part fires correctly from what I can see.
Here is code for the ClientTokenRequirement
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ClientTokenRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext authFilterContext)
{
if (string.IsNullOrWhiteSpace(_tokenName))
throw new UnauthorizedAccessException("Token not provided");
var httpContext = authFilterContext.HttpContext;
if (!httpContext.Request.Headers.TryGetValue(_tokenName, out var tokenValues))
return Task.CompletedTask;
var tokenValueFromHeader = tokenValues.FirstOrDefault();
var matchedToken = _tokens.FirstOrDefault(t => t.Token == tokenValueFromHeader);
if (matchedToken != null)
{
httpContext.Succeed(requirement);
}
}
return Task.CompletedTask;
}
When we are in the ClientTokenRequirement and have not matched a token it returns
return Task.CompletedTask;
This is done how it is documented at
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1
This works correctly when there is a valid token, but when there isnt and it returns Task.Completed, there is no 401 but an exception instead
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
I have read other stackoverflow articles about using Authentication rather than Authorization, but really this policy Authorization is the better fit for purpose. So I am looking for ideas on how to prevent this exception.
Interestingly, I think this is just authentication, without any authorisation (at least not in your question). You certainly want to authenticate the client but you don't appear to have any authorisation requirements. Authentication is the process of determining who is making this request and authorisation is the process of determining what said requester can do once we know who it is (more here). You've indicated that you want to return a 401 (bad credentials) rather than a 403 (unauthorised), which I believe highlights the difference (more here).
In order to use your own authentication logic in ASP.NET Core, you can write your own AuthenticationHandler, which is responsible for taking a request and determining the User. Here's an example for your situation:
public class ClientTokenHandler : AuthenticationHandler<ClientTokenOptions>
{
private readonly string[] _clientTokens;
public ClientTokenHandler(IOptionsMonitor<ClientTokenOptions> optionsMonitor,
ILoggerFactory loggerFactory, UrlEncoder urlEncoder, ISystemClock systemClock,
IConfiguration config)
: base(optionsMonitor, loggerFactory, urlEncoder, systemClock)
{
_clientTokens = config.GetSection("ClientTokens").Get<string[]>();
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var tokenHeaderValue = (string)Request.Headers["X-TOKEN"];
if (string.IsNullOrWhiteSpace(tokenHeaderValue))
return Task.FromResult(AuthenticateResult.NoResult());
if (!_clientTokens.Contains(tokenHeaderValue))
return Task.FromResult(AuthenticateResult.Fail("Unknown Client"));
var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(
Enumerable.Empty<Claim>(),
Scheme.Name));
var authenticationTicket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);
return Task.FromResult(AuthenticateResult.Success(authenticationTicket));
}
}
Here's a description of what's going on in HandleAuthenticateAsync:
The header X-TOKEN is retrieved from the request. If this is invalid, we indicate that we are unable to authenticate the request (more on this later).
The value retrieved from the X-TOKEN header is compared against a known list of client-tokens. If this is unsuccessful, we indicate that authentication failed (we don't know who this is - more on this later too).
When a client-token matches the X-TOKEN request header, we create a new AuthenticationTicket/ClaimsPrincipal/ClaimsIdentity combo. This is our representation of the User - you can include your own Claims instead of using Enumerable.Empty<Claim>() if you want to associate additional information with the client.
You should be able to use this as-is for the most part, with a few changes (I've simplified to both keep the answer short and fill in a few gaps from the question):
The constructor takes an instance of IConfiguration as the final parameter, which is then used to read a string[] from, in my example, appsettings.json. You are likely doing this differently, so you can just use DI to inject whatever it is you're currently using here, as needed.
I've hardcoded X-TOKEN as the header name to use when extracting the token. You'll likely be using a different name for this yourself and I can see from your question that you're not hardcoding it, which is better.
One other thing to note about this implementation is the use of both AuthenticateResult.NoResult() and AuthenticateResult.Fail(...). The former indicates that we did not have enough information in order to perform the authentication and the latter indicates that we had everything we needed but the authentication failed. For a simple setup like yours, I think you'd be OK using Fail in both cases if you'd prefer.
The second thing you'll need is the ClientTokenOptions class, which is used above in AuthenticationHandler<ClientTokenOptions>. For this example, this is a one-liner:
public class ClientTokenOptions : AuthenticationSchemeOptions { }
This is used for configuring your AuthenticationHandler - feel free to move some of the configuration into here (e.g. the _clientTokens from above). It also depends on how configurable and reusable you want this to be - as another example, you could define the header name in here, but that's up to you.
Lastly, to use your ClientTokenHandler, you'll need to add the following to ConfigureServices:
services.AddAuthentication("ClientToken")
.AddScheme<ClientTokenOptions, ClientTokenHandler>("ClientToken", _ => { });
Here, we're just registering ClientTokenHandler as an AuthenticationHandler under our own custom ClientToken scheme. I wouldn't hardcode "ClientToken" here like this, but, again, this is just a simplification. The funky _ => { } at the end is a callback that is given an instance of ClientTokenOptions to modify: we don't need that here, so it's just an empty lambda, effectively.
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
The "DefaultChallengeScheme" in your error message has now been set with the call to services.AddAuthentication("ClientToken") above ("ClientToken" is the scheme name).
If you want to go with this approach, you'll need to remove your ClientTokenRequirement stuff. You might also find it interesting to have a look through Barry Dorrans's BasicAuthentication project - it follows the same patterns as the official ASP.NET Core AuthenticationHandlers but is simpler for getting started. If you're not concerned about the configurability and reusability aspects, the implementation I've provided should be fit for purpose.

Categories

Resources