Home .NET A secureway to exchange JWT in ASP.NET Core + SPA

A secureway to exchange JWT in ASP.NET Core + SPA

by admin

Introduction

JWT (JSON Web Token) authentication is a fairly uniform, consistent authorization and authentication mechanism between server and clients.The advantages of JWT are that it gives us less state management and scales well.Not surprisingly, authorization and authentication using it is increasingly used in modern web applications.
When developing applications with JWT the question often arises: where and how is it recommended to store the token? If we are developing a web application, we have the two most common options :

  • HTML5 Web Storage (localStorage or sessionStorage)
  • Cookies

Comparing these methods, we can say that they both store values in the client browser, both are quite easy to use and are the usual storage of key-value pairs.The difference is in the storage environment.
Web Storage (localStorage/sessionStorage) is accessible via JavaScript in the same domain. This means that any JavaScript code in your application has access to Web Storage, and this creates vulnerability to cross-site scripting (XSS) attacks. As a storage mechanism, Web Storage doesn’t provide any way to secure your data during storage and sharing. We can use it only for auxiliary data we want to keep when refreshing (F5) or closing a tab: status and page number, filters, etc.
Tokens can also be transferred via browser cookies. Cookies used with the flag httpOnly , are not vulnerable to XSS. httpOnly is a flag for access to read, write and delete cookies only on the server. They will not be accessible through JavaScript on the client, so the client will not be aware of the token, and authorization will be handled entirely on the server side.
We can also install secure flag to ensure that the cookie is only sent over HTTPS. Given these advantages, my choice was cookies.
This article describes an approach to implement authorization and authentication using httpOnly secure cookies + JSON Web Token in ASP.NET Core Web Api in conjunction with SPA. A variant where the server and the client are in different origin is considered.

Setting up a local development environment

In order to properly configure and debug the client-server relationship over HTTPS, I highly recommend setting up your local development environment so that both client and server have an HTTPS connection right away.
If you don’t do that right away and try to build a relationship without an HTTPS connection, you’ll end up with a lot of little things, without which secure cookies and additional secure-policy in a production with HTTPS won’t work correctly.
I’ll show you an example of setting up HTTPS on OS Windows 10, the server is ASP.NET Core, the SPA is React.
To configure HTTPS in ASP.NET Core, you can use the Configurefor HTTPS flag during project creation or, if we didn’t do it during creation, turn on the corresponding option in the Properties.
To configure the SPA, you need to modify the script to "start" by setting it to "set HTTPS=true" My setup looks like this :

'start': 'set HTTPS=truerimraf ./buildreact-scripts start'

To configure HTTPS for the development environment in other environments I suggest to look at create-react-app.dev/docs/using-https-in-development

Configuring ASP.NET Core Server

JWT Setup

In this case, the most common JWT implementation from the documentation or any article will do, with the additional tuning options.RequireHttpsMetadata = true; because our development environment uses HTTPS:
ConfigureServices

services.AddAuthentication(options =>{options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>{options.RequireHttpsMetadata = true;options.SaveToken = true;options.TokenValidationParameters = new TokenValidationParameters{// your additional config};});

CORS policy setting

Important : CORS-policy must contain AllowCredentials() This is needed to get the request with XMLHttpRequest.withCredentials and send the cookies back to the client. This will be explained in more detail later in this article. The rest of the options are configured according to the needs of the project.
If the server and the client are on the same origin, the whole configuration below is unnecessary.
ConfigureServices

services.AddCors();

Configure

app.UseCors(x => x.WithOrigins("https://localhost:3000") // the path to our SPA client.AllowCredentials().AllowAnyMethod().AllowAnyHeader())

Cookie policy setting

Forcibly set cookie-policy to httpOnly and secure.
If possible, install MinimumSameSitePolicy = SameSiteMode.Strict; – This increases the security of cookies for application types that don’t rely on cross-origin request handling.
Configure

app.UseCookiePolicy(new CookiePolicyOptions{MinimumSameSitePolicy = SameSiteMode.Strict, HttpOnly = HttpOnlyPolicy.Always, Secure = CookieSecurePolicy.Always});

The idea of secure token exchange

This part is a concept. We are going to do two things :

  1. Sneak token into HTTP request using httpOnly and secure flags.
  2. Get and validate client application tokens from an HTTP request.

For this we need :

  • Write token to httpOnly cookie when login and delete token from httpOnly cookie when logout.
  • If you have a token in cookies, substitute a token in the HTTP header of each subsequent request.
  • If there is no token in the cookies, the header will be empty and the request will not be authorized.

Middleware

The basic idea is to implement Custom Middleware to insert token into incoming HTTP request. Once the user is authorized, we store the cookie under a specific key, such as : ".AspNetCore.Application.Id" I recommend specifying a name that has nothing to do with authorization or tokens, in which case the token cookie will look like some unremarkable AspNetCore system constant of the application. This way, there is a higher chance that an attacker will see a lot of system variables and, without understanding which authorization mechanism is being used, will move on. Of course, if he doesn’t read this article and doesn’t look for such a constant on purpose.
Next, we need to insert this token into all future incoming HTTP requests. To do this, we will write a few lines of Middleware code. This is nothing less than an HTTP-pipeline.
A secureway to exchange JWT in ASP.NET Core + SPA
Configure

app.Use(async (context, next) =>{var token = context.Request.Cookies[".AspNetCore.Application.Id"];if (!string.IsNullOrEmpty(token))context.Request.Headers.Add("Authorization", "Bearer " + token);await next();});app.UseAuthentication();

We can put this logic in a separate Middleware service, so it won’t clutter up Startup.cs, but it won’t change the idea.
In order to write a value to cookies, we just need to add the following line to the authorization logic :

if (result.Succeeded)HttpContext.Response.Cookies.Append(".AspNetCore.Application.Id", token, new CookieOptions{MaxAge = TimeSpan.FromMinutes(60)});

With our cookie-policy these cookies will automatically be sent as httpOnly and secure. You don’t need to override their cookie policy in cookie options.
In CookieOptions you can set MaxAge to specify the lifetime. This is useful to specify along with JWT Lifetime when issuing a token, so that the cookie disappears when the time expires. The rest of the CookieOptions properties are configurable depending on project requirements.
For more security, I suggest adding the following headers to Middleware :

context.Response.Headers.Add("X-Content-Type-Options", "nosniff");context.Response.Headers.Add("X-Xss-Protection", "1");context.Response.Headers.Add("X-Frame-Options", "DENY");

  • Title X-Content-Type-Options is used to protect against MIME sniffing type vulnerabilities. This vulnerability can occur when a site allows users to download content, but the user masks a specific file type as something else. This can give attackers the ability to perform cross-site scripting or compromise the website.
  • All modern browsers have built-in XSS filtering capabilities that try to catch XSS vulnerabilities before the page is fully displayed to us. These are enabled in the browser by default, but the user can be trickier and disable them. Using the header X-XSS-Protection we can actually tell the browser to ignore what the user has done and apply the built-in filter.
  • X-Frame-Options tells the browser that if your site is placed inside an HTML frame, it will not display anything. This is very important when trying to protect yourself from clickjacking attempts.

I haven’t described all of the headers. There are plenty more ways to make your web application more secure. I suggest you take a look at the security checklist from securityheaders.com

Setting up the SPA client

If the client and the server are located on different origin, additional configuration is required on the client as well. It is necessary to wrap each request by using XMLHttpRequest.withCredentials
I wrapped my methods as follows :

import axios from "axios";const api = axios.create({ baseURL: process.env.REACT_APP_API_URL });api.interceptors.request.use(request => requestInterceptor(request))const requestInterceptor = (request) => {request.withCredentials = true;return request;}export default api;

We can wrap our request config any way we want, as long as it has withCredentials = true
Property XMLHttpRequest.withCredentials Determines whether cross-domain requests should be created using identities such as cookies, authorization headers, or TLS certificates.
This flag is also used to determine whether cookies passed in the response will be ignored. An XMLHttpRequest from another domain cannot set a cookie on its own domain if the withCredentials flag is not set to true before creating this request.
In other words, if we do not specify this attribute, our cookie will not be saved by the browser, i.e. we cannot send the cookie back to the server, and the server will not find the desired cookie with JWT and will not sign the Bearer Token in our HTTP-pipeline.

What is all this for?

Above I described an XSS-resistant way to exchange tokens. Let’s walk through and see the result of the implemented functionality.
If we go into Developer Tools, we see the following flags httpOnly and secure :
A secureway to exchange JWT in ASP.NET Core + SPA
Let’s do a crush-test, try to pull cookies out of the client :
A secureway to exchange JWT in ASP.NET Core + SPA
We observe ‘ ‘, i.e. cookies are not available from the document space, which makes it impossible to read them with scripts.
We can try to get these cookies using additional tools or extensions, but all the tools I tried were calling the native implementation from the document space.

Demo project

Launch instructions are in README.MD

UPD: Protection against CSRF

Configuring ASP.NET Core Server

Middleware Services
XsrfProtectionMiddleware.cs

public class XsrfProtectionMiddleware{private readonly IAntiforgery _antiforgery;private readonly RequestDelegate _next;public XsrfProtectionMiddleware(RequestDelegate next, IAntiforgery antiforgery){_next = next;_antiforgery = antiforgery;}public async Task InvokeAsync(HttpContext context){context.Response.Cookies.Append(".AspNetCore.Xsrf", _antiforgery.GetAndStoreTokens(context).RequestToken, new CookieOptions {HttpOnly = false, Secure = true, MaxAge = TimeSpan.FromMinutes(60)});await _next(context);}}

MiddlewareExtensions.cs

public static class MiddlewareExtensions{public static IApplicationBuilder UseXsrfProtection(this IApplicationBuilder builder, IAntiforgery antiforgery)=> builder.UseMiddleware<XsrfProtectionMiddleware> (antiforgery);}

ConfigureServices

services.AddAntiforgery(options => { options.HeaderName = "x-xsrf-token"; });services.AddMvc();

Configure

app.UseAuthentication();app.UseXsrfProtection(antiforgery);

SPA settings

api.axios.js

import axios from "axios";import cookie from 'react-cookies';const api = axios.create({ baseURL: process.env.REACT_APP_API_URL });api.interceptors.request.use(request => requestInterceptor(request))const requestInterceptor = (request) => {request.headers['x-xsrf-token'] = cookie.load('.AspNetCore.Xsrf')return request;}export default api;

Using

To protect our API methods, we need to add the attribute [AutoValidateAntiforgeryToken] – for the controller or [ValidateAntiForgeryToken] – For method.

You may also like