DEV Community

Cover image for Authentication for a Gatsby React GraphQL App
Ryan Bethel
Ryan Bethel

Posted on • Originally published at ryanbethel.org

Authentication for a Gatsby React GraphQL App

Introduction

Authentication is a critical function for any web app. There are many general resources on the topic. This is not an "everything you need to know about" post. I will show one authentication solution for an SPA (single page app) using Gatsby and React with a graphQL API backend. It solves most of the problems specific to SPA stateless authentication.

TL;DR

JSON Web Tokens allow stateless authentication for SPAs. They can create security and usability problems if not used properly. Splitting the JWT (JSON Web Token) into two cookies protects against the whole token being stolen. A client-side permissions object is placed in local storage for UI purposes only. The authentication token itself does not use local storage for security purposes. An additional permissions token is used for blacklisting tokens, CSRF protection, and expiring tokens.

Authentication with JWT (the ideal case)

For this SPA the entire app is downloaded from static hosting and the backend API is used to authenticate a user and send the needed data. This allows the API server to handle requests as stateless. Session cookies could be used for authenticating, similar to a server-rendered app, but that would defeat some advantages of an SPA. The most popular solution is JSON Web Tokens or JWT. A JWT is a three-part signed token. It includes a header with some configuration information, a payload with the payload you define, and a signature. Each part is base64 encoded and separated by a period. Typically the payload is sent un-encrypted (although Base64 encoded) meaning it can be read by anyone who can access it. The signature is a cryptographically secure hash of the payload using a key that only the server has. This means that the client can read the payload (which is useful) and anyone who can read it can change the payload (which is not useful). But even if the payload is changed the signature will no longer match the modified payload.

In the simplest implementation, the user would log in to the API and receive a JWT token where the payload is the user ID. The token would be stored on the client in local storage or in a cookie. The client keeps that token and the server does not need to store it. Anytime the user sends the token the API uses its private key to check the signature and then it knows that the user listed in the payload is authenticated. Authentication Done! But in practice, this simple implementation creates many problems that need to be solved. For a more complete explanation of JWT's I recommend jwt.io (or here).

The Problems

A successful implementation should consider the following:

  1. Stolen Token
    If a valid JWT token is stolen it can be used to authenticate as that user. It could be stolen through cross-site scripting (XSS), SQL injection, or some rogue javascript 3rd party asset. If these JWT tokens never expire and there is no way to invalidate them it is like making a copy of your house key for every guest you ever have and never even asking them to throw it away.

  2. CSRF (Cross-Site Request Forgery)
    If you use cookies to authenticate any time your browser sends a request to your site those cookies are sent (with a few exceptions). CSRF is the risk that after a user has authenticated to your app a malicious site could trick the user into clicking a link and submitting a request to the server that they did not intend to send. Because the request is going from an authenticated user client with authenticated cookies and going to the server backend it will be accepted. For a comprehensive explanation of CSRF and recommended best practice refer to OWASP CSRF prevention cheat sheet

  3. Client-side permissions
    Because this is an SPA everything in the UI is either generated by data requested from the API server or generated programmatically on the client. Some form of permissions need to be passed to the client in authentication to render the correct UI. Because anything on the browser could be manipulated, the permissions on the client cannot be trusted to determine what data it can access from the server.

  4. Cookie size
    Some solutions to the permissions problem require larger cookies. As the cookies become larger at a certain point they can impact performance. For example, a more granular permissions model that authorizes individual resources can grow into kilobytes. There is a limit of 4kb per cookie. Cookies are sent with every request to the server, and because of the asymmetric speed of upload vs download they can have an even larger effect on the performance than you might expect (i.e. downloading 4kb of data is much faster than uploading a 4kb cookie in every request). Large cookie performance impact describes this in more detail.

  5. Local Storage
    Some people use local storage to store the JWT and included it in the header of each request sent to the server. Recommended best practice is not to store authentication tokens in local storage because of the risk they can be stolen.

  6. Expiration
    There needs to be some way to blacklist or expire tokens once they are created. Without taking other steps to enable expiring tokens the only option available would be to change the servers key used to sign all the tokens. This is really the nuclear option because it immediately invalidates every JWT token and forces all users to login again.

A Real Solution

The architecture below addresses the problems above with a more complete solution.
Login Sequence

Here are some of the important features:

  1. Split Cookie
    If we use an httponly cookie to store the JWT it prevents the cookie from being read (or stolen) by any javascript on the page, but this also makes it impossible for our app to read the cookie to use anything from the payload. To solve this the JWT is split in half, separating the signature and the payload. The payload is then sent as a standard secure cookie that can be read by the client to see if the user is logged in. The signature is sent in a httponly cookie. Both cookies are sent with any request. The server stitches them back together and verifies the token is valid. This ensures the client has access to the payload (which it needs), but it cannot read the signature preventing it from being stolen and used by an attacker to authenticate themselves. You can read more about this "split cookie" approach here and here

  2. Permissions object in local storage
    If app permissions object are simple (i.e. {role: "ADMIN"}) the cookie would be small, but with more granular permissions where individual resources and actions are individually authorized (i.e. {policies:[{USER-INVENTORY-CREATE}, {TEAM-INVENTORY-READ}, ...]}) the cookie data for client-side permissions can get large. This results in a large payload for the JWT if the permissions are put in the payload. The solution is to return the permissions object in the login response rather than in the JWT token. The client then stores this permissions object in local storage. This allows the client to use the permissions to shape the UI while not exposing the authentication token to being stolen. It also means the data in the permissions object is only sent at login and does not get sent with every request.

  3. Permissions Token
    In order to invalidate previous JWT's we set a permissions token at login. It is used to reference the state of the user when the JWT was created. This is just a cryptographically secure random number that is stored with the user in the database and returned to the client in the payload cookie. Every time the user sends a request to the server that permissions token is compared with the one in the server and if they don't match the request is denied. This token serves three functions:

    • Permissions Change If a users permissions are changed on the server the token changes and the very next request by the user will force them to login again to refresh their permissions. This way if an administrator removes access to some data the changes will take effect immediately. Otherwise, they might stay authenticated for hours or days with stale permissions that no longer match their account.
    • Blacklist old tokens To force a user to authenticate again for any reason. If there is any indication the token was stolen, or the user changes their password you would want to invalidate any previous tokens. Simply changing the permissions token on the backend ensures that any old tokens are no longer valid.
    • CSRF token One defense against CSRF is to use a unique random number generated at login. When a request is sent to the server (or just certain critical requests) that random number must be included with the request and compared to what is stored in the server. This works because even if a user is tricked into clicking a link to submit a request with their cookies the attacker will not know the token number (unless they have compromised it already in some other way). This is often called a CSRF token. Because this token is stored in the database it is not truly stateless.

The sequence below shows a request after logged in. This also shows timeout if the user is idle for more than 30 minutes. The two split tokens are set with different expirations. The payload token will expire in 30 minutes if not renewed. Any server request that succeeds refreshes the payload token with a new 30 minute window meaning that if the user performs some activity every 30 minutes they will not be asked to login again.

Request Authentication Sequence

Discussion (2)

Collapse
samsch_org profile image
Samuel Scheiderich

You're results are effectively that you've manage to create a secure-as-sessions system, but with a bunch of extra steps, and a couple missing features.

Because you are querying your database to validate every request by the "permissions token", you don't actually even need to check the signature is valid (except for the other data, which I'll get to a moment). With a long random string, you have a secure credential by itself. You can instead just use that long random string as the only identifier (in the httpOnly cookie), and when you query your user, also grab the permission from the database. Now your permission are always fresh. For the client, since you don't need to send the permission (or any other "validated" data) back, you can just send the plain data down, no need to encode it or use a JWT at all.

And... that's how regular cookie-based sessions work.

Collapse
ryanbethel profile image
Ryan Bethel Author

Thank you for your feedback. I appreciate you taking the time to read it and give helpful feedback. I take it as a slight compliment that you said I "managed to create a secure-as sessions system"😃. I suppose I could have done much worse. I agree that the end result doesn't have much advantage over cookie-based sessions and is arguably more complicated. This is the first authentication system I have ever built from scratch. I did it with the purpose of learning authentication a bit better. It started out with the goal of being stateless as I was originally only going to use the split cookie approach. As I realized the need for some additional features for CSRF and expiring tokens if permission changed etc. it changed incrementally to what is shown in the post. At that point it would probably have been a better idea to switch over to a sessions based system.

When I first tweeted with a link to this post I was mainly trying to make the point that if you are talking about using a JWT and storing the token in local storage it would be better and more secure to use a split token with the signature in an httpOnly cookie so that the whole token could not be accessed by javascript in the client.

Again, thank you for your feedback.