Prevent Cross-Site Request Forgery Attacks With X-CSRF Token
We’ll take an additional step in this lesson to prevent Cross-Site Request Forgery attacks. We'll see how we can have the client pass a CSRF token with every request and where the server can use the token to verify the identity of the viewer making the request.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
[00:00 - 00:26] At this moment in time, our login via cookie functionality works as intended, and a viewer is able to log in when a cookie is available in the browser. When we set the cookie, we added some options to help ensure the cookie can only be sent through HTTPS and used the same site flag to state that the cookie can only be sent from the site a user is currently viewing, which helps prevent cross-site request forgery attacks.
[00:27 - 00:50] In this lesson, we'll take an additional step to prevent cross-site request for gery attacks and look to see how we can have the client pass a CSRF token with every request and the server will use the token to verify the identity of the request in certain cases. That is to say the request is coming from the viewer from the tiny house site and nowhere else.
[00:51 - 01:15] So the authorized function we'll set up will be used when we start to build functionality in our app that is to be used or accessed only by a user that's logged in or that is to say the viewer. So when a request is to be made for some sensitive data, for example, if somebody is trying to see a viewer's income, we'll want to verify that the request is coming from the viewer that's logged in themselves.
[01:16 - 01:29] This will make a little more sense as we set this up and start to use this in the next coming lessons. So in this lesson, we'll add code to both the client and server projects.
[01:30 - 01:49] In the server, we'll simply introduce a function called authorize that will accept a token given to it and return a user document from the user's collection that it finds based on two things. The viewer ID from the client cookie and the auto-generated token from a viewer when a viewer signs in.
[01:50 - 02:00] If we recall, we auto-generated a token and store it in the user's collection when a user signs in. At any moment when the user signs in again, we regenerate this token.
[02:01 - 02:18] This authorize function we'll create will accept the token from the client and verify that, "Hey, this token is the token of the viewer currently signed into our app." As a result, we'll authorize this viewer to make further requests for sensitive data.
[02:19 - 02:43] Since this authorize function will be used in potentially many different Graph QL resolvers, we'll create this authorize function in a utils index file within the lib folder. In the index file, we'll export an authorize function that is to accept the database object and the request object.
[02:44 - 03:06] We'll import the request interface from express and the database interface from our lib types file and set the database and request parameters with these types. We'll want the authorize function to access the user's collection and return a user that matches the cookie and token of the logged in viewer.
[03:07 - 03:29] We'll import the user interface from the local types file and state the return type of our function is a promise that when resolved, we'll either return an instance of the user or null. We're saying null instead of undefined, since the find one function we'll use for Mongo will either return the intended document's object or a null value.
[03:30 - 03:43] With that said, in our authorize function, we'll use Mongo's find one helper to help find one document from the user's collection. The find one function takes a query that finds one document that satisfies the query.
[03:44 - 04:12] We'll query for the document in which the document ID matches the value of the viewer cookie in our request, with which we can access in the signed cookies field. We'll also want to verify the token value of the document matches that of the request, since token is to be the header passed in our request, we can get this value with the request.get function and state the value of the header key, which we'll call XCSRF token.
[04:13 - 04:23] And finally, our authorize function will return the viewer that finds. And that's all we'll need to do for now.
[04:24 - 04:44] We'll use this authorize function in upcoming resolver functions we'll create to authorize the request to see if the intended viewer is making it, and if so, return the viewer object. We'll now head over to our client project and look to see how we can pass the X CSRF token as part of our requests.
[04:45 - 05:10] We're interested in having the client send the viewer token on every request so the server can receive that token as part of the request header, and authorize that the request is coming from a signed-in user in certain cases . In the Apollo Boost setting, there exists a request configuration option, which is a function that's called on every request, and where we can set headers as part of the context of our operation.
[05:11 - 05:18] So let's see how this can work. In our Apollo client constructor, we'll specify the request configuration option.
[05:19 - 05:34] It'll be an asynchronous function and it will receive the GraphQL operation. We'll use the operation object and run the setContext function available to us and pass in the options it can accept.
[05:35 - 05:47] We'll declare the headers option and specify the XCSRF token header we want to pass. Here is where we have a small issue.
[05:48 - 05:58] The token is part of the viewerState object and is set only after the user logs in. Our Apollo client configuration is unaware of this viewerState object.
[05:59 - 06:14] So what we'll do is have the token be part of our client's session storage and retrieve the token here from session storage. If the token is available from session storage, we'll pass it in as part of our header.
[06:15 - 06:31] If it isn't, we'll simply pass an empty string to ensure a string value is being passed. Here we're using the double pipe operator as the logical OR operator to say what passed the token from session storage or an empty string if it doesn't exist.
[06:32 - 06:47] Session storage is great for the storage mechanism here since data in session storage is not automatically sent to the server unlike our cookie. And we want our token to be part of the request header as another alternative verification step.
[06:48 - 07:07] Now we'll need to ensure the token is set in session storage when a user logs in and is cleared from session storage when a user logs out. First, when a viewer or user successfully logs in with a cookie, we'll set our token in session storage.
[07:08 - 07:24] If the login is unsuccessful, we'll also go ahead and clear the existing token from session storage. This is to be on the safe side and ensure no token exists as part of session storage in the case a user can't log in with a cookie.
[07:25 - 07:49] We'll also need to set the token in our session storage when we log in from the login component. And finally we'll also need to clear out the token from our session storage when we log out.
[07:50 - 08:19] And that's all we'll do for now. For every request made in our app, the X CSRF token will now be passed as a header with which we can actually see in the network logs.
[08:20 - 08:36] Our server will now retrieve this particular token and check to see if the viewer can be found with this token in the request header in our database. And if so, we'll allow the request to eventually query or manipulate sensitive information in our app.
[08:37 - 08:47] This will ensure that the request to be made is going to come from our tiny house client application, which will prevent any cross site request forgery attacks.