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://docs.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 a403
(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 theUser
. Here's an example for your situation:Here's a description of what's going on in
HandleAuthenticateAsync
: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).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).X-TOKEN
request header, we create a newAuthenticationTicket
/ClaimsPrincipal
/ClaimsIdentity
combo. This is our representation of theUser
- you can include your ownClaim
s instead of usingEnumerable.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):
IConfiguration
as the final parameter, which is then used to read astring[]
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.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()
andAuthenticateResult.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 usingFail
in both cases if you'd prefer.The second thing you'll need is the
ClientTokenOptions
class, which is used above inAuthenticationHandler<ClientTokenOptions>
. For this example, this is a one-liner: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 toConfigureServices
:Here, we're just registering
ClientTokenHandler
as anAuthenticationHandler
under our own customClientToken
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 ofClientTokenOptions
to modify: we don't need that here, so it's just an empty lambda, effectively.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 CoreAuthenticationHandler
s 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.