InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found
Token based authentication is preferred. However, if you do need a custom ApiKeyAuth
scheme, well, it's possible.
Firstly, it seems that Authorize("APIKeyAuth")
does not make sense here, as we have to authenticate the user before authorization. When there's an incoming request, the server has no idea who the user is. So, let's move the ApiKeyAuth
from Authorization
to Authentication
.
To do that, just create a dummy ApiKeyAuthOpts
that can be used to hold options
public class ApiKeyAuthOpts : AuthenticationSchemeOptions
{
}
and a simple ApiKeyAuthHandler
to handle authentication (I just copy some of your codes above):
public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOpts>
{
public ApiKeyAuthHandler(IOptionsMonitor<ApiKeyAuthOpts> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
private const string API_TOKEN_PREFIX = "api-key";
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = null;
string authorization = Request.Headers["Authorization"];
if (string.IsNullOrEmpty(authorization)) {
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith(API_TOKEN_PREFIX, StringComparison.OrdinalIgnoreCase)) {
token = authorization.Substring(API_TOKEN_PREFIX.Length).Trim();
}
if (string.IsNullOrEmpty(token)) {
return AuthenticateResult.NoResult();
}
// does the token match ?
bool res =false;
using (DBContext db = new DBContext()) {
var login = db.Login.FirstOrDefault(l => l.Apikey == token); // query db
res = login ==null ? false : true ;
}
if (!res) {
return AuthenticateResult.Fail($"token {API_TOKEN_PREFIX} not match");
}
else {
var id=new ClaimsIdentity(
new Claim[] { new Claim("Key", token) }, // not safe , just as an example , should custom claims on your own
Scheme.Name
);
ClaimsPrincipal principal=new ClaimsPrincipal( id);
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
At last, we still need a little of configuration to make them to work:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication("ApiKeyAuth")
.AddScheme<ApiKeyAuthOpts,ApiKeyAuthHandler>("ApiKeyAuth","ApiKeyAuth",opts=>{ });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ...
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseMvc();
}
When you send a request to action method protected by [Authorize]
:
GET https://localhost:44366/api/values/1 HTTP/1.1
Authorization: api-key xxx_yyy_zzz
the response will be HTTP/1.1 200 OK
. When you send a request without the correct key, the response will be:
HTTP/1.1 401 Unauthorized
Server: Kestrel
X-SourceFiles: =?UTF-8?B?RDpccmVwb3J0XDIwMThcOVw5LTEyXFNPLkFwaUtleUF1dGhcQXBwXEFwcFxhcGlcdmFsdWVzXDE=?=
X-Powered-By: ASP.NET
Date: Wed, 12 Sep 2018 08:33:23 GMT
Content-Length: 0
Related videos on Youtube
scorpion5211
Updated on June 19, 2022Comments
-
scorpion5211 almost 2 years
We have a Net Core 2.1 API project. We use the request headers to retrieve API key which we check against our database to see if it matches one of the expected keys. If it does then we allow the request to continue, otherwise we want to send back Unauthorized response.
our startup.cs
services.AddAuthorization(options => { options.AddPolicy("APIKeyAuth", policyCorrectUser => { policyCorrectUser.Requirements.Add(new APIKeyAuthReq()); }); }); services.AddSingleton<Microsoft.AspNetCore.Authorization.IAuthorizationHandler, APIKeyAuthHandler>();
Our APIKeyAuthHandler.cs
public class APIKeyAuthReq : IAuthorizationRequirement { } public class APIKeyAuthHandler : AuthorizationHandler<APIKeyAuthReq> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, APIKeyAuthReq requirement) { if (context == null) throw new ArgumentNullException(nameof(context)); if (requirement == null) throw new ArgumentNullException(nameof(requirement)); var httpContext = context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext; var headers = httpContext.HttpContext.Request.Headers; if (headers.TryGetValue("Authorization", out Microsoft.Extensions.Primitives.StringValues value)) { using (DBContext db = new DBContext ()) { var token = value.First().Split(" ")[1]; var login = db.Login.FirstOrDefault(l => l.Apikey == token); if (login == null) { context.Fail(); httpContext.HttpContext.Response.StatusCode = 403; return Task.CompletedTask; } else { httpContext.HttpContext.Items.Add("CurrentUser", login); context.Succeed(requirement); return Task.CompletedTask; } } } } }
and our controller.cs
[Route("api/[controller]/[action]")] [Authorize("APIKeyAuth")] [ApiController] public class SomeController : ControllerBase { }
Everything works fine when a valid key exists but when it doesnt, there is a 500 internal error thrown for No authenticationScheme instead of 403.
We are relatively new to net core (coming from Net Framework/Forms Authentication) so if there is more accurate way of doing this sort of auth, please let me know.
Error Message:
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
-
scorpion5211 over 5 yearsIt seems like token based authentication is for when you have a username/password to start with? Our application will be based on static API key that is stored in DB. I believe token based auth won’t work in our case unless I’m missing something
-
juunas over 5 yearsI would suggest that you create an authentication scheme for your authentication method. Then you can base authorization on the user principal created by your scheme. E.g. joonasw.net/view/creating-auth-scheme-in-aspnet-core-2
-
-
scorpion5211 over 5 yearsThis is perfect! thank you so much for taking time to explain in detail! One question - what would be a proper way to get the login name populated in User.Identity.Name? before i was using
httpContext.HttpContext.Items.Add("CurrentUser", login);
but i believe there is a better way of doing this? -
itminus over 5 years@Gio , When authentication succeeds , we need set the
AuthenticationTicket
, which is actually a combination ofClaimsPrincipal
andAuthenticatonProperties
. We can createClaimsPrincipal
by usingClaimsIdentity
. TheClaimsIdentity
has a property ofName
that represents current user .