InvalidOperationException: не указана AuthenticationScheme и не найдено DefaultChallengeScheme

У нас есть проект Net Core 2.1 API. Мы используем заголовки запроса для получения ключа API, который мы проверяем по нашей базе данных, чтобы увидеть, соответствует ли он одному из ожидаемых ключей. Если это так, мы разрешаем выполнение запроса, в противном случае мы хотим отправить обратно неавторизованный ответ.

наш startup.cs

services.AddAuthorization(options =>
            {
                options.AddPolicy("APIKeyAuth", policyCorrectUser =>
                {
                    policyCorrectUser.Requirements.Add(new APIKeyAuthReq());
                });

            });
services.AddSingleton<Microsoft.AspNetCore.Authorization.IAuthorizationHandler, APIKeyAuthHandler>();

Наш 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;
                    }
                }
            }
        }
    }

и наш controller.cs

    [Route("api/[controller]/[action]")]
    [Authorize("APIKeyAuth")]
    [ApiController]
    public class SomeController : ControllerBase
    {
    }

Все работает нормально, когда существует действительный ключ, но когда его нет, возникает внутренняя ошибка 500 для No authenticationScheme вместо 403.

Мы относительно плохо знакомы с сетевым ядром (пришедшим из Net Framework / Forms Authentication), поэтому, если есть более точный способ выполнения такого рода аутентификации, пожалуйста, дайте мне знать.

Сообщение об ошибке:

InvalidOperationException: не была указана схема проверки подлинности, и схема DefaultChallengeScheme не найдена. Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync (контекст HttpContext, строковая схема, свойства AuthenticationProperties)


person scorpion5211    schedule 12.09.2018    source источник
comment
Я думаю, вам следует подумать об использовании аутентификации на основе токенов. Вы можете проверить эту ссылку - stackoverflow.com/questions/38661090/   -  person Souvik Ghosh    schedule 12.09.2018
comment
Похоже, аутентификация на основе токенов предназначена для тех случаев, когда у вас есть имя пользователя / пароль для начала? Наше приложение будет основано на статическом API-ключе, который хранится в БД. Я считаю, что аутентификация на основе токенов не будет работать в нашем случае, если я что-то не упущу   -  person scorpion5211    schedule 12.09.2018
comment
Я бы посоветовал вам создать схему аутентификации для вашего метода аутентификации. Затем вы можете основывать авторизацию на принципале пользователя, созданном вашей схемой. Например. joonasw.net/view/creating-auth-scheme-in- aspnet-core-2   -  person juunas    schedule 12.09.2018
comment
Удалить [Authorize("APIKeyAuth")]   -  person davidfowl    schedule 12.09.2018


Ответы (1)


Предпочтительна аутентификация на основе токенов. Однако, если вам действительно нужна нестандартная схема ApiKeyAuth, что ж, это возможно.

Во-первых, кажется, что Authorize("APIKeyAuth") здесь не имеет смысла, поскольку мы должны аутентифицировать пользователя перед авторизацией. Когда есть входящий запрос, сервер не знает, кто этот пользователь. Итак, переместим ApiKeyAuth с Authorization на Authentication.

Для этого просто создайте фиктивный ApiKeyAuthOpts, который можно использовать для хранения параметров.

public class ApiKeyAuthOpts : AuthenticationSchemeOptions
{
}

и простой ApiKeyAuthHandler для обработки аутентификации (я просто скопировал некоторые из приведенных выше кодов):

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);
        }
    }
}

Наконец, нам все еще нужно немного настроить, чтобы они работали:

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();
}

Когда вы отправляете запрос к методу действия, защищенному [Authorize]:

GET https://localhost:44366/api/values/1 HTTP/1.1
Authorization: api-key xxx_yyy_zzz

ответ будет HTTP/1.1 200 OK. Если вы отправите запрос без правильного ключа, ответ будет следующим:

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
person itminus    schedule 12.09.2018
comment
Это потрясающе! Большое спасибо за то, что нашли время подробно объяснить! Один вопрос - как правильно указать имя пользователя в User.Identity.Name? раньше я использовал httpContext.HttpContext.Items.Add("CurrentUser", login);, но я считаю, что есть способ лучше? - person scorpion5211; 12.09.2018
comment
@Gio, когда аутентификация прошла успешно, нам нужно установить AuthenticationTicket, который на самом деле представляет собой комбинацию ClaimsPrincipal и AuthenticatonProperties. Мы можем создать ClaimsPrincipal, используяClaimsIdentity. ClaimsIdentity имеет свойство Name, которое представляет текущего пользователя. - person itminus; 12.09.2018