JSON Web Token (JWT)
JWT (JSON Web Token) is a token-based authentication mechanism where the client stores a signed token and sends it with every request. The server can verify the token without needing to store a session, which makes JWT suitable for stateless and scalable APIs.
✅ When is it appropriate
JWT is suitable if most of the following apply:
- the API must authenticate requests without storing session data on the server, so each server in a cluster can verify the token independently
- the application issues its own tokens for its own users and does not need to accept logins from Google, GitHub, or other external identity providers
- the token can carry enough information such as user ID and role so the server does not need to query a database on every request
- the service communicates with other internal services that need to verify the caller's identity without making a separate network call to an auth server
- short-lived tokens of 15 to 60 minutes are acceptable and the application can handle token refresh without disrupting the user
A JWT is a compact string made of three Base64-encoded parts separated by dots: a header describing the algorithm, a payload containing claims such as the user ID and expiry time, and a signature the server uses to verify the token was not tampered with. The server never stores the token. Instead, it re-computes the signature on every request and rejects the token if the signature does not match.
❌ When is it NOT appropriate
JWT may not be ideal if:
- users must be able to log in with an existing account from Google, GitHub, or another external provider, which requires OAuth2 rather than custom tokens
- a revoked token must stop working immediately, because a signed JWT remains valid until its expiry time even after the user logs out or an account is suspended
- a third-party service needs temporary, limited access to the user's data on another platform, which is the exact problem OAuth2 is designed to solve
- permissions change frequently and each change must take effect on the next request, since a JWT can only reflect the roles that were current when the token was issued
The most critical limitation of JWT is that a token cannot be invalidated before it expires. If a user's account is compromised or suspended, the attacker's stolen token remains fully valid until the expiry time passes. The only way to stop it immediately is to maintain a server-side blocklist of revoked tokens, which reintroduces the server state that JWT was chosen to avoid.
👍 Advantages
- the server verifies every request by checking the token's signature without querying a database or session store
- any server instance can verify the token independently, so the application scales horizontally without sharing session state between servers
- the token payload can carry user ID, role, and other claims the service needs, reducing the number of database lookups per request
- a single token issued by one service can be accepted by other internal services that share the same signing key or public key
- tokens are self-contained strings that can be passed in an HTTP header, making them easy to use across different platforms and languages
👎 Disadvantages
- a token cannot be revoked before it expires; if a user is banned, their token still works until the expiry time passes
- roles and permissions are embedded in the token at issue time; a permission change takes effect only after the old token expires and the user receives a new one
- a stolen token grants full access for its remaining lifetime because the server cannot distinguish the real user from someone using a copied token
- the token payload is Base64-encoded but not encrypted, so anyone who intercepts the token can read its contents
- OAuth2 flows such as login with Google require additional libraries and infrastructure; JWT alone does not cover them
🛠️ Typical use cases
- internal APIs where the server issues and verifies its own tokens without delegating to an external identity provider
- microservice communication where one service passes a token to another to identify the calling service or user
- mobile and single-page apps that send the token in an Authorization header on each API request
- horizontally scaled APIs where multiple server instances must verify tokens without sharing a session database
- short-lived access tokens in an OAuth2 flow, where JWT is the format of the token issued by an authorization server
⚠️ Common mistakes (anti-patterns)
- accepting a token signed with the
alg: nonealgorithm, which carries no signature and grants access to anyone who constructs a valid-looking token payload - setting token expiry to 24 hours or longer, which gives an attacker a large window to use a stolen token after the user has logged out
- storing the token in
localStorageinstead of anhttpOnlycookie, making it accessible to any JavaScript running on the page including injected scripts - including sensitive data such as a password hash or payment details in the token payload, which is Base64-encoded and readable by anyone who decodes the token
- writing custom JWT signing and verification logic instead of using a well-audited library such as
jsonwebtokenfor Node.js orPyJWTfor Python
The alg: none vulnerability has caused real-world breaches. Some JWT libraries historically accepted a token with no signature if the header said the algorithm was none. An attacker could forge any token and gain full admin access. Always explicitly specify and validate the expected algorithm in your verification code and reject any token that uses a different one.
💡 How to build on it wisely
Recommended approach:
- Always specify and enforce the expected signing algorithm such as
HS256orRS256in your verification call. Never allow the library to accept the algorithm stated in the incoming token's header. - Store tokens in
httpOnlycookies so that browser JavaScript cannot read them. Send them over HTTPS only and set theSecureandSameSite=Strictcookie attributes. - Set access token expiry to 15 to 60 minutes and issue a separate long-lived refresh token. When the access token expires, use the refresh token to obtain a new one without asking the user to log in again.
- If users must be able to log out immediately or accounts must be suspendable in real time, maintain a server-side revocation list of token IDs, or switch to OAuth2 with opaque tokens managed by an authorization server.
- Use an audited library for all signing and verification. Run OWASP JWT security tests and verify that your server rejects tokens with tampered payloads, wrong algorithms, and expired expiry times.
If users need to log in with an external provider such as Google or GitHub, if a suspended user's token must stop working immediately, or if permissions change frequently and each change must take effect on the next request, these are concrete signals that OAuth2 with an authorization server is the right choice rather than self-issued JWT.
Related topics
☕ If you found this page helpful, consider supporting my work by buying me a coffee.
Feedback & Sharing
Give us your thoughts on this page, or share it with others who may find it useful.
Share with your network:
Feedback
Found this helpful? Let me know what you think or suggest improvements 👉 Contact me.