Use encrypted cookie data to reduce database lookups

Use encrypted cookie data to reduce database lookups
Photo by Chris Ried / Unsplash

If you're building your own application, at one point you're going to get into doing some sort of authorization. What developers have done before is store a randomized session token in your browser to keep track of a logged in session and associate login information in the backend using some caching mechanism to associate user data to a session.

This unfortunately leads to interesting problems when the number of users grow and the backend expands to multiple servers to keep of this data and keeping those backends (redis, memcache or even just a sql database) up to date.

Other challenges include federated access between systems. Suppose that you have an authentication server and several other services that you'd like to run together. Would you create one monolithic system or find a way to allow development to occur in a more decentralized manner using smaller services that can run in a modularlized way? If you do end up fanning out your systems across modularized services, then not needing to check back on an authorization system everytime some request is made will become very handy as you'll get rid of one layer of lookup (this does change in the case where a revocation list is needed however!).

Javascript Web Tokens (JWTs)

One solution would be to use JWTs as a method to encode user information into a user's cookie. These are paritcularly great as it makes 2 guarantees:

  1. Json information is available to the browser
  2. The Json token has not been tampered with based on a cryptographic signature

The cavieat, however, for this design is that it is important to not put sensitive user information into the JWT as the json content of the token is publically readable. If you are concerned about data leakage from the JWT, in particular, related to data that might related to the inner workings of a system -- it is not recommended to put that data there.

For a sample implementation of JWTs, take a look at python implementations of flask-jwt or sanic-jwt which are great ways of understanding how they are used. Signing of JWTs are done using a secret (a random string) that it combined with the Json data to create a signature. Any system with this shared secret will be able to verify the data.

Just as a note, there are 2 different algorithms that can be used for signing a JWT. One being a symmetric algorithm like HS256 which is means that the same secret is used to create the signature and verify it, and the second being RS256 which is an asymmetric algorithm where a public and private keys are used to verify the signature and write the signature respectively. RS256 requires a little more work to manage as you have a public and private component to signing data which can be used to validate data over a distributed system without needing to share a private signing key across the system which could lead to a security risk. If you're interested in learning a little about asymmetric encryption this youtube video provides a great introduction, in addition to this one that provides introduction to how digital signatuures work.

Just a reminder however, that this still does not provide privacy of cookie data – it does just provide a guarantee that the cookie data has not been tampered with when it is sent back your server.

Encrypted Cookies

If you are sending data inclosed in a cookie that is not used by the user client, in this case it would be safe to encrypt the data that is part of the user's cookie data. For example when a user logs into an application, they might require basic infomration about themselves such as their e-mail address, username and other information about themselves. Behind the scenes if the user_id of the user is stored in a database and that is being used across multiple database tables, it might be advantageous to not expose that data to the user as it might give hackers an additional understanding about the user data and how you backend works.

This security risk can also be avoided by encrypting backend information about the user_id or user access levels in an ecrypted format. If the system that you are running on is a monolithic system (mainly running on a single server) then using symmetric encryption would be a simple but viable solution to this problem as there would be no key distribution work needed.

A very simple implementation using the Fernet encryption algorithm in Python could be:

from base64 import b64encode, b64decode
import binascii
from cryptography.fernet import Fernet

class Encrypt:
    """Used to encrypt and decrypt strings"""

    key = Fernet.generate_key()

    def __init__(self):
        self.fernet = Fernet(self.key)

    def encrypt(self, data):
        return b64encode(self.fernet.encrypt(data.encode())).decode()

    def decrypt(self, data):
        """Returns none if bad decryption"""
        try:
            return self.fernet.decrypt(b64decode(data.encode())).decode("utf8")
        except binascii.Error:
            return None

The code above handles encryption and decryption of a simple string (a new secret key is generated everytime the program is restarted and can be easily extended to handle Json strings – all one would need to do in the end is just attach this encrypted data to a cookie and the server would be able to decode this data once it is sent back. Something similar can be done as well too using asymmetric keys but that will be left as an exercise to the reader.