How to implement CSRF protection on a JWT-based app (Node, csurf , Angular)

I have been developing an application in Node.js and Angular that uses JWT for authentication and authorization. The authentication token is stored as an HTTP-only cookie on the client’s browser. This simplifies the front-end application code, as it doesn’t have to keep track of the token (cookies are automatically sent by the browser on each request).

However, using cookies for the authentication makes the application vulnerable to CSRF attacks. There are excellent resources online to learn more about CSRF, like Wikipedia and the OWASP page. Let’s find out how such an attack can happen and how to mitigate the vulnerability.

The problem

A CSRF attack against a website that uses cookies for authentication (say app.com) could be performed in the following way:

  • The user accesses a malicious website, evil.com, with his browser.
  • The malicious page contains a hidden request to the victim site, app.com. It could be, for example, a hidden form that posts some content to the /api/transfer_money endpoint.
  • The browser automatically attaches all the cookies that it has for the app.com domain. The authentication cookie is attached to the request.
  • From the server’s point of view, this is a legitimate request, because it contains the authentication cookie. The POST action is successful.
  • As a result, the attacker can perform actions on behalf of the user. However, the attacker’s code can’t read server responses, due to same-origin policy.

The solution

We need to set an extra value — token — that can be passed to the server to verify the request’s authenticity.

  • When the user navigates for the first time to the website, app.com, the server sets a CSRF cookie.
  • This cookie is only readable by the JavaScript code on the app.com domain.
  • The cookie needs to be sent back to the server in two different ways: as a cookie, and as a header. Adding it as a header is the frontend application’s responsibility, and guarantees that the request is genuine: no external code could have set it.

The csurf library works a bit differently, let’s have a look at it next.

Server-side: the csurf library

Many projects use the csurf library on the server side to add mitigation against CSRF attacks. It is a great library, but I have found that the way it works is often misunderstood by developers.

Let’s see how this library should be used. We are going to use csurf with the “cookie” option set to true, without a session middleware.

First, let’s have a look at an example back-end code.

As you can see, our application includes 4 endpoints:

  • The landing page (/)
  • /login: receives the username and password combination and validates them against the database. As you can see, we have omitted this process and we are simply sending back the authentication cookie, as if the user was successfully logged in.
  • /api/stats: a GET endpoint that returns some data.
  • /api/transfer_money: a POST endpoint that executes an action for the user. In this case, it transfers some money to a different account.

We have also greatly simplified the authentication checks. I decided to keep those details out of this example. Let’s find out how csurf works in detail.

Client — server communication

On a first GET request, the server sends the client two cookies, with the names of _csrf and XSRF-TOKEN:

  • _csrf is generated automatically because we chose to set { cookie: true }. This is a secret, not the CSRF token. The server uses this secret to match the actual token against it. The _csrf cookie is an alternative to using sessions: instead of storing the secret on the server, tied to a user session, we store it on the client’s browser as a cookie.
  • XSRF-TOKEN is the CSRF token. We need to generate it manually with the function req.csrfToken(). It needs to be sent to the client on the first request. This can be done in multiple ways; our example project sets it as a cookie. The client needs to send it back either in the body, query string, or headers of every mutating request (see the docs).

For every following GET request:

  • CSRF protection is not necessary.
  • We shouldn’t use the csrfProtection middleware on the server code, for any GET endpoint other than / .

For every following POST/PATCH/PUT/DELETE request:

  • We must use the csrfProtection middleware on the server code.
  • The server will check the _csrf cookie sent by the client against the value of XSRF-TOKEN, sent also by the client, in the request body, query string, or headers.

Other things to consider

  • The XSRF-TOKEN will probably be sent back by the client as a cookie (because of the way cookies work), but the server will ignore it, as it will look for it only in the request body/query string/headers.
  • The code that generates the token (res.cookie('XSRF-TOKEN', req.csrfToken())) should only be run once, when we first GET the root page /.
  • The _csrf secret is generated by the server (and sent as a ‘set-cookie’) every time the client sends a request fulfilling the following conditions: (1) It doesn’t include the _csrf cookie, and (2) it hits a route that uses the csrfProtection middleware.
  • When using the csrfProtection on a GET route in the server code, the server will ignore any CSRF check. However, the server will generate and set a new secret cookie, _csrf, if the client didn’t send it.

This setup will provide us with CSRF protection if the client-side is configured properly

Client-side: Angular

Fortunately for us, Angular has built-in CSRF protection mechanisms. All we have to do is adding the following lines to our AppModule:

imports: [
HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: 'XSRF-TOKEN',
headerName: 'X-XSRF-TOKEN',
}),
],

The Angular app will then automatically intercept the XSRF-TOKEN cookie and send it back as a header, named X-XSRF-TOKEN , on every mutating request.

Check this story and more at my blog cybertricks.blog!

--

--

--

Passionate full-stack web developer.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Lightning-fast search using Symfony AlgoliaBundle

YOP 2.0 Takes Off

How I Passed the AWS Cloud Practitioner Exam in a Week

Dusting off the account and coming back to code: a story and some technical details

Flutter’s Big (Well, Actually It’s Pretty Small) Mistake

A construction site

7 Configurations I Make To All My Flutter Projects Before Release

A config file

Overall understanding about Software Licensing

React Reddit Reader With Rails API

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
David Silva

David Silva

Passionate full-stack web developer.

More from Medium

The Complete Guide to JSON Web Tokens (JWT) and Token Based Authentication

Easy alternatives for Map and Set in a JavaScript-oriented front-end, back-end configuration

How to implement an inline styles Content Security Policy with Angular and Express Static

HOW TO BUILD A LOGIN AND SIGN UP PAGE WITH API AUTHENTICATION IN ANGULAR