Balancing Security and Scalability: Understanding Sessions, JWTs, and Refresh Tokens


When developing the authentication and authorization system for an application, I had to choose between using sessions and JWT tokens.
Sessions offered a straightforward approach: when a user logs in, the server creates a session and stores it on the server-side. Initially, storing sessions in memory might become problematic when scaling the application horizontally. To address this, we can use a fast, distributed storage system like Redis, which is optimized for such scenarios. The session ID is then sent back to the user in a cookie. To enhance security, the cookie can be configured with the HttpOnly and SameSite attributes, preventing client-side scripts from accessing it and mitigating cross-site request forgery (CSRF) attacks. Now with each request this session id will be sent from browser to server in cookies. The server retrieves the session ID from the cookie, looks it up in Redis, and verifies its validity. If the session exists, the user is granted access; otherwise, they are denied.
For every request, the server must call Redis or the database to validate the session, which adds latency. To avoid this overhead, some developers use JWTs(JSON Web Tokens), which are self-contained and can be verified without database calls.
What is JWT ?
a compact and self-contained way for securely transmitting information between parties as a JSON object. A JWT (JSON Web Token) is not a method for encrypting sensitive data. while it can be signed to verify integrity, the payload itself is not encrypted and can be read by anyone with access to the token, meaning you should not store highly confidential information within it.
How JWT-Based Authentication Works
When a user logs in, the server generates a JWT token and sends it to the client, typically stored in a secure cookie. This token is then included in the headers of every subsequent request. On the server side, the token is validated using a SECRET key (stored securely on the server). The validation process involves verifying the token’s signature to ensure it hasn’t been tampered with and checking its expiration time. If the token is valid, the server processes the request and returns a successful response to the user. This stateless approach eliminates the need for server-side session storage.
What Happens if an Attacker Steals Your Session Cookie?
Even with security measures like HttpOnly and SameSite attributes in place, there’s always a possibility that an attacker could steal your session cookie. If this happens, the attacker can send requests using the same session ID, potentially bypassing security checks. When the server receives the request, it will validate the session ID against the database (e.g., Redis). If the session ID exists, the attacker will receive a successful response, gaining unauthorized access.
However, modern security systems are designed to detect suspicious activity, such as unusual login locations or patterns. Once such activity is identified, the system can invalidate the session by removing the session ID from the database. The next time a request is made with the same session ID, it will no longer exist in the database, and the attacker will receive an unauthorized response. This ability to quickly invalidate sessions is a significant advantage of server-side session management.
Now, consider the same scenario in a JWT-based flow. If an attacker steals a user’s JWT token and somehow bypasses all security checks, they can send requests with the stolen token. Since the JWT is valid (properly signed and not expired), the server will process the request and return a successful response. Unlike sessions, where the server can invalidate a session ID by removing it from the database, JWTs are stateless and self-contained. This raises a critical question: how do you invalidate a JWT token once it has been compromised?
At this point, you might think of a solution like blacklisting compromised JWT tokens by storing them in a database. This way, when a request comes in, the server can check if the token is both valid and not present in the blacklist. However, this approach reintroduces the need for a database call for every request—bringing us back to the same performance bottleneck we initially tried to avoid by using JWTs. Not only that, but JWTs also come with additional complexities and higher memory consumption. For instance, JWTs typically take 3x more memory than session IDs due to their larger size and the need to store payload data directly in the token.
To address these challenges, we use Access Tokens (short-lived JWTs) and Refresh Tokens (long-lived JWTs). For each request, the server validates the short-lived access token. If it’s valid, the request is allowed. If it’s expired, the server validates the refresh token and issues a new access token.
But what if an attacker steals both tokens? They could use the access token until it expires and then use the refresh token to generate a new one. To mitigate this, we can maintain a blacklist for invalid refresh tokens. When the security system detects suspicious activity, it adds the compromised refresh token to the blacklist. Now, even if the attacker has both tokens, they can only use the access token for a short time. When they try to refresh it, the server checks the blacklist and denies the request if the refresh token is invalid.
This approach strikes a balance: it reduces database calls (since we only check the blacklist when refreshing tokens) and limits the damage of compromised tokens to a short duration.
Many YouTubers and authentication providers have adopted JWTs, but that doesn’t necessarily mean they’re the best choice for every scenario. Sessions, on the other hand, are often easier to implement and more secure by design. While JWTs can be useful, they come with their own set of challenges, which is why there’s a strong guideline that JWTs should always be short-lived. This minimizes the risk of prolonged exposure if a token is compromised.
JWT’s were build for different use cases like -
Stateless Authentication in Microservices:
JWTs are ideal for distributed systems where services need to authenticate requests without sharing a central session store.Third-Party API Authorization:
JWTs are often used to grant limited access to third-party APIs, as they can include specific permissions in the token payload.When a user signs up for the first time, you can send them a verification link via email. This link contains a JWT token that is valid for a specific amount of time (e.g., 24 hours). When the user clicks the link, the request is sent to your authentication server. The server validates the JWT token to ensure it hasn’t expired or been tampered with. If the token is valid, the server creates a session ID for the user and sends it back, allowing them to proceed with authentication.
Resources -
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens
https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
https://medium.com/@getintheshell/finally-understanding-the-benefits-of-a-long-lived-refresh-token-bf021176a9d1
Subscribe to my newsletter
Read articles from Prithviraj Patil directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
