ASP.NET Core 2.0 combining Cookies and Bearer Authorization for the same endpoint

maximiniini picture maximiniini · Oct 25, 2017 · Viewed 13.3k times · Source

I've created a new ASP.NET Core Web Application project in VS17 using the "Web Application (Model-View-Controller)" template and ".Net Framework" + "ASP.NET Core 2" as the configuration. The authentication config is set to "Individual User Accounts".

I have the following sample endpoint:

[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" is derived by concatenating CookieAuthenticationDefaults.AuthenticationScheme and JwtBearerDefaults.AuthenticationScheme.

The objective is to be able to configure the authorization for the end point so that it's possible access it using both the token and cookie authentication methods.

Here is the setup I have for Authentication in my 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"]))
                                                };
        });

So, when I try to access the endpoint using a browser, I get the 401 response with a blank html page.
I get the 401 response with a blank html page.

Then I login and when I try to access the endpoint again, I get the same response.

Then, I try to access the endpoint by specifying the bearer token. And that returns the desired result with the 200 response.
And that returns the desired result with the 200 response.

So then, if I remove [Authorize(AuthenticationSchemes = "Cookies,Bearer")], the situation becomes the opposite - cookie authentication works and returns 200, however the same bearer token method as used above doesn't give any results and just redirect to the default AspIdentity login page.

I can see two possible problems here:

1) ASP.NET Core doesn't allow 'combined' authentication. 2) 'Cookies' is not a valid schema name. But then what is the right one to use?

Please advise. Thank you.

Answer

David Kirkland picture David Kirkland · Apr 11, 2019

If I understand the question correctly then I believe that there is a solution. In the following example I am using cookie AND bearer authentication in a single app. The [Authorize] attribute can be used without specifying the scheme, and the app will react dynamically, depending on the method of authorization being used.

services.AddAuthentication is called twice to register the 2 authentication schemes. The key to the solution is the call to services.AddAuthorization at the end of the code snippet, which tells ASP.NET to use BOTH schemes.

I've tested this and it seems to work well.

(Based on Microsoft docs.)

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

EDIT

This works for authenticated users, but simply returns a 401 (unauthorized) if a user has not yet logged in.

To ensure that unauthorized users are redirected to the login page, add the following code to the Configure method in your Startup class. Note: it's essential that the new middleware is placed after the call the 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");
    }
});

If you know a cleaner way to achieve this redirect, please post a comment!