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 functionreq.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 ofXSRF-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 thecsrfProtection
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!