Replay Attack in Adonisjs with Jwt Tokens
Replay Attack in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
A replay attack occurs when an adversary intercepts a valid request—typically including a JSON Web Token (JWT)—and retransmits it to reproduce the original effect. In AdonisJS, JWT-based authentication protects routes via middleware that verifies the token’s signature, expiration, and, optionally, its intended audience and issuer. If these checks are incomplete or if tokens lack mechanisms to prevent reuse, an attacker can replay a captured request to perform unauthorized actions, such as placing an order, changing an email, or escalating privileges.
Several factors specific to AdonisJS with JWT tokens can create or expose this vulnerability. First, JWTs are often designed for stateless verification, meaning the server does not inherently track whether a token has already been used. Without additional application-level bookkeeping, a valid token remains valid for every request until it expires, making replay feasible. Second, if tokens are transmitted over insecure channels or stored insecurely on the client, interception risk increases. Third, certain AdonisJS JWT helper configurations—such as omitting the jti (JWT ID) claim or not validating the iat (issued at) and jti against a denylist—reduce resilience. Finally, if endpoints perform sensitive operations based solely on token validity without ensuring idempotency or freshness checks, replay becomes more impactful. Together, these create a scenario where an attacker can reuse intercepted authentication to compromise workflows protected by AdonisJS JWT middleware.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
To mitigate replay attacks in AdonisJS when using JWT tokens, combine standard security practices with token uniqueness and short lifetimes. Always use HTTPS to prevent interception, set short expiration times, and include the jti claim to identify tokens uniquely. Maintain a server-side denylist (or a lightweight cache) of used jti values within the token’s remaining lifetime to reject replays. Below are concrete code examples showing how to implement these controls.
Generating a JWT with jti and iat validation
import { base64urlDecode, createHash } from 'jose';
import { DateTime } from 'luxon';
import jwt from 'jsonwebtoken';
const payload = {
sub: 'user-123',
iat: Math.floor(DateTime.local().toSeconds()),
exp: Math.floor(DateTime.local().plus({ minutes: 15 }).toSeconds()),
jti: createHash('sha256').update(DateTime.local().toISO() + Math.random().toString(36)).digest('hex'),
aud: 'api.mystore.com',
iss: 'adonisjs-app',
};
const token = jwt.sign(payload, process.env.JWT_SECRET, { algorithm: 'HS256' });
console.log(token);
Validating jti against a denylist in AdonisJS middleware
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import { Jwt } from '@ioc:Adonis/Addons/Jwt';
import { DateTime } from 'luxon';
import { dataSource } from '@ioc:Adonis/Lucid/DataSource';
export default class JwtReplayProtection {
protected jwt = new Jwt();
public async handle({ request, response, auth }: HttpContextContract, next: () => Promise) {
const token = request.headers().authorization?.replace('Bearer ', '');
if (!token) {
return response.unauthorized('Missing token');
}
let decoded: any;
try {
decoded = this.jwt.verify(token, process.env.JWT_SECRET);
} catch {
return response.unauthorized('Invalid token');
}
// Ensure token has jti and iat
if (!decoded.jti || !decoded.iat) {
return response.badRequest('Malformed token');
}
// Reject if older than 2 minutes to prevent replay within short windows
const issuedAt = DateTime.fromSeconds(decoded.iat);
if (DateTime.local().diff(issuedAt, 'minutes').minutes! > 2) {
return response.badRequest('Token too old for replay protection');
}
// Check denylist (e.g., Redis or database)
const usedToken = await dataSource
.query()
.from('used_jti')
.where('jti', decoded.jti)
.first();
if (usedToken) {
return response.forbidden('Token already used');
}
// Record jti with expiration time (cleanup handled separately)
await dataSource
.query()
.insert()
.into('used_jti')
.values({ jti: decoded.jti, expires_at: DateTime.local().plus({ minutes: 15 }).toJSDate() });
await next();
}
}
Using AdonisJS provider to centralize denylist checks
import { Redis } from '@ioc:Adonis/Addons/Redis';
export class JwtReplayProvider {
public async denyListContains(jti: string): Promise {
const exists = await Redis.get(`jti:denylist:${jti}`);
return !!exists;
}
public async addToDenylist(jti: string, expiresInSeconds: number) {
await Redis.setex(`jti:denylist:${jti}`, expiresInSeconds, 'used');
}
}
Example route registration with middleware
Route.get('/account/email', 'EmailController.update')
.middleware(['auth:jwt', 'replayProtection'])
.validate({
schema: schema.create({
body: schema.object({ email: schema.string.email() }),
}),
});
Complementary measures
- Use short token lifetimes (e.g., 15 minutes) and refresh tokens with rotation to limit the window for replay.
- Bind tokens to client context (e.g., a hash of the user agent or IP) when appropriate, and validate on replay.
- Leverage the dashboard to monitor scan results and track security scores over time; enable alerts via the Starter plan to be notified of risky patterns.
- For CI/CD integration, use the GitHub Action to fail builds if risk scores degrade; the Pro plan supports continuous monitoring and policy enforcement.