A Developer's Guide to JWT Validation
Learn to validate JWTs correctly. This guide covers signature and claim validation, common pitfalls like alg:none, and provides code for Node.js, Python, and Java.
By Belqassim Djafer · Published · Updated
Why is my JWT invalid?
A service returns 401 Unauthorized. The request definitely includes an Authorization: Bearer <token> header with a long, opaque JSON Web Token (JWT). The token looks correct, but the API rejects it. What's next?
This is a common scenario. The token could be expired, intended for a different service, or have an invalid signature. Guessing is inefficient. A systematic approach to validation is required, both for debugging and for securing services.
Properly validating JWTs is a cornerstone of stateless authentication and authorization systems. Getting it wrong can range from frustrating bugs to critical security vulnerabilities. This guide covers the essential validation steps, provides practical code examples, and details common pitfalls.
The Anatomy of a JWT
A JWT is a compact, URL-safe string defined in RFC 7519 that consists of three parts separated by dots:
HEADER.PAYLOAD.SIGNATURE
- Header: A JSON object specifying the token type (
typ, which is"JWT") and the signing algorithm (alg, e.g.,HS256orRS256). - Payload (Claims): A JSON object containing the claims. Claims are statements about an entity (typically, the user) and additional metadata. This is where you find user info (
sub,name), permissions, and validation parameters like expiration time (exp). - Signature: A cryptographic signature computed over the header and payload. It verifies the token's authenticity (who it's from) and integrity (it hasn't been altered).
The header and payload are simply Base64Url encoded. They are not encrypted. Anyone can decode and read them. The security of a JWT relies entirely on the verification of its signature.
To see this structure for yourself, you can paste a token into a JWT Decoder.
The Two Pillars of Validation
When a service receives a JWT, it must perform two distinct validation steps. A failure at either step means the token must be rejected.
1. Signature Validation
This is the primary security check. It answers the question: Is this token authentic and untampered with?
The process involves re-computing the signature using the Base64Url-encoded header, the payload, the algorithm specified in the header, and a secret (for symmetric algorithms like HS256) or a public key (for asymmetric algorithms like RS256). If the re-computed signature matches the one in the token, the token is authentic.
If signature validation fails, the token is either fraudulent or has been corrupted. The claims in the payload cannot be trusted, and processing must stop immediately.
2. Claims Validation
Once the signature is verified, the claims in the payload can be trusted. The next step is to validate them against the current context. This answers the question: Is this token acceptable for this specific request?
This involves checking standard, registered claims:
exp(Expiration Time): Has the token expired? The value is a Unix timestamp. This is the most common claims check.nbf(Not Before): Is the token being used before it's valid? Also a Unix timestamp.iss(Issuer): Was this token issued by the authentication server we trust? This is a string, e.g.,"https://auth.example.com".aud(Audience): Is this token intended for our service? This is a string or an array of strings, e.g.,"https://api.example.com". This is critical for preventing a token issued for one service from being used to access another.
Most JWT libraries handle exp and nbf checks automatically, but you usually need to explicitly provide the expected iss and aud values.
How to Validate JWTs in Code
Never implement JWT validation logic from scratch. The cryptographic operations are subtle and easy to get wrong. Always use a well-maintained library.
For the following examples, we'll use this sample JWT. It was signed with the HS256 algorithm and the secret your-256-bit-secret.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3OTg3OTAwMDAsImlzcyI6ImF1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJhcGkuZXhhbXBsZS5jb20ifQ.ixpW30YlC_9ucy_S1C03h5a3zSFEaF1o3y5C4n36I8E
Node.js (with jsonwebtoken)
The jsonwebtoken library is the standard in the Node.js ecosystem.
const jwt = require('jsonwebtoken');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3OTg3OTAwMDAsImlzcyI6ImF1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJhcGkuZXhhbXBsZS5jb20ifQ.ixpW30YlC_9ucy_S1C03h5a3zSFEaF1o3y5C4n36I8E';
const secretKey = 'your-256-bit-secret';
try {
// The verify function handles both signature and standard claims validation (exp, nbf).
const decoded = jwt.verify(token, secretKey, {
algorithms: ['HS256'], // Explicitly specify the algorithm
audience: 'api.example.com',
issuer: 'auth.example.com'
});
console.log('JWT is valid!');
console.log('Decoded Payload:', decoded);
/* Output:
JWT is valid!
Decoded Payload: {
sub: '1234567890',
name: 'John Doe',
iat: 1516239022,
exp: 1798790000,
iss: 'auth.example.com',
aud: 'api.example.com'
}
*/
} catch (err) {
// err.name can be 'TokenExpiredError', 'JsonWebTokenError', etc.
console.error('JWT validation failed:', err.message);
}
Python (with PyJWT)
For Python, PyJWT is the go-to library. Note the mandatory algorithms parameter, which is a key security feature.
import jwt
token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3OTg3OTAwMDAsImlzcyI6ImF1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJhcGkuZXhhbXBsZS5jb20ifQ.ixpW30YlC_9ucy_S1C03h5a3zSFEaF1o3y5C4n36I8E'
secret_key = 'your-256-bit-secret'
try:
# decode() validates signature, expiration, and other claims.
decoded_payload = jwt.decode(
token,
secret_key,
algorithms=['HS256'], # Critical for security
audience='api.example.com',
issuer='auth.example.com'
)
print("JWT is valid!")
print(f"Decoded Payload: {decoded_payload}")
# Output:
# JWT is valid!
# Decoded Payload: {'sub': '1234567890', 'name': 'John Doe', 'iat': 1516239022, 'exp': 1798790000, 'iss': 'auth.example.com', 'aud': 'api.example.com'}
except jwt.ExpiredSignatureError:
print("Token has expired.")
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
Java (with auth0/java-jwt)
In the Java ecosystem, Auth0's java-jwt is a popular and robust choice. It uses a builder pattern for verification.
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
public class JwtValidator {
public static void main(String[] args) {
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3OTg3OTAwMDAsImlzcyI6ImF1dGguZXhhbXBsZS5jb20iLCJhdWQiOiJhcGkuZXhhbXBsZS5jb20ifQ.ixpW30YlC_9ucy_S1C03h5a3zSFEaF1o3y5C4n36I8E";
String secretKey = "your-256-bit-secret";
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("auth.example.com")
.withAudience("api.example.com")
.build(); //Reusable verifier instance
DecodedJWT decodedJWT = verifier.verify(token);
System.out.println("JWT is valid!");
System.out.println("Subject: " + decodedJWT.getSubject());
System.out.println("Issuer: " + decodedJWT.getIssuer());
// Output:
// JWT is valid!
// Subject: 1234567890
// Issuer: auth.example.com
} catch (JWTVerificationException exception){
System.err.println("JWT validation failed: " + exception.getMessage());
}
}
}
Real-world Usage: API Middleware
A common use case for JWT validation is in API middleware to protect routes. The middleware intercepts incoming requests, validates the token, and attaches the user's identity to the request object for use by downstream handlers.
Here is an example using Node.js with Express:
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const SECRET_KEY = 'your-256-bit-secret';
// Middleware to validate JWT
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (token == null) {
return res.sendStatus(401); // No token provided
}
jwt.verify(token, SECRET_KEY, { audience: 'api.example.com' }, (err, user) => {
if (err) {
return res.sendStatus(403); // Token is invalid (expired, wrong signature, etc.)
}
req.user = user; // Attach decoded payload to the request object
next();
});
};
// This route is protected by the middleware
app.get('/api/profile', authenticateToken, (req, res) => {
// req.user contains the JWT payload
res.json({
message: `Welcome, user ${req.user.sub}`,
userData: req.user
});
});
// This route is not protected
app.get('/api/health', (req, res) => {
res.send('OK');
});
app.listen(3000, () => console.log('Server started on port 3000'));
Real-world Usage: Service-to-Service Authentication
JWTs are also excellent for securing communication between microservices. When Service A needs to call Service B, it can generate a JWT to authenticate itself. The iss and aud claims are crucial here.
Service A (the caller) generates a JWT. The payload includes:
iss:"service-a"(identifies the issuer)aud:"service-b"(identifies the intended recipient)sub:"service-a"(subject is the service itself)exp: A short expiration time (e.g., 60 seconds).
Service A calls an endpoint on Service B, including the JWT in the
Authorizationheader.Service B validates the token:
- It verifies the signature using a shared secret or public key.
- It verifies the claims, specifically checking that
issis"service-a"andaudis"service-b".
This ensures that only Service A can call Service B, and that a token issued for Service B cannot be replayed against Service C.
Common Mistakes and Security Pitfalls
JWT validation has several well-known traps.
Ignoring Signature Verification
The most dangerous mistake is to decode the payload without verifying the signature. Since the payload is just Base64Url encoded, an attacker can easily craft a token with any claims they want (e.g., "isAdmin": true) and send it to the server. Without signature verification, the server would blindly trust the forged claims.
The alg: none Vulnerability
Historically, some libraries interpreted a header with "alg": "none" as a signal to skip signature validation. An attacker could take a valid JWT, change the algorithm to none, strip the signature, and the library would accept it. Modern libraries now reject alg: none by default or require explicit configuration to allow it. According to the PyJWT documentation, you must now explicitly opt-in to use the 'none' algorithm. Always ensure your library is up-to-date.
Not Specifying the Algorithm
An attacker can use algorithm confusion attacks. A common one is to switch RS256 (asymmetric) to HS256 (symmetric). If the server is expecting an RS256 token, it will use the public key to verify the signature. If an attacker changes the alg to HS256, the server might be tricked into using that same public key as the HMAC secret. Since the public key is public, the attacker can now sign tokens with it, forging any payload they wish.
Solution: Always explicitly specify the allowed algorithm(s) in your verification logic, as shown in the code examples.
Ignoring Critical Claims
Failing to validate aud and iss can lead to security gaps. If a service accepts a token without checking the aud (audience) claim, a token intended for a completely different service could be used to gain access. Always validate that the token was issued by a trusted authority (iss) and is intended for your specific service (aud).
Not Accounting for Clock Skew
It's possible for the clock on the issuing server and the validating server to be slightly out of sync. This can cause a token to appear expired or not-yet-valid when it should be acceptable. Most libraries provide a leeway or clockTolerance option to account for a few seconds of difference. Use it judiciously, as a large leeway can weaken the timeliness guarantees of exp and nbf.
Conclusion
Validating a JWT is more than just decoding it. It's a two-step process: cryptographic signature verification followed by contextual claims validation. Getting this right is non-negotiable for building secure systems.
To summarize the key practices:
- Always use a trusted, up-to-date library.
- Always verify the signature before trusting any claims.
- Always be explicit about the expected algorithm, issuer (
iss), and audience (aud). - Always check for token expiration (
exp).
By following these principles, you can confidently debug token issues and build robust, secure services that rely on JWTs for authentication and authorization.