ASP.NET Core 2.0, объединяющий файлы cookie и авторизацию на предъявителя для одной и той же конечной точки

Я создал новый проект ASP.NET Core Web Application в VS17, используя шаблон "Web Application (Model-View-Controller)" и ".Net Framework" + "ASP.NET Core 2" в качестве конфигурации. Конфигурация аутентификации установлена на "Индивидуальные учетные записи пользователей".

У меня есть следующая конечная точка образца:

[Produces("application/json")]
[Route("api/price")]
[Authorize(Roles = "PriceViwer", AuthenticationSchemes = "Cookies,Bearer")]
public class PriceController : Controller
{

    public IActionResult Get()
    {
        return Ok(new Dictionary<string, string> { {"Galleon/Pound",
                                                   "999.999" } );
    }
}

"Cookies,Bearer" получают путем объединения CookieAuthenticationDefaults.AuthenticationScheme и JwtBearerDefaults.AuthenticationScheme.

Цель состоит в том, чтобы иметь возможность настроить авторизацию для конечной точки так, чтобы она могла получить к ней доступ, используя методы аутентификации как с помощью токена, так и с помощью cookie.

Вот настройки, которые я имею для Аутентификации в моем Startup.cs:

    services.AddAuthentication()
        .AddCookie(cfg => { cfg.SlidingExpiration = true;})
        .AddJwtBearer(cfg => {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
            cfg.TokenValidationParameters = new TokenValidationParameters() {
                                                    ValidIssuer = Configuration["Tokens:Issuer"],
                                                    ValidAudience = Configuration["Tokens:Issuer"],
                                                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]))
                                                };
        });

Поэтому, когда я пытаюсь получить доступ к конечной точке с помощью браузера, я получаю ответ 401 с пустой HTML-страницей.
I get the 401 response with a blank html page.

Затем я вхожу в систему, и когда я снова пытаюсь получить доступ к конечной точке, я получаю тот же ответ.

Затем я пытаюсь получить доступ к конечной точке, указав токен на предъявителя. И это возвращает желаемый результат с ответом 200.
And that returns the desired result with the 200 response.

Итак, если я удаляю [Authorize(AuthenticationSchemes = "Cookies,Bearer")], ситуация становится противоположной - проверка подлинности cookie работает и возвращает 200, однако тот же метод маркера носителя, который использовался выше, не дает никаких результатов и просто перенаправляет на страницу входа по умолчанию AspIdentity.

Я вижу две возможные проблемы здесь:

1) ASP.NET Core не допускает "комбинированную" аутентификацию. 2) "Cookies" не является допустимым именем схемы. Но тогда что правильно использовать?

Пожалуйста, порекомендуйте. Спасибо.

Ответ 1

Я думаю, вам не нужно устанавливать AuthenticationScheme для вашего контроллера. Просто используйте аутентифицированного пользователя в ConfigureServices следующим образом:

// requires: using Microsoft.AspNetCore.Authorization;
//           using Microsoft.AspNetCore.Mvc.Authorization;
services.AddMvc(config =>
{
    var policy = new AuthorizationPolicyBuilder()
                     .RequireAuthenticatedUser()
                     .Build();
    config.Filters.Add(new AuthorizeFilter(policy));
});

Для документации моих источников: registerAuthorizationHandlers

Для части, если схема-ключ был недействительным, вы можете использовать интерполированную строку, чтобы использовать правильные ключи:

[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}")]

Редактировать: я провел дальнейшие исследования и пришел к следующему выводу: невозможно авторизовать метод с двумя схемами или подобными, но вы можете использовать два открытых метода для вызова частного метода, подобного этому:

//private method
private IActionResult GetThingPrivate()
{
   //your Code here
}

//Jwt-Method
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme}")]
[HttpGet("bearer")]
public IActionResult GetByBearer()
{
   return GetThingsPrivate();
}

 //Cookie-Method
[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme}")]
[HttpGet("cookie")]
public IActionResult GetByCookie()
{
   return GetThingsPrivate();
}

Ответ 2

Если я правильно понимаю вопрос, то я считаю, что есть решение. В следующем примере я использую проверку подлинности на основе файлов cookie и предъявителя в одном приложении. Атрибут [Authorize] можно использовать без указания схемы, и приложение будет реагировать динамически, в зависимости от используемого метода авторизации.

services.AddAuthentication вызывается дважды, чтобы зарегистрировать 2 схемы аутентификации. Ключом к решению является вызов services.AddAuthorization в конце фрагмента кода, который указывает ASP.NET использовать ОБА схемы.

Я проверил это, и, кажется, работает хорошо.

(На основе документов Microsoft.)

services.AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = "https://localhost:4991";
        options.RequireHttpsMetadata = false;

        options.ClientId = "WebApp";
        options.ClientSecret = "secret";

        options.ResponseType = "code id_token";
        options.Scope.Add("api");
        options.SaveTokens = true;
    });

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://localhost:4991";
        options.RequireHttpsMetadata = false;
        // name of the API resource
        options.Audience = "api";
    });

services.AddAuthorization(options =>
{
    var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
        CookieAuthenticationDefaults.AuthenticationScheme,
        JwtBearerDefaults.AuthenticationScheme);
    defaultAuthorizationPolicyBuilder =
        defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
    options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});

РЕДАКТИРОВАТЬ

Это работает для аутентифицированных пользователей, но просто возвращает 401 (не авторизованный), если пользователь еще не вошел в систему.

Чтобы гарантировать, что неавторизованные пользователи будут перенаправлены на страницу входа, добавьте следующий код в метод Configure в своем классе запуска. Примечание: важно, чтобы новое промежуточное ПО было размещено после вызова app.UseAuthentication().

app.UseAuthentication();
app.Use(async (context, next) =>
{
    await next();
    var bearerAuth = context.Request.Headers["Authorization"]
        .FirstOrDefault()?.StartsWith("Bearer ") ?? false;
    if (context.Response.StatusCode == 401
        && !context.User.Identity.IsAuthenticated
        && !bearerAuth)
    {
        await context.ChallengeAsync("oidc");
    }
});

Если вы знаете более чистый способ добиться этого перенаправления, пожалуйста, оставьте комментарий!