How can I validate a JWT passed via cookies?

28,942

Solution 1

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

Solution 2

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

    ...
}

Solution 3

You can also use Events.OnMessageReceived property of JwtBearerOptions class

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddCookie()
.AddJwtBearer(options =>
{
    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;
        };
    };
})
Share:
28,942
Nate Barbettini
Author by

Nate Barbettini

Developer, author, and speaker. I'm passionate about helping people learn how to write clean, secure code. I wrote The Little ASP.NET Core Book, a free book that teaches you how to build web apps with ASP.NET Core. I also have deep-dive video courses published on LinkedIn Learning: Building and Securing RESTful APIs in ASP.NET Core Deploying ASP.NET Core Applications Learning SignalR with ASP.NET Core

Updated on July 09, 2022

Comments

  • Nate Barbettini
    Nate Barbettini almost 2 years

    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.

  • KreepN
    KreepN over 7 years
    I have to ask, did you ever check who the author of that blog post was? i.ytimg.com/vi/OGAu_DeKckI/hqdefault.jpg
  • Darxtar
    Darxtar over 7 years
    You make a very valid point, no I didn't check the author. I'll look into a more objective solution. I have made some custom auth validation equivalent using oauth2, I'll edit that soon to provide an alternative.
  • KreepN
    KreepN over 7 years
    Lol, I'm still not sure you notice: you linked the OP his own blog post and code. That is all I was asking.
  • Darxtar
    Darxtar over 7 years
    Yes I noticed. Hence why I want to provide an alternative solution, that is not only what the OP has written.
  • O.MeeKoh
    O.MeeKoh about 5 years
    Was the cookie you implemented with this solution indeed an HttpOnly cookie? It seemed that the cookie sent to the browser was just a regular cookie that contained the JWT token for my situation.
  • lolol
    lolol about 5 years
    @O.MeeKoh Change the CookieOptions to new CookieOptions { HttpOnly = true });
  • O.MeeKoh
    O.MeeKoh about 5 years
    I actually got it all to work nicely. Once i restarted my front end server it all started working. I am also using same-site = strict, this should be more secure option than localStorage.
  • Sudheer Muhammed
    Sudheer Muhammed almost 5 years
    Thank you Darxtar, implemented middle ware solution in my application and it is working great.
  • SoroushNeshat
    SoroushNeshat almost 5 years
    i really enjoyed the middleware approach. brilliant :)
  • Clint Simon
    Clint Simon about 4 years
    Huge help. I went with the virtual bearer authorization header method. Works like a charm.
  • Antonio Buonaiuto
    Antonio Buonaiuto over 2 years
    Indeed a simpler solution is already available to swap the location of where the JWT token is retrieved using the Events options in the AddJwtBearer implementing the OnMessageReceived and setting the context.Token value. Sample code here: spin.atomicobject.com/2020/07/25/…
  • jjxtra
    jjxtra over 2 years
    In the words of the Mandalorian, This is the Way...