Session Fixation in Adonisjs with Jwt Tokens
Session Fixation in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Session fixation in AdonisJS when using JWT tokens occurs when an application accepts an attacker-supplied token or session identifier for authentication without validating its origin or ensuring it is issued after authentication. With traditional cookie-based sessions, fixation involves forcing a user to use a known session ID; with JWTs, the risk shifts to accepting a pre-shared or predictable token as proof of identity.
AdonisJS does not manage server-side sessions when JWTs are used; instead, the client presents the token on each request via an Authorization header. If your application accepts a JWT issued before login (for example, a token generated with a static subject or without tying it to an authenticated user ID), an attacker can fixate that token and reuse it after the legitimate user authenticates. This can happen when tokens are generated with weak or static subject (sub) claims, or when the application uses the same token across authentication boundaries (e.g., reusing a token issued during registration after the user logs in).
Another vector involves insufficient validation of token issuers and audiences. If AdonisJS verifies the signature but does not enforce strict checks on the iss (issuer) and aud (audience) claims, an attacker can supply a token issued by a different service or intended for another resource. Additionally, if the secret or private key used to sign tokens is exposed, or if tokens have long lifetimes without rotation, a fixed token remains valid for extended periods, increasing the window for exploitation.
The framework’s JWT utilities, such as use('jwt') helpers, provide methods to generate and verify tokens, but they do not automatically mitigate fixation. Developers must ensure that tokens are issued only after successful authentication, include dynamic per-user claims (e.g., a user ID and a random session nonce), and are rejected if presented before authentication. Without these safeguards, an attacker can trick a victim into authenticating with a token the attacker already knows, effectively hijacking the session.
Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes
Remediation centers on issuing fresh, authenticated-bound JWTs after login and enforcing strict validation on each request. Below are concrete, syntactically correct examples for AdonisJS using the official JWT provider.
1. Issue a new token only after successful authentication
Do not reuse tokens generated before login. After validating credentials, generate a new token that binds to the authenticated user.
import { BaseController } from '@ioc:Adonis/Core/Controller'
import { DateTime } from 'luxon'
import { JWTProvider } from '@ioc:Adonis/Addons/Auth'
export default class SessionsController extends BaseController {
public async store({ request, auth, response }) {
const { email, password } = request.only(['email', 'password'])
const user = await User.findBy('email', email)
if (!user || !(await user.verifyPassword(password))) {
return response.badRequest({ error: 'Invalid credentials' })
}
// Issue a fresh token tied to the authenticated user
const token = await auth.use('jwt').generate(user)
return response.ok({ token })
}
}
2. Validate issuer, audience, and user binding on each request
Ensure tokens include and validate dynamic claims such as user ID and a nonce or session identifier.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
import { Exception } from '@poppinss/utils'
export async function validateJwt(ctx: HttpContextContract) {
const jwt = ctx.auth.use('jwt')
const payload = await jwt.verify((token) => {
// Custom verification rules can be applied here if needed
return token
})
// Enforce strict claims
const jwtSchema = schema.create({
sub: schema.number(),
iat: schema.number(),
exp: schema.number(),
iss: schema.literal('your-app-issuer'),
aud: schema.literal('your-app-audience'),
})
// This will throw if claims do not match
const validated = jwtSchema.validate(payload)
// Ensure the token is bound to the current user in your application state
const user = await User.findOrFail(validated.sub)
if (!user.sessionNonce) {
throw new Exception('Invalid session', 401)
}
// Optional: bind token to a nonce or device fingerprint
// if (payload.nonce !== user.sessionNonce) { ... }
return user
}
3. Configure token lifetime and rotation
Short-lived tokens reduce the impact of a fixed token. Use refresh tokens or re-authentication for sensitive operations.
// In config/auth.ts
export const jwt = {
secret: process.env.JWT_SECRET,
token: {
age: '15m', // short lifetime
type: 'jwt',
},
refreshToken: {
age: '7d', // longer but revocable
},
}
4. Enforce issuer and audience checks globally
AdonisJS allows you to define these within the JWT provider configuration to prevent acceptance of tokens from other sources.
// In config/auth.ts
export const jwt = {
secret: process.env.JWT_SECRET,
token: {
age: '15m',
type: 'jwt',
},
provider: 'jwt',
issuer: 'your-app-issuer',
audience: 'your-app-audience',
}