InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found

12,395

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
Share:
12,395

Related videos on Youtube

scorpion5211
Author by

scorpion5211

Updated on June 19, 2022

Comments

  • scorpion5211
    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
      scorpion5211 over 5 years
      It 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
      juunas over 5 years
      I 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
    scorpion5211 over 5 years
    This 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
    itminus over 5 years
    @Gio , When authentication succeeds , we need set the AuthenticationTicket , which is actually a combination of ClaimsPrincipal and AuthenticatonProperties . We can create ClaimsPrincipal by usingClaimsIdentity . The ClaimsIdentity has a property of Name that represents current user .