KamilTroczewski

You probably do it wrong - in other words a token on the frontend 🙅‍♂️

💡

This article is available in Polish too. If you'd like then

If you want to go straight to the point, this article essentially covers two methods:

  1. LocalStorage
  2. Cookie

Introduction

Many applications on the Internet have implemented sign-up and access granting systems. Although that scenario is common, programming this is not that easy. Security of web applications is very important as we cannot allow someone to steal the identity of our users. You have to think carefully about it from both the backend and frontend side. Therefore, this article discusses how we should protect the token 🤭

Confirming identity and granting access

The user entering website such as Facebook, Youtube or Github is obligated to confirm his identity. The backend authenticates him, so it checks if this user is really the person claiming to be. Therefore, when the user makes a request to grab protected resources, the server authorizes - which means that it checks if user really has appropriate rights. That is where the token is needed, which is something similar to an ID card. An ID card can give us some data about a person and the token works similarly, allowing us to check with who we are dealing with. However, how is such a token issued?

The application communicates with the server. The user signs up and signs in, and the server returns him a token

As you can see, the fifth point says - user is signed in and his token .... So what is happening later with the token? We have to have access to it, if we want to perform requests to the server and authenticate that we are really the person who we claim to be. It's worth keeping it in the storage of a browser, so users wont't need to sign in every time they enter the website.

Storing token in the localStorage

After we happily receive the token, we can save it in the localStorage object of a browser. How can we do that?

const signInUser = async (login, password) => {
  const signInURL = 'http://website.com/auth/signin';
  try {
    const response = await fetch(signInURL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ login, password }),
    });

    const { data } = await res.json();

    const { accessToken } = data;

    // Save token in the localStorage
    localStorage.setItem('accessToken', accessToken);
  } catch (err) {
    console.error('Ooops, something went wrong', err);
  }
};

Storing token in the localStorage is one of the possibilities to store token, but you have to remember that it's saved in the user's browser and as you may know everything on the frontend can be checked by anyone, even by a third party app! This method is vulnerable to XSS attacks and somebody could inject a script to our site, which could hijack user token. After all, we don't want it to end up in the wrong hands.

XSS attack, hacker hijacks user token which is saved in the localStorage

As you can see, if the website is poorly secured it's really easy to steal the token. So I don't recommend using localStorage, as this solution is very vulnerable and we have to give it up. To our advantage, we have other possibilities. Let's see where else we could save our token.

The Cookie also allows you to store information on the site but it differs from localStorage in that it can be set either by the backend server or by the frontend application.

If we decide to set a cookie on the frontend, situation will repeat as someone could still steal it. Therefore, we should save the token in cookie through the server. You may ask - What will be changed if I set a cookie on the backend? It will still be saved in the browser. Yes, it's true, but we can set some flags that will block access to cookies.

httpOnly flag

Enabling httpOnly flag prevents the cookie from being read in the application. If we check the content of document.cookie, it won't be there, which gives us better protection. We also make the process of attaching cookie with token on the frontend easier, we don't need to worry about adding the token in the header, because it's visible in every single request.

For example, let's assume that backend and frontend are at the same address. We do request on the frontend using the fetch function, which is builtin to browser, but to have a token attached we have to set one option.

fetch(signInURL, {
  method: 'POST',
  credentials: 'same-origin', // We have to set it!
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ login, password }),
});

When we set the credentials option to same-origin, cookies will be attached to the query, but only within the same origin. So the token in the cookie will be sent every time, but only to our server. However __ [origin] (https://developer.mozilla.org/en-US/docs/Glossary/Origin) __ is what we see in our browser address bar. It consists of a protocol (HTTP or HTTPS), a domain, and a port. If any of these three things are different, credentials with a value of same-origin would block sending a cookie to that address.

Origin consists of a protocol, a domain, and a port

For curiosity, you can also set the credentials option to include, which allows cookies to be sent between different origins, but this isn't safe because other backend servers could read this cookie.

Now, on the backend, we need to set the httpOnly flag to true for our cookie. In this example, the server is running on [NodeJS] (https://nodejs.org/) with the support of [ExpressJS] (https://expressjs.com/) framework.

const MAX_AGE_1_MONTH = 1000 * 3600 * 24 * 30; // in milliseconds
res.cookie('access_token', accessToken, {
  httpOnly: true,
  maxAge: MAX_AGE_1_MONTH,
});

And we already have way more secure authorization in our application. The access_token cookie won't be returned when we check the content of a document.cookie on the frontend.

secure flag

A cookie with the secure flag, as the name might suggest, is only sent to the server when the request is encrypted with the HTTPS protocol. If our website does not use this protocol, the token will not be sent. This protects us from users who may reach our website using the HTTP protocol, which later will send dangerous requests.

res.cookie('access_token', accessToken, {
  httpOnly: true,
  secure: true,
  maxAge: MAX_AGE_1_MONTH,
});

sameSite flag

The sameSite flag restricts domains from which authorized queries could be sent. It protects us against websites that tries to reach our website with the token attached. So no one from an unauthorized site will be able to download resources that they do not have access to, even if someone has a token in the cookie. This type of attack is called [CSRF] (https://wikipedia.org/wiki/Cross-site_request_forgery).

For a sameSite you can also assign value secure but this option is too restrictive and blocks links that are on other sites which leads to our site. Therefore, there is also the Lax option, which is the golden mean, because it blocks cookies from other domains, but allows you to redirect to our website.

res.cookie('access_token', accessToken, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: MAX_AGE_1_MONTH,
});

Our application is already much safer than what we had at the beginning. Now, only the backend server has to check the incoming cookies. For this, the Middleware is very well suited, as it verifies if the token is invalid or if it's not attached. If everything is good it returns the requested resource.

const authMiddleware = (req, res, next) => {
  const { access_token: accessToken } = req.cookies;

  if (!accessToken) {
    return res.status(401).json({ status: 401, message: 'Cookie with a token is not attached' });
  }

  try {
    jwt.verify(accessToken, getEnv('ACCESS_TOKEN_SECRET'));
    next();
  } catch {
    res.status(401).json({ status: 401, message: 'Invalid token' });
  }
};

Summary

As you can see there are a lot of possibilities for how to store the token on the frontend. Some solutions are less secure (yes, I'm looking at you localStorage 👀) and others are more. Therefore, this topic must be well understood in order to not let someone else steal the identity of our users