33
votes

The UseJwtBearerAuthentication middleware in ASP.NET Core makes it easy to validate incoming JSON Web Tokens in Authorization headers.

How do I authenticate a JWT passed via cookies, instead of a header? Something like UseCookieAuthentication, but for a cookie that just contains a JWT.

3
Curious: what's the point of using bearer tokens if you want to use cookies to flow them? The whole point of using bearer tokens instead of cookies is to avoid security concerns like XSRF attacks. If you reintroduce cookies in the equation, you reintroduce its threat model.Kévin Chalet
@Pinpoint JWTs aren't strictly bearer tokens; they can be used either via a Bearer header, or via cookies. I'm using JWTs to do stateless "sessions", but still storing them in cookies because the browser support is simple. XSS is mitigated by cookie flags.Nate Barbettini
1. by definition, JWTs are either bearer or PoP tokens (in the first case, you don't need to prove you're a legitimate holder of the token, in the second one, you need to give the server a proof of possession). 2. using JWTs to represent a "session" and storing them in an authentication cookie (which is itself a "session") makes no sense, I'm afraid. 3. XSS has nothing to do with XSRF, it's a completely different threat.Kévin Chalet
@Pinpoint I'm doing token authentication and storing the access token JWT in a (plaintext) cookie instead of HTML5 storage. I realize XSS != XSRF, you're absolutely right. I should have clarified: I'm picking cookies for strong security against XSS, which does mean I do to handle CSRF concerns.Nate Barbettini
TBH, your scenario really sounds like an odd mix between tokens and cookies. If you really want to use a cookie, then don't use token authentication at all and directly use cookies for authentication. You'll have to deal with XSRF risks but it won't be different from the scenario you're trying to achieve. IMHO, it's not really worth it, specially when you know that doing that can't really mitigate XSS attacks: don't forget that if I can't steal a HttpOnly cookie, nothing prevents me from sending a malicious API request on behalf of the user when there's a XSS flaw in your JS app.Kévin Chalet

3 Answers

41
votes

I suggest you take a look at the following link.

https://stormpath.com/blog/token-authentication-asp-net-core

They store JWT token in an http only cookie to prevent XSS attacks.

They then validate the JWT token in the cookie by adding the following code in the Startup.cs:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    AuthenticationScheme = "Cookie",
    CookieName = "access_token",
    TicketDataFormat = new CustomJwtDataFormat(
        SecurityAlgorithms.HmacSha256,
        tokenValidationParameters)
});

Where CustomJwtDataFormat() is their custom format defined here:

public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket>
{
    private readonly string algorithm;
    private readonly TokenValidationParameters validationParameters;

    public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
    {
        this.algorithm = algorithm;
        this.validationParameters = validationParameters;
    }

    public AuthenticationTicket Unprotect(string protectedText)
        => Unprotect(protectedText, null);

    public AuthenticationTicket Unprotect(string protectedText, string purpose)
    {
        var handler = new JwtSecurityTokenHandler();
        ClaimsPrincipal principal = null;
        SecurityToken validToken = null;

        try
        {
            principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);

            var validJwt = validToken as JwtSecurityToken;

            if (validJwt == null)
            {
                throw new ArgumentException("Invalid JWT");
            }

            if (!validJwt.Header.Alg.Equals(algorithm, StringComparison.Ordinal))
            {
                throw new ArgumentException($"Algorithm must be '{algorithm}'");
            }

            // Additional custom validation of JWT claims here (if any)
        }
        catch (SecurityTokenValidationException)
        {
            return null;
        }
        catch (ArgumentException)
        {
            return null;
        }

        // Validation passed. Return a valid AuthenticationTicket:
        return new AuthenticationTicket(principal, new AuthenticationProperties(), "Cookie");
    }

    // This ISecureDataFormat implementation is decode-only
    public string Protect(AuthenticationTicket data)
    {
        throw new NotImplementedException();
    }

    public string Protect(AuthenticationTicket data, string purpose)
    {
        throw new NotImplementedException();
    }
}

Another solution would be to write some custom middleware that would intercept each request, look if it has a cookie, extract the JWT from the cookie and add an Authorization header on the fly before it reaches the Authorize filter of your controllers. Here is some code that work for OAuth tokens, to get the idea:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;

namespace MiddlewareSample
{
    public class JWTInHeaderMiddleware
    {
        private readonly RequestDelegate _next;

        public JWTInHeaderMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task Invoke(HttpContext context)
        {
           var authenticationCookieName = "access_token";
           var cookie = context.Request.Cookies[authenticationCookieName];
           if (cookie != null)
           {
               var token = JsonConvert.DeserializeObject<AccessToken>(cookie);
               context.Request.Headers.Append("Authorization", "Bearer " + token.access_token);
           }

           await _next.Invoke(context);
        }
    }
}

... where AccessToken is the following class:

public class AccessToken
{
    public string token_type { get; set; }
    public string access_token { get; set; }
    public string expires_in { get; set; }
}

Hope this helps.

NOTE: It is also important to note that this way of doing things (token in http only cookie) will help prevent XSS attacks but however does not immune against Cross Site Request Forgery (CSRF) attacks, you must therefore also use anti-forgery tokens or set custom headers to prevent those.

Moreover, if you do not do any content sanitization, an attacker can still run an XSS script to make requests on behalf of the user, even with http only cookies and CRSF protection enabled. However, the attacker will not be able to steal the http only cookies that contain the tokens, nor will the attacker be able to make requests from a third party website.

You should therefore still perform heavy sanitization on user-generated content such as comments etc...

EDIT: It was written in the comments that the blog post linked and the code have been written by the OP himself a few days ago after asking this question.

For those who are interested in another "token in a cookie" approach to reduce XSS exposure they can use oAuth middleware such as the OpenId Connect Server in ASP.NET Core.

In the method of the token provider that is invoked to send the token back (ApplyTokenResponse()) to the client you can serialize the token and store it into a cookie that is http only:

using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using Newtonsoft.Json;

namespace Shared.Providers
{
public class AuthenticationProvider : OpenIdConnectServerProvider
{

    private readonly IApplicationService _applicationservice;
    private readonly IUserService _userService;
    public AuthenticationProvider(IUserService userService, 
                                  IApplicationService applicationservice)
    {
        _applicationservice = applicationservice;
        _userService = userService;
    }

    public override Task ValidateTokenRequest(ValidateTokenRequestContext context)
    {
        if (string.IsNullOrEmpty(context.ClientId))
        {
            context.Reject(
                error: OpenIdConnectConstants.Errors.InvalidRequest,
                description: "Missing credentials: ensure that your credentials were correctly " +
                             "flowed in the request body or in the authorization header");

            return Task.FromResult(0);
        }

        #region Validate Client
        var application = _applicationservice.GetByClientId(context.ClientId);

            if (applicationResult == null)
            {
                context.Reject(
                            error: OpenIdConnectConstants.Errors.InvalidClient,
                            description: "Application not found in the database: ensure that your client_id is correct");

                return Task.FromResult(0);
            }
            else
            {
                var application = applicationResult.Data;
                if (application.ApplicationType == (int)ApplicationTypes.JavaScript)
                {
                    // Note: the context is marked as skipped instead of validated because the client
                    // is not trusted (JavaScript applications cannot keep their credentials secret).
                    context.Skip();
                }
                else
                {
                    context.Reject(
                            error: OpenIdConnectConstants.Errors.InvalidClient,
                            description: "Authorization server only handles Javascript application.");

                    return Task.FromResult(0);
                }
            }
        #endregion Validate Client

        return Task.FromResult(0);
    }

    public override async Task HandleTokenRequest(HandleTokenRequestContext context)
    {
        if (context.Request.IsPasswordGrantType())
        {
            var username = context.Request.Username.ToLowerInvariant();
            var user = await _userService.GetUserLoginDtoAsync(
                // filter
                u => u.UserName == username
            );

            if (user == null)
            {
                context.Reject(
                        error: OpenIdConnectConstants.Errors.InvalidGrant,
                        description: "Invalid username or password.");
                return;
            }
            var password = context.Request.Password;

            var passWordCheckResult = await _userService.CheckUserPasswordAsync(user, context.Request.Password);


            if (!passWordCheckResult)
            {
                context.Reject(
                        error: OpenIdConnectConstants.Errors.InvalidGrant,
                        description: "Invalid username or password.");
                return;
            }

            var roles = await _userService.GetUserRolesAsync(user);

            if (!roles.Any())
            {
                context.Reject(
                        error: OpenIdConnectConstants.Errors.InvalidRequest,
                        description: "Invalid user configuration.");
                return;
            }
        // add the claims
        var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
        identity.AddClaim(ClaimTypes.NameIdentifier, user.Id, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
        identity.AddClaim(ClaimTypes.Name, user.UserName, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
         // add the user's roles as claims
        foreach (var role in roles)
        {
            identity.AddClaim(ClaimTypes.Role, role, OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken);
        }
         context.Validate(new ClaimsPrincipal(identity));
        }
        else
        {
            context.Reject(
                    error: OpenIdConnectConstants.Errors.InvalidGrant,
                    description: "Invalid grant type.");
            return;
        }

        return;
    }

    public override Task ApplyTokenResponse(ApplyTokenResponseContext context)
    {
        var token = context.Response.Root;

        var stringified = JsonConvert.SerializeObject(token);
        // the token will be stored in a cookie on the client
        context.HttpContext.Response.Cookies.Append(
            "exampleToken",
            stringified,
            new Microsoft.AspNetCore.Http.CookieOptions()
            {
                Path = "/",
                HttpOnly = true, // to prevent XSS
                Secure = false, // set to true in production
                Expires = // your token life time
            }
        );

        return base.ApplyTokenResponse(context);
    }
}
}

Then you need to make sure each request has the cookie attached to it. You must also write some middleware to intercept the cookie and set it to the header:

public class AuthorizationHeader
{
    private readonly RequestDelegate _next;

    public AuthorizationHeader(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var authenticationCookieName = "exampleToken";
        var cookie = context.Request.Cookies[authenticationCookieName];
        if (cookie != null)
        {

            if (!context.Request.Path.ToString().ToLower().Contains("/account/logout"))
            {
                if (!string.IsNullOrEmpty(cookie))
                {
                    var token = JsonConvert.DeserializeObject<AccessToken>(cookie);
                    if (token != null)
                    {
                        var headerValue = "Bearer " + token.access_token;
                        if (context.Request.Headers.ContainsKey("Authorization"))
                        {
                            context.Request.Headers["Authorization"] = headerValue;
                        }else
                        {
                            context.Request.Headers.Append("Authorization", headerValue);
                        }
                    }
                }
                await _next.Invoke(context);
            }
            else
            {
                // this is a logout request, clear the cookie by making it expire now
                context.Response.Cookies.Append(authenticationCookieName,
                                                "",
                                                new Microsoft.AspNetCore.Http.CookieOptions()
                                                {
                                                    Path = "/",
                                                    HttpOnly = true,
                                                    Secure = false,
                                                    Expires = DateTime.UtcNow.AddHours(-1)
                                                });
                context.Response.Redirect("/");
                return;
            }
        }
        else
        {
            await _next.Invoke(context);
        }
    }
}

In Configure() of startup.cs:

    // use the AuthorizationHeader middleware
    app.UseMiddleware<AuthorizationHeader>();
    // Add a new middleware validating access tokens.
    app.UseOAuthValidation();

You can then use the Authorize attribute normally.

    [Authorize(Roles = "Administrator,User")]

This solution works for both api and mvc apps. For ajax and fetch requests however your must write some custom middleware that will not redirect the user to the login page and instead return a 401:

public class RedirectHandler
{
    private readonly RequestDelegate _next;

    public RedirectHandler(RequestDelegate next)
    {
        _next = next;
    }

    public bool IsAjaxRequest(HttpContext context)
    {
        return context.Request.Headers["X-Requested-With"] == "XMLHttpRequest";
    }

    public bool IsFetchRequest(HttpContext context)
    {
        return context.Request.Headers["X-Requested-With"] == "Fetch";
    }

    public async Task Invoke(HttpContext context)
    {
        await _next.Invoke(context);
        var ajax = IsAjaxRequest(context);
        var fetch = IsFetchRequest(context);
        if (context.Response.StatusCode == 302 && (ajax || fetch))
        {
            context.Response.Clear();
            context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
            await context.Response.WriteAsync("Unauthorized");
            return;
        }
    }
}
13
votes

I implemented the middleware successfully (based on Darxtar answer):

// TokenController.cs

public IActionResult Some()
{
    ...

    var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

    Response.Cookies.Append(
        "x",
        tokenString,
        new CookieOptions()
        {
            Path = "/"
        }
    );

    return StatusCode(200, tokenString);
}


// JWTInHeaderMiddleware.cs

public class JWTInHeaderMiddleware
{
    private readonly RequestDelegate _next;

    public JWTInHeaderMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        var name = "x";
        var cookie = context.Request.Cookies[name];

        if (cookie != null)
            if (!context.Request.Headers.ContainsKey("Authorization"))
                context.Request.Headers.Append("Authorization", "Bearer " + cookie);

        await _next.Invoke(context);
    }
}

// Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    ...

    app.UseMiddleware<JWTInHeaderMiddleware>();

    ...
}
0
votes

You can also use Events.OnMessageReceived property of JwtBearerOptions class

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddCookie()
.AddJwtBearer(options =>
{
    /*other settings*/
    options.Events = new()
    {
        OnMessageReceived = context =>
        {
            var request = context.HttpContext.Request;
            var cookies = request.Cookies;
            if (cookies.TryGetValue("AccessTokenCookieName",
                out var accessTokenValue))
            {
                context.Token = accessTokenValue;
                return Task.CompletedTask;
            }
            return Task.CompletedTask;
        };
    };
})