The Ultimate Guide to JWT client side auth (Stop using local storage!!!)
Tags: react, tenseijs, jwt, auth
Published: 2020-11-23 04:41:46

---
title: The Ultimate Guide to JWT client side auth (Stop using local storage!!!)
published: true
description: 
tags: reactjs,tenseijs,jwt,auth
cover_image: https://res.cloudinary.com/bahdcoder/image/upload/v1606010752/Bg_aa7556.png
series: [Web security]
canonical_url: https://katifrantz.com/the-ultimate-guide-to-jwt-client-side-authentication-stop-using-local-storage
---

Hello, my name is Kati Frantz, and thank you so much for checking out this tutorial. I want to talk about how to handle JWTs effectively and securely on the client-side. 

The most popular practice in the industry today is to save your JWT in a cookie or local storage. I've done this for a couple of years, and I have even taught others to do the same, but I didn't think it was a big deal until one of the applications I worked on was hacked. 


This was an `XSS` attack. This is an attack in which a malicious person runs malicious code on the client's browser directly attacking your application. 

Now, they could do this to get access to local storage or cookies and extract the JWT from there.

These tokens used in sessions are usually long-lived, and the attackers can get access to your API for a very long time. 

The solution we want to talk about today is one that would, first of all, prevent us from saving our tokens in a risky place, and secondly, implementing another solution that makes sure even if the attacker manages to get hold of a token, the access to the API would expire almost immediately.

Let's get started.

For this tutorial, the first thing we need is a real project. I have set up a [sample project](https://github.com/bahdcoder/jwt-best-practices) with user registration, login, and logout. 

The `/api` folder has a fully-featured graphql and auth server using just 20 lines of [Tensei.js](https://github.com/tenseijs/tensei.git).

```javascript
const { auth } = require('@tensei/auth')
const { tensei } = require('@tensei/core')
const { graphql } = require('@tensei/graphql')

tensei()
    .plugins([
        auth()
            .user('Customer')
            .plugin(),
        graphql()
            .middlewareOptions({
                cors: {
                    credentials: true,
                    origin: ['http://localhost:3000']
                }
            })
            .plugin()
    ])
    .databaseConfig({
        type: 'sqlite',
        dbName: 'tensei.sqlite',
    })
    .start()
    .catch(console.log)
```

<br />

The `/client` folder is a React.js project generated with [create react app](https://create-react-app.dev). We have three routes: `Login`, `Register`, and `Dashboard`. 

### User registration

![registe-page](https://res.cloudinary.com/bahdcoder/image/upload/v1606002630/register-page-jwt-tutorial.png)

When a user registers a new account, we make a request to the backend to get a JWT so we can automatically login the customer. At this point, this is usually when we'll set the JWT to local storage, but we won't be doing that. Here's the implementation of the register function:

```javascript
client
      .request(register, {
        name: name.value,
        email: email.value,
        password: password.value,
      })
      .then(({ register_customer: { customer, token } }) => {
        client.setHeader("authorization", `Bearer ${token}`);

        setCustomer(customer);

        history.push("/");
      })
```

We do not set the `token` to local storage, but we save it in memory. Here, we're setting it on the HTTP client so we can make subsequent authenticated requests to the API.

Next, we set the customer and redirect to the dashboard.

There's something very important that happens when we receive a response from the backend. Let's have a look at the backend response:

![](https://res.cloudinary.com/bahdcoder/image/upload/v1606003553/Screenshot_2020-11-22_at_1.05.20_AM_imp0pr.png)
![](https://res.cloudinary.com/bahdcoder/image/upload/v1606003552/Screenshot_2020-11-22_at_1.05.33_AM_emerbc.png)

The backend set's an `HttpOnly` cookie called `___refresh_token` on the response. This cookie has the unique property of not being accessible from the client-side. This means if you run `document.cookie` in the developer console, you won't see the `___refresh_token` cookie. 

This is because an `HttpOnly` cookie can only be exchanged with the server, and cannot be accessed using client-side javascript.

Using this kind of cookie to set the refresh token gives us additional security, and assurance that the token can't fall into the wrong hands.

### Understanding refresh tokens
The token we received in the JSON response from the API is an access token. This type of token gives the customer access to the API resources. 

An access token should expire in about 10 to 15 minutes so that if it falls into the wrong hands, it becomes invalid as soon as possible.

 A refresh token on the other hand does not give access. Instead, it can be used to request a new access token. That way, before the access token expires, you can silently request a new access token to keep your customers logged in.

### Handling silent refresh
After registration, the customer is redirected to the dashboard, and they can access the dashboard because they are logged in. What happens when she refreshes the page or opens the app in a new tab? 

Well, since we only set the token in memory, the customer loses access and is redirected to the sign-in page instead.

This is not pleasant, and we need to persist the customer's session somehow. 

That's where a silent refresh comes in. Before actually redirecting the customer to the sign-in screen, we need to check if the user has an active session. We do this by calling the API to request a new access token.

A good place to do this is [when the app mounts](https://github.com/bahdcoder/jwt-best-practices/blob/master/client/src/store/index.js), showing a loading indicator to the user while we make this request:

```javascript
  const client = useClient();
  const [customer, setCustomer] = useState(null);
  const [working, setWorking] = useState(true);

  const refreshToken = () => {
    client
      .request(refresh_token)
      .then(({ refresh_token: { customer, token, expires_in } }) => {
        client.setHeader("authorization", `Bearer ${token}`);

        setCustomer(customer);
      })
      .catch(console.log)
      .finally(() => {
        setWorking(false);
      });
  };

  useEffect(() => {
    refreshToken();
  }, [])
```

As soon as the app mounts, we make an HTTP request to the backend to refresh the access token. Since the `___refresh_token` is already set on the customer's browser, it is sent along with the request.

The backend gets the cookie, authenticates this cookie, and sends back a new access token with the customer's information.

We then set the `token` on the HTTP client for subsequent requests and set the customer in the state. This means every time the customer visits the app, their session is fetched from the API and they are automatically logged in.

This solves the first problem, and the customer has a persistent session, but the access token will expire in 10 minutes, and we need to handle this case too. 

The API also responds with how long the JWT takes to expire, so we can use this value to know when to silently call the API to get a new access token.

```javascript
  const client = useClient();
  const [customer, setCustomer] = useState(null);
  const [working, setWorking] = useState(true);

  const refreshToken = () => {
    client
      .request(refresh_token)
      .then(({ refresh_token: { customer, token, expires_in } }) => {
        client.setHeader("authorization", `Bearer ${token}`);

        setTimeout(() => {
          refreshToken()
        }, (expires_in * 1000) - 500)

        setCustomer(customer);
      })
      .catch(console.log)
      .finally(() => {
        setWorking(false);
      });
  };

  useEffect(() => {
    refreshToken();
  }, []);
```

<br />

We're using the `expires_in` value to set a `setTimeout` to refresh the token. This means a few milliseconds before the token expires, the `refreshToken()` method is called again, and it'll set a new access token.

Great, we can now keep the customer always logged in with the access token only stored in memory.

### Handling logout

What happens when the user needs to logout? We do not have access to the `___refresh_token` cookie from client-side javascript, so how do we clear it?

We need to call the API, and the API would invalidate the `___refresh_token`. On the [dashboard page](https://github.com/bahdcoder/jwt-best-practices/blob/master/client/src/pages/Dashboard.js), when the `logout` button is clicked, we'll invoke the following function:

```javascript
  const logout = () => {
    client.request(remove_refresh_token).finally(() => {
      history.push("/auth/signin");

      setCustomer(null);
    });
  };
```

<br />

We call the `remove_refresh_token` endpoint on the backend, and the response invalidates the `___refresh_token` cookie as such:

![](https://res.cloudinary.com/bahdcoder/image/upload/v1606006228/Screenshot_2020-11-22_at_1.49.50_AM_ntxxw7.png)

The backend response contains a `Set-Cookie` header, which sets the `Max-Age` of the `___refresh_token` header to `0` and its value to `''`, thus expiring it and making it invalid.

We then set the customer to `null` and redirect to the sign-in page.

### Cross domain considerations
In the example project, the client and server run on separate domains. This would most likely be the case for your application, and to allow two domains to exchange sensitive information with each other, you need to set some configuration on both client and server.

On the server, first, you need to enable `CORS`, allowing the client domain to request resources from the server. Secondly, you need to allow the exchange of credentials. This informs the server to accept sensitive information such as cookies from the incoming client request. On our demo server, we configured this as such:

```javascript
.middlewareOptions({
    cors: {
        credentials: true,
        origin: ['http://localhost:3000']
    }
})
```

Tensei.js uses `apollo-server-express` behind the scenes for the graphql server, and this configuration is directly passed to it.

On the client, you need to configure your HTTP client such as Axios or Fetch to include sensitive credentials when making requests to an external API. In the demo project we used `graphql-request`, which we configured as such:

```javascript
import { GraphQLClient } from "graphql-request";

export default new GraphQLClient(
  process.env.REACT_APP_API_URL || "http://localhost:4500/graphql",
  {
    credentials: "include",
  }
)
```

### Conclusion
When building applications that are not customer-facing, for tutorials or just fun projects, security might not be a big deal, but if working with real customer data, security has to be a top priority. 

I highly recommend implementing a very secure JWT authentication system when building applications that would be used in the real world. 

Please consider [following me on Twitter](https://twitter.com/bahdcoder) and also checking out [tensei.js](https://github.com/tenseijs/tensei) and giving it a star. 

Thank you very much for reading so far, and I hope this changes the way you handle JWT.