ASP.NET Core WebAPI Cookie + JWT Authentication

28,725

Solution 1

Okay, I have been trying achieving this for a while and i solved same issue of using jwt Authentication Tokens and Cookie Authentication with the following code.

API Service Provider UserController.cs

This Provide Different Services to the User with Both (Cookie and JWT Bearer)Authentication Schemes

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] 
[Route("[controller]")]
[ApiController]
public class UsersController : ControllerBase
{ 
    private readonly IUserServices_Api _services;
    public UsersController(IUserServices_Api services)
    {
        this._services = services;
    }
     
    [HttpGet]
    public IEnumerable<User> Getall()
    {
        return _services.GetAll();
    }
}

My Startup.cs

public void ConfigureServices(IServiceCollection services)
    {
          
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
         
        services.AddAuthentication(options => {
            options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
            .AddCookie(options =>
            {
                options.LoginPath = "/Account/Login";
                options.AccessDeniedPath = "/Home/Error";
            })
            .AddJwtBearer(options =>
            {
                options.SaveToken = true;
                options.RequireHttpsMetadata = false;
                options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidAudience = " you site link blah blah",
                    ValidIssuer = "You Site link Blah  blah",
                    IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(sysController.GetSecurityKey()))
                    ,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero
                };
            });

    }

And further if you want custom Authentication for a specific Controller then you have to specify the Authentitcation Type for the Authorization like:

[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
public IActionResult Index()
{
    return View();    // This can only be Access when Cookie Authentication is Authorized.
}

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public IActionResult Index()
{
    return View();    // And this one will be Access when JWT Bearer is Valid
}

Solution 2

I've been having the same issue and i just found what it seems to be the solution in another question here in stackoverflow.

Please take a look at this.

I'll try that solution myself and update this answer with the results.

Edit: It seems it's not possible to achieve double authentication types in a same method but the solution provided in the link i mentioned says:

It's not possible to authorize a method with two Schemes Or-Like, but you can use two public methods, to call a private method

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

Anyway you should take a look at the link, it sure helped me. Credit goes to Nikolaus for the answer.

Solution 3

I have not been able to find much information on a good way to do this - having to duplicate the API is a pain just to support 2 authorization schemes.

I have been looking into the idea of using a reverse proxy and it looks to me like a good solution for this.

  1. User signs into Website (use cookie httpOnly for session)
  2. Website uses Anti-Forgery token
  3. SPA sends request to website server and includes anti-forgery token in header: https://app.mydomain.com/api/secureResource
  4. Website server verifies anti-forgery token (CSRF)
  5. Website server determines request is for API and should send it to the reverse proxy
  6. Website server gets users access token for API
  7. Reverse proxy forwards request to API: https://api.mydomain.com/api/secureResource

Note that the anti-forgery token (#2,#4) is critical or else you could expose your API to CSRF attacks.


Example (.NET Core 2.1 MVC with IdentityServer4):

To get a working example of this I started with the IdentityServer4 quick start Switching to Hybrid Flow and adding API Access back. This sets up the scenario I was after where a MVC application uses cookies and can request an access_token from the identity server to make calls the API.

I used Microsoft.AspNetCore.Proxy for the reverse proxy and modified the quick start.

MVC Startup.ConfigureServices:

services.AddAntiforgery();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

MVC Startup.Configure:

app.MapWhen(IsApiRequest, builder =>
{
    builder.UseAntiforgeryTokens();

    var messageHandler = new BearerTokenRequestHandler(builder.ApplicationServices);
    var proxyOptions = new ProxyOptions
    {
        Scheme = "https",
        Host = "api.mydomain.com",
        Port = "443",
        BackChannelMessageHandler = messageHandler
    };
    builder.RunProxy(proxyOptions);
});

private static bool IsApiRequest(HttpContext httpContext)
{
    return httpContext.Request.Path.Value.StartsWith(@"/api/", StringComparison.OrdinalIgnoreCase);
}

ValidateAntiForgeryToken (Marius Schulz):

public class ValidateAntiForgeryTokenMiddleware
{
    private readonly RequestDelegate next;
    private readonly IAntiforgery antiforgery;

    public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
    {
        this.next = next;
        this.antiforgery = antiforgery;
    }

    public async Task Invoke(HttpContext context)
    {
        await antiforgery.ValidateRequestAsync(context);
        await next(context);
    }
}

public static class ApplicationBuilderExtensions
{
    public static IApplicationBuilder UseAntiforgeryTokens(this IApplicationBuilder app)
    {
        return app.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
    }
}

BearerTokenRequestHandler:

public class BearerTokenRequestHandler : DelegatingHandler
{
    private readonly IServiceProvider serviceProvider;

    public BearerTokenRequestHandler(IServiceProvider serviceProvider, HttpMessageHandler innerHandler = null)
    {
        this.serviceProvider = serviceProvider;
        InnerHandler = innerHandler ?? new HttpClientHandler();
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
        var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
        request.Headers.Authorization =new AuthenticationHeaderValue("Bearer", accessToken);
        var result = await base.SendAsync(request, cancellationToken);
        return result;
    }
}

_Layout.cshtml:

@Html.AntiForgeryToken()

Then using your SPA framework you can make a request. To verify I just did a simple AJAX request:

<a onclick="sendSecureAjaxRequest()">Do Secure AJAX Request</a>
<div id="ajax-content"></div>

<script language="javascript">
function sendSecureAjaxRequest(path) {
    var myRequest = new XMLHttpRequest();
    myRequest.open('GET', '/api/secureResource');
    myRequest.setRequestHeader("RequestVerificationToken",
        document.getElementsByName('__RequestVerificationToken')[0].value);
    myRequest.onreadystatechange = function () {
        if (myRequest.readyState === XMLHttpRequest.DONE) {
            if (myRequest.status === 200) {
                document.getElementById('ajax-content').innerHTML = myRequest.responseText;
            } else {
                alert('There was an error processing the AJAX request: ' + myRequest.status);
            }
        }  
    };
    myRequest.send();
};
</script>

This was a proof of concept test so your mileage may very and I'm pretty new to .NET Core and middleware configuration so it could probably look prettier. I did limited testing with this and only did a GET request to the API and did not use SSL (https).

As expected, if the anti-forgery token is removed from the AJAX request it fails. If the user is has not logged in (authenticated) the request fails.

As always, each project is unique so always verify your security requirements are met. Please take a look at any comments left on this answer for any potential security concerns someone might raise.

On another note, I think once subresource integrity (SRI) and content security policy (CSP) is available on all commonly used browsers (i.e. older browsers are phased out) local storage should be re-evaluated to store API tokens which will lesson the complexity of token storage. SRI and CSP should be used now to help reduce the attack surface for supporting browsers.

Solution 4

I think the easiest solution is one proposed by David Kirkland:

Create combined authorization policy (in ConfigureServices(IServiceCollection services)):

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

And add middleware that will redirect to login in case of 401 (in Configure(IApplicationBuilder app)):

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

Solution 5

while looking for combined firebase authorization with net core web api (cookie for web site and authorization header for mobile app ) end with the following solution.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
               .AddJwtBearer(options =>
               {
                   options.Authority = "https://securetoken.google.com/xxxxx";
                   options.TokenValidationParameters = new TokenValidationParameters
                   {
                       ValidateIssuer = true,
                       ValidIssuer = options.Authority,
                       ValidateAudience = true,
                       ValidAudience = "xxxxx",
                       ValidateLifetime = true
                   };
                   options.Events = new JwtBearerEvents
                   {
                       OnMessageReceived = context =>
                       {
                           if (context.Request.Cookies.ContainsKey(GlobalConst.JwtBearer))
                           {
                               context.Token = context.Request.Cookies[GlobalConst.JwtBearer];
                           }
                           else if (context.Request.Headers.ContainsKey("Authorization"))
                                {
                                    var authhdr = context.Request.Headers["Authorization"].FirstOrDefault(k=>k.StartsWith(GlobalConst.JwtBearer));
                                    if (!string.IsNullOrEmpty(authhdr))
                                    {
                                        var keyval = authhdr.Split(" ");
                                        if (keyval != null && keyval.Length > 1) context.Token = keyval[1];
                                    }
                                }
                           return Task.CompletedTask;
                       }
                   };
               });

where

 public static readonly string JwtBearer = "Bearer";

seems working fine. checked it from mobile & postman (for cookie )

Share:
28,725
Luke1988
Author by

Luke1988

Updated on August 22, 2021

Comments

  • Luke1988
    Luke1988 over 2 years

    we have a SPA (Angular) with API backend (ASP.NET Core WebAPI):

    SPA is listens on app.mydomain.com, API on app.mydomain.com/API

    We use JWT for Authentication with built-in Microsoft.AspNetCore.Authentication.JwtBearer; I have a controller app.mydomain.com/API/auth/jwt/login which creates tokens. SPA saves them into local storage. All works perfect. After a security audit, we have been told to switch local storage for cookies.

    The problem is, that API on app.mydomain.com/API is used by SPA but also by a mobile app and several customers server-2-server solutions.

    So, we have to keep JWT as is, but add Cookies. I found several articles which combines Cookies and JWT on different controllers, but I need them work side-by-side on each controller.

    If client sends cookies, authenticate via cookies. If client sends JWT bearer, authenticate via JWT.

    Is this achievable via built-in ASP.NET authentication or DIY middleware?

    Thanks!