In this article, we will show you how to use HttpOnly Cookie in .NET Core apps to secure our JWT or JSON Web Tokens, when implementing the authentication and refresh token actions.

So, let’s start.

The Standard Flow for the Authentication Logic Using JWT

When we follow the usual flow with the JWTs:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

jwt access token flow

We can see that once the client sends the authentication request, they get the access token, which we then usually store inside the local or session storage.

Both storages are considered not that secure, and of course, we can increase the security of our tokens by encrypting them before storing them inside the storage. Then we can decrypt them before sending them with the request. In that case, if an attacker somehow gets access to the storage the tokens will be of no use for them.

But, there is another option, which is considered a bit better and more secure. Of course, we are talking about the HttpOnly cookies.

What is an HttpOnly Cookie?

An HttpOnly Cookie is a tag that we add to a browser cookie to prevent client-side scripts from accessing data. It provides a gate that we use to prevent specialized cookies from being accessed by anything other than the server. When we generate a cookie, using the HttpOnly tag helps mitigate the risk of client-side scripts accessing the protected cookie, thus making these cookies more secure.

So, with all this in mind, let’s see how we can implement the HttpOnly cookie in .NET Core application with the authentication and refresh token action.

Implement HttpOnly Cookie in .NET Core Application

Now, we already have a project with both the authentication and refresh actions implemented:

Project solution for Using HttpOnly Cookie in .NET Core apps

Here, we are using the regular way where the tokens are sent to the client using the response body:

[HttpPost("login")]
public async Task<IActionResult> Authenticate([FromBody] UserForAuthenticationDto user)
{
    if (!await _service.AuthenticationService.ValidateUser(user))
        return Unauthorized();

    var tokenDto = await _service.AuthenticationService.CreateToken(populateExp: true);

    return Ok(tokenDto);
}

This is the code we used in our Refresh Token video, so we won’t dive into the authentication or refresh token logic here. If you want to learn more about both, you can watch the video, or you can read about the Refresh Token logic and the JWT authentication logic.

Ok, the first thing first.

Let’s start by modifying our JWT configuration inside the ServiceExtensions class:

.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,

        ValidIssuer = jwtSettings["validIssuer"],
        ValidAudience = jwtSettings["validAudience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey))
    };

    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = ctx =>
        {
            ctx.Request.Cookies.TryGetValue("accessToken", out var accessToken);
            if (!string.IsNullOrEmpty(accessToken))
                ctx.Token = accessToken;

            return Task.CompletedTask;
        }
    };
});

So, to avoid any confusion, we are using the JWT authentication and we will continue to use the JWT authentication even after we implement the HttpOnly cookie logic. The cookie will be used as a transporting mechanism and nothing more – so we are not using the cookie authentication here and we don’t have to register the cookie middleware.

But what we have to do is extend the JwtBearer configuration with the Events property and instantiate it with a new JwtBearerEvents class… Then inside, we call the OnMessageReceived property that acts as an event here once the authentication protocol message is received. This is a Func delegate that accepts the context as a parameter.

With the help of the context parameter, we call the Request.Cookies property trying to extract the value of the accessToken cookie and place it inside the accessToken local variable. Of course, only if we find the accessToken, we use the Token property and set it to the value of the accessToken.

Finally, we return the Task.completedTask.

Using TestController To Authorize the Request With the HttpOnly Cookie Attached

Now, to explain this a bit. If you check the test controller:

[Route("api/test")]
[ApiController]
public class TestController : ControllerBase
{
    [HttpGet]
    [Authorize]
    public IActionResult TestAction()
    {
        return Ok();
    }
}

It has a single dummy Get action and it is decorated with the Authorize attribute. As soon as the request reaches that Authorize attribute, the OnMessageReceived event will be triggered. Then, we simply want to extract the token’s value from the cookie and set it as the Token for the validation process.

Also, to make it clear, we will store both the access and the refresh tokens inside the HttpOnly cookie, but for the authorization part, we only need the access token.

That’s all regarding the configuration, and we can move on to modify the logic inside the Authentication controller.

Modify the Authentication Action to Use the HttpOnly Cookie in .NET Core Apps.

In this controller, we have different actions, but the Authenticate action is the one that concerns us:

[HttpPost("login")]
public async Task<IActionResult> Authenticate([FromBody] UserForAuthenticationDto user)
{
    if (!await _service.AuthenticationService.ValidateUser(user))
        return Unauthorized();

    var tokenDto = await _service.AuthenticationService.CreateToken(populateExp: true);

    return Ok(tokenDto);
}

You can see we are calling the authentication service to do the heavy lifting regarding the authentication logic and the token creation logic, and then, we return both the access and the refresh tokens to the controller:

public async Task<TokenDto> CreateToken(bool populateExp)
{
    …
    var accessToken = new JwtSecurityTokenHandler().WriteToken(tokenOptions);

    return new TokenDto(accessToken, refreshToken);
}

Then, we simply return the tokenDto containing both tokens to the client.

Well, now, we don’t want to do that. What we want to do is to set both tokens inside the HttpOnly cookie, and send it to the client.

Authentication Service Modification

So, let’s return to the authentication service class and create a new method that will store the tokens inside the cookie. We are creating it inside the authentication service so we can reuse it for both the authentication and the refresh logic:

public void SetTokensInsideCookie(TokenDto tokenDto, HttpContext context)
{
    context.Response.Cookies.Append("accessToken", tokenDto.AccessToken,
        new CookieOptions
        {
            Expires = DateTimeOffset.UtcNow.AddMinutes(5),
            HttpOnly = true,
            IsEssential = true,
            Secure = true,
            SameSite = SameSiteMode.None
        });

    context.Response.Cookies.Append("refreshToken", tokenDto.RefreshToken,
        new CookieOptions
        {
            Expires = DateTimeOffset.UtcNow.AddDays(7),
            HttpOnly = true,
            IsEssential = true,
            Secure = true,
            SameSite = SameSiteMode.None
        });
}

Inside the method, we use the context parameter, call the Cookies properties, and then call the Append method to add a new cookie. We have to provide the key first, and this key must be the same as the one we used inside the configuration event. Then we need the value, which is the generated access token.

Finally, we need to set up some cookie options using the CookieOptions class.

This cookie entry will expire in five minutes, and this is the exact value for the access token expiration period. Also, we want this one to be an HttpOnly cookie, and we will state it is essential for the app to work. Additionally, we want to enable a cookie transfer via SSL or HTTPS only with the Secure property, and finally, let’s use the SameSite property and populate it to SameSiteMode.None.

We do the same for the refresh token with some small changes for the names and the expiration period.

That’s it.

Of course, we have to modify the IAuthenticationService interface, and add the same member there:

void SetTokensInsideCookie(TokenDto tokenDto, HttpContext context);

AuthenticationController Modification to Use the HttpOnly Cookie 

Now, we can go back to my authentication controller:

[HttpPost("login")]
public async Task<IActionResult> Authenticate([FromBody] UserForAuthenticationDto user)
{
    if (!await _service.AuthenticationService.ValidateUser(user))
        return Unauthorized();

    var tokenDto = await _service.AuthenticationService.CreateToken(populateExp: true);

    _service.AuthenticationService.SetTokensInsideCookie(tokenDto, HttpContext);

    return Ok();
}

Here, we first remove the payload from the Ok method. We don’t want to return any token inside the response body.

Then, we use the AuthenticationService and then the SetTokens method to set the required tokens inside the cookie.

At this point, we will return a 200 status code to the client but the cookie with the tokens will be sent separately.

Let’s test this.

Let’s first send a request to the TestController:

https://localhost:5001/api/test

We get the 401 unauthorized response, and we expect that since we protect the action with the Authorize attribute.

Now, let’s send the request to the authentication action:

POST actions that will trigger adding the HttpOnly cookie in .NET

We get the 200 OK result, but if we check the Cookies link:

C:\Users\a\Desktop\HttpOnly Cookies in .NET stored in Postman.png

We can see we have both tokens here. You can inspect both tokens to find both token values and other properties we set in the cookie options class.

Excellent.

Now, with the cookies stored inside the Postman, we can send the previous request again. Again, we will get a 200 OK result. This means the authentication works.

Implement HttpOnly Cookie in .NET for the Refresh Token Logic

Great, now we can move to the refresh logic:

[HttpPost("refresh")]
public async Task<IActionResult> Refresh([FromBody] TokenDto tokenDto)
{
    var tokenDtoToReturn = await _service.AuthenticationService.RefreshToken(tokenDto);

    return Ok(tokenDtoToReturn);
}

Well, to be honest, we need to make only a slight change here. We don’t want to modify anything inside the service layer, so just this part inside the action.

As you can see, right now, we are accepting both the accessToken and the refreshToken inside the request body. But now, we want to do it differently:

[HttpPost("refresh")]
public async Task<IActionResult> Refresh()
{
    HttpContext.Request.Cookies.TryGetValue("accessToken", out var accessToken);
    HttpContext.Request.Cookies.TryGetValue("refreshToken", out var refreshToken);

    var tokenDto = new TokenDto(accessToken, refreshToken);

    var tokenDtoToReturn = await _service.AuthenticationService.RefreshToken(tokenDto);

    _service.AuthenticationService.SetTokensInsideCookie(tokenDtoToReturn, HttpContext);

    return Ok();
}

So, we remove the input parameter from the action. What I want to do is to extract both entries from the cookie manually.

To do so, we use the HttpContext property, and then call the Request.Cookies property and call the TryGetValue method to try to extract the accessToken inside the local variable named accessToken.

We do the same for the refreshToken with some small name changes.

Now, once we have both values, we create a new tokenDto variable and populate it with both the access and the refresh tokens.

Then, we again use the authentication service and call the SetTokensInsideCookie method with both required arguments to add new tokens into a cookie – tokens created inside the RefreshToken method.
Finally, I don’t want to return anything inside the response body.

And that’s all.

I can again start the app… and repeat the flow, but first remove the cookies from the postman.

Then, once we send the test request, we get 401. After that, we authenticate the user, and get the cookies… Of course, this will allow us to access the test action…

Finally, we can send the refresh token request:

POST: https://localhost:5001/api/token/refresh

Again we get a 200 OK result. If we inspect the token, we can find a different refresh token and a different access token.

Great.

Conclusion

In this article, we learned about how to implement the HttpOnly Cookie in .NET applications with the authentication and refresh token actions. This feature can improve the security of our tokens and prevent client-side scripts from manipulating the tokens.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!