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

scorpion5211 picture scorpion5211 · Sep 12, 2018 · Viewed 8.5k times · Source

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)

Answer

itminus picture itminus · Sep 12, 2018

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 use 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