TL;DR - Sessions can be securely stored on the client-side with cryptographic signatures, allowing your servers to remain stateless.

Photo by Nicole De Khors from Burst

Years ago, I was thinking about authentication & authorization for a new API. At the time, the most popular options in the industry were login/password or API keys. We stored those credentials in a database. To authenticate every single request, you had to query the database, hence the IO overhead for every single API request.

To deal with this issue, people have come up with Server-Side Sessions. Authentication happens only once before each session - clients send their credentials to the authentication endpoint. If they are valid, the endpoint issues a random token (Session ID) and temporarily keeps it in memory. All following requests from the client have to include the Session ID, which proves the requests are legitimate.

This solution reduces IO overhead. Also, you can keep information between requests in the session storage. But it brings other issues. For example, suppose your API runs on multiple servers and has load balancing

In that case, you need to sync sessions between the servers. You can store sessions in memcached or any other storage. Cons are that you are adding more moving parts into your system. Designing the architecture so that the session storage doesn’t become a single point of failure is another challenge by itself, which complicates the architecture even further.

Alternatively, you can ensure that your load balancer always routes all requests from the client to the same server, which issued the session ID (Sticky Sessions). The simplest way to achieve it is by hashing the client’s IP address, which will give almost uniform distribution across servers. Downsides are that this will not work if you have large clients, which generate enough load to exhaust a single server’s capacity.

Here comes the cryptography

But can we authenticate the requests without looking up in a storage or memory? It turns out that we can. It’s a straightforward idea - take claims (e.g., user info and the list of permissions), serialize them into a string, sign it with a secret key that only you know. You got a token which you hand to the client. Clients can’t modify the data stored inside the token without knowing the secret.

Now requests can be handled by any server, and no synchronized session storage is needed. Valid tokens authenticate and authorize the requests. Signature validation takes only CPU time, no reads from the database. IO is expensive; CPU time is cheap. Tokens also can be used to keep information between requests

Don’t use JWT 🚫

JSON Web Tokens (JWT) standard was designed for this use case. JWT is a cryptographically signed Base64 encoded JSON, structured in the following way header.payload.signature. You can put user claims in the payload, and libraries will take care of the header and signature.

However, there’s a fundamental flaw in the JWT standard - it’s vague and allows too much freedom for developers. In this article, Tim McLean demonstrates that many popular JWT libraries have critical vulnerabilities that stem from the specification itself, offering developers too many ways to shoot themselves in the leg. You won’t believe how many services out there on the Internet can be tricked to skip token validation.

What to use instead of JWT?

Use PASETO, or implement signed tokens yourself. It’s hard to screw up

  1. Serialize the information you want to include in your token into a string. For example: { "permissions": ["read", "write"] }, I encoded with Base64url and got eyAicGVybWlzc2lvbnMiOiBbInJlYWQiLCAid3JpdGUiXSB9
  2. Sign it with a symmetric algorithm (HMAC with SHA256), and never share your secret key. Here I computed signature for the string and encoded with Base64url R4Yjs_8YOnfui3C8W4bGCMpQZTySLt5zCWxf7jwvQZs
  3. Concatenate the two eyAicGVybWlzc2lvbnMiOiBbInJlYWQiLCAid3JpdGUiXSB9.R4Yjs_8YOnfui3C8W4bGCMpQZTySLt5zCWxf7jwvQZs

Now you got your token. If, for some reason, you need an asymmetric algorithm, use ed25519.