问题
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.
回答1:
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 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):
- The constructor takes an instance of
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. - 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 AuthenticationHandler
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.
来源:https://stackoverflow.com/questions/52008000/how-to-correctly-setup-policy-authorization-for-web-api-in-net-core