Advanced JWT Topics
Advanced concepts and techniques for expert-level JWT implementations.
1. JWE (JSON Web Encryption)
JWE allows you to encrypt JWT payloads, not just sign them. Use when you need to protect sensitive data.
// JWE encrypts the payload
// Structure: header.encrypted_key.iv.ciphertext.tag
const jose = require('jose');
// Encrypt JWT
const secret = new TextEncoder().encode('your-256-bit-secret');
const jwt = await new jose.EncryptJWT({ sub: 'user123' })
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
.setIssuedAt()
.setExpirationTime('2h')
.encrypt(secret);
// Decrypt JWT
const { payload } = await jose.jwtDecrypt(jwt, secret);
// Use cases:
// - Sensitive data in payload
// - Compliance requirements (HIPAA, PCI-DSS)
// - Multi-party scenarios2. Nested JWTs
A JWT that contains another JWT as a claim. Useful for delegation and multi-party scenarios.
// Create inner JWT
const innerToken = jwt.sign(
{ sub: 'user123', permissions: ['read'] },
innerSecret,
{ algorithm: 'HS256' }
);
// Create outer JWT containing inner JWT
const outerToken = jwt.sign(
{
sub: 'service-account',
delegatedToken: innerToken, // Nested JWT
issuer: 'auth-service'
},
outerSecret,
{ algorithm: 'RS256' }
);
// Verify outer token
const outerDecoded = jwt.verify(outerToken, outerPublicKey);
// Extract and verify inner token
const innerDecoded = jwt.verify(
outerDecoded.delegatedToken,
innerSecret
);
// Use cases:
// - Token delegation
// - Multi-party authentication
// - Service-to-service with user context3. Token Chaining
Chain multiple tokens together for complex authorization scenarios.
// User token
const userToken = jwt.sign(
{ sub: 'user123', role: 'user' },
userSecret
);
// Service token (references user token)
const serviceToken = jwt.sign(
{
sub: 'service-id',
userToken: userToken, // Chained token
serviceRole: 'api-service'
},
serviceSecret
);
// Verify chain
function verifyTokenChain(token) {
const decoded = jwt.verify(token, serviceSecret);
// Verify chained user token
const userDecoded = jwt.verify(
decoded.userToken,
userSecret
);
return {
service: decoded,
user: userDecoded
};
}4. JWKS (JSON Web Key Set)
Standard way to publish public keys for token verification.
// JWKS endpoint
app.get('/.well-known/jwks.json', (req, res) => {
res.json({
keys: [
{
kty: 'RSA',
kid: 'key-1',
use: 'sig',
alg: 'RS256',
n: 'public-key-modulus',
e: 'AQAB'
}
]
});
});
// Client fetches JWKS and verifies token
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://auth.example.com/.well-known/jwks.json'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
jwt.verify(token, getKey, {
algorithms: ['RS256']
}, (err, decoded) => {
// Token verified with public key from JWKS
});5. Token Introspection
RFC 7662 defines a standard way to query token metadata from the authorization server.
// Introspection endpoint
app.post('/oauth/introspect', async (req, res) => {
const { token } = req.body;
try {
const decoded = jwt.verify(token, SECRET);
// Check if token is active
const isActive = !isTokenBlacklisted(token) &&
!isTokenExpired(token);
res.json({
active: isActive,
sub: decoded.sub,
exp: decoded.exp,
iat: decoded.iat,
scope: decoded.scope,
client_id: decoded.client_id
});
} catch (error) {
res.json({ active: false });
}
});
// Client uses introspection
const response = await fetch('/oauth/introspect', {
method: 'POST',
body: JSON.stringify({ token }),
headers: { 'Content-Type': 'application/json' }
});
const introspection = await response.json();
if (introspection.active) {
// Token is valid and active
}6. Proof of Possession (PoP) Tokens
Tokens bound to a specific client or key to prevent token theft and replay attacks.
// Generate client key pair
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
namedCurve: 'prime256v1'
});
// Include public key in token
const token = jwt.sign(
{
sub: 'user123',
cnf: {
jwk: publicKey // Client's public key
}
},
serverSecret
);
// Client must prove possession of private key
function provePossession(token, privateKey) {
const decoded = jwt.verify(token, serverSecret);
const clientPublicKey = decoded.cnf.jwk;
// Client signs a nonce with private key
const nonce = crypto.randomBytes(32);
const signature = crypto.sign(null, nonce, privateKey);
// Server verifies signature with public key from token
const isValid = crypto.verify(
null,
nonce,
clientPublicKey,
signature
);
return isValid;
}7. Token Aggregation
Combine multiple tokens into a single aggregated token for simplified authorization.
// Multiple service tokens
const serviceAToken = jwt.sign({ service: 'A', permissions: ['read'] }, secretA);
const serviceBToken = jwt.sign({ service: 'B', permissions: ['write'] }, secretB);
// Aggregate into single token
const aggregatedToken = jwt.sign(
{
sub: 'user123',
tokens: {
serviceA: serviceAToken,
serviceB: serviceBToken
},
aggregatedAt: Math.floor(Date.now() / 1000)
},
aggregationSecret
);
// Verify aggregated token and extract service tokens
function verifyAggregated(token) {
const decoded = jwt.verify(token, aggregationSecret);
return {
user: decoded.sub,
serviceA: jwt.verify(decoded.tokens.serviceA, secretA),
serviceB: jwt.verify(decoded.tokens.serviceB, secretB)
};
}8. Stateless vs Stateful Considerations
When to Use Stateless (JWT)
- Distributed systems and microservices
- High scalability requirements
- No need for immediate revocation
- Stateless API design
When to Use Stateful (Sessions)
- Need immediate token revocation
- Complex authorization logic
- Single-server applications
- Need to track user sessions
💡 Hybrid Approach
Use JWT for access tokens (short-lived) and maintain session state for refresh tokens and revocation.