In the early days of electronic computing, there was no concern for privacy or security because computers were not accessible to just anyone, and even if someone had access to a computer, the chances of them knowing how to use it were negligible. Moreover, computers were not typically used to store data beyond a few dozen numbers; they were primarily used as calculators.
Two decades later, even with personal computing, systems built in Clipper, Delphi, or Visual Basic ran locally or accessed databases hosted on local networks using a ring topology with coaxial cables and BNC connectors, or even on the workstation itself. The term "user session" was practically confined to academic environments and had little relevance in the real world.
The real concern for data privacy and security emerged only with the popularization of the Internet, when the first applications using network communication began to appear, and the term "user session" started to gain traction among developers.
Unlike applications that ran locally on the user's workstation, web applications are executed on application servers, with the frontend running in the user's browser. To maintain the user experience, the application server saves some user session information in memory or a database and sends a session ID to the user's browser, typically via cookies. This creates two problems. The first is that if a malicious person intercepts this ID, they can hijack the user's session, a process known as session hijacking. The second issue is that servers now need to allocate memory space or maintain database records to store some user information, without knowing whether the user is still actively using the application. With a small number of users, this may not be an issue, but with thousands or millions of users, things get significantly more complicated.
To address the first problem, several mechanisms were created, such as CSRF tokens, session binding to the user's IP address, and others. For the second issue, stateless applications emerged. Client-side applications became more complex, leveraging JavaScript, and servers began storing minimal user session data. However, with the introduction of the iPhone, which popularized smartphones and native apps running on devices, it became necessary to find a way to maintain user sessions for these devices as well.
It wouldn’t be efficient to have different methods for managing sessions across different devices, and since server-side applications had become stateless, why not apply the same principle to user sessions? This is where the JSON Web Token, more commonly known as JWT, was introduced.
The Token
JWT, defined in RFC 7519, was created to meet the needs of web and mobile applications in a stateless, scalable way, supporting cross-domain environments like APIs and single sign-on systems. So, let’s break down how it works.
A JWT is composed of three Base64-encoded JSON strings, divided into three parts separated by dots: Header.Payload.Signature:
- Header: Contains information about the token type (JWT, in this case), the signing algorithm, and other metadata.
- Payload: The actual token data, such as user information, permissions, token expiration date and time, and any custom data the developer wishes to include.
- Signature: The most critical part for token security, as it ensures the token was issued by a trusted authentication server.
Example of a Base64-encoded token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
If you decode this token, it will reveal the following—don’t believe me? Copy it and paste it into this site:
Header:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
Each JSON object contains several keys called claims. In the payload above, for example, sub refers to the user the token was issued to. The iat claim stands for "issued at," which stores the token issuance date in Unix timestamp format as an integer. In addition to these, there are standard claims like exp and nbf, which refer to the token’s expiration date and time and the date and time from which the token becomes valid, respectively.
Beyond standard claims, you can define custom claims, but use this feature cautiously to avoid exposing sensitive data, as the token’s content is not encrypted!
Even though the token’s content is not encrypted, the signature ensures the token’s validity for both the client and the server. To bring a JWT into the real world, imagine a wristband used at concerts or events. After presenting your ticket, the security guard gives you a wristband based on your ticket type. Depending on the wristband’s color or design, you might access the VIP area, backstage, or only the general seating. The security guards authorizing your entry don’t need to check with anyone else about your access rights, as they can validate the wristband’s authenticity themselves.
How Are JWT Tokens Issued?
In simple terms, issuing a JWT token is not much different from issuing a session ID, but some details completely change the dynamics of how these tokens are used. Let’s dive in.
- The client application sends a login request to the server.
- If successful, the server responds with an Auth Token and a Refresh Token.
Refresh token? Yes. Applications are dynamic. A user might gain or lose access permissions, change their name, or undergo other changes. For this reason, Authentication Tokens should have a short lifespan—short enough to avoid issues if a user’s permissions change after the token is issued, but long enough to prevent the token from expiring with every request.
But what happens if the token expires? That’s where the Refresh Token comes in.
How Is the Refresh Token Used?
Unlike the Authentication Token, the Refresh Token carries minimal information beyond what’s necessary for the server to issue a new Authentication Token. Additionally, the Refresh Token has a longer lifespan.
The token refresh process can be proactive or reactive. I like to combine both approaches.
Reactive Approach:
- The client application sends a request to the server with the Auth Token in the request header.
- The server verifies that the Auth Token is expired and responds with a 401 error.
- The client application then sends a Token Refresh request to the server with the Refresh Token in the header.
- The server validates the Refresh Token and responds with a new Auth Token and, optionally, a new Refresh Token, depending on how you choose to manage the user session.
- After receiving the new token, the client resends the initial request with the valid new token.
Proactive Approach:
- The client application checks that the Auth Token is expired and sends a Token Refresh request to the server with the Refresh Token in the header.
- The server validates the Refresh Token and responds with a new Auth Token and, optionally, a new Refresh Token, depending on how you manage the user session.
- After receiving the new token, the client sends the request with the valid new token.
As mentioned earlier, to avoid issues due to clock synchronization problems between the client and server, I like to send a refresh request when the Auth Token is close to expiring. However, I also maintain the reactive approach as a fallback in case the proactive method fails for any reason.
How Is the Auth Token Used?
There’s no mystery here: for every server request requiring authorization, the client application sends the Auth Token in the request header. If the request fails, the token refresh process is initiated.
Security Recommendations
As we discussed earlier, JWT token contents are not encrypted. Additionally, these tokens are susceptible to session hijacking. So, what can you do to reduce security risks with JWT? Below are 10 recommendations to improve the security of JWT-based applications:
- Use strong signing keys (e.g., HS256, RSA) and secure storage.
- Validate tokens (signature, claims like exp, aud).
- Set short expiration times and use refresh tokens.
- Transmit over HTTPS only.
- Store tokens securely (e.g., HttpOnly cookies, not localStorage).
- Implement revocation (e.g., blacklists).
- Limit payload data, avoiding sensitive information.
- Protect against XSS/CSRF, replay, and algorithm downgrade attacks.
- Use trusted libraries and follow RFC 7519.
- Monitor and log token usage.
Why Use JWT?
With so many security recommendations, why should you use JWT?
The biggest advantage of JWT over traditional sessions is that it is stateless. Since it doesn’t rely on being stored on the application server, it saves server memory and is better suited for distributed applications across multiple domains, such as APIs, mobile apps, and more.
For example, consider an application using microservices. You could have a dedicated microservice for user authentication. Other services would only need to know the authentication server’s public key to validate the user’s JWT token, without needing to query the authentication server or a database.
Additionally, JWT avoids issues like CSRF, which is a concern in applications with session ID-based systems.
Some frameworks, like Spring, have native support for JWT. There are also many libraries for token creation and validation. If you prefer, you can delegate authentication and authorization responsibilities to tools and services like Keycloak (open source), Auth0, Clerk, and others.
Libraries for Working with JWT
One of the most widely used libraries for JWT is jose, which has implementations for various languages like Java, JavaScript, TypeScript, Python, and C#.
Some languages have specific libraries, such as firebase/php-jwt for PHP, ruby-jwt for Ruby, golang-jwt/jwt for Go, cpp-jwt for C++, and SwiftJWT for Swift.
The JWT.io website provides a comprehensive and detailed list of JWT libraries. Additionally, the site’s homepage offers a tool for analyzing JWT tokens, but be cautious—do not use this tool with production tokens.
Final Thoughts
The goal of this post was not to show code but to explain the fundamental concepts of JWT. If you have a traditional website or web application with the frontend and backend in the same layer, you can continue using sessions. However, if your requirements include mobile apps, a separated frontend and backend, or distributed scenarios, JWT might be the best solution for managing your users’ sessions.
If this post helped you or if you have any questions, leave them in the comments below. Until next time!
Originally posted in my blog
Top comments (0)