Session Fixation in Feathersjs with Jwt Tokens
Session Fixation in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Session fixation is a class of vulnerability where an attacker forces a user to authenticate using a session identifier the attacker knows or sets. In FeathersJS applications that use JWT tokens for authentication, the risk profile differs from traditional cookie-based session stores because JWTs are typically self-contained and transmitted via headers (e.g., Authorization: Bearer <token>). However, fixation can still occur in the surrounding application flow when a pre-authentication identifier is accepted and then naively associated with a newly issued JWT without validating that the identifier was freshly generated by the server.
Consider a FeathersJS service that accepts both an initial login credential (e.g., username/password) and an optional pre-shared token (e.g., from a query parameter or header) during authentication. If the server issues a JWT without ensuring that the optional token was freshly generated by the server (or that it was explicitly linked to the authenticated principal), an attacker can set a known token in a user’s browser (via URL, malicious link, or injected script) and later trick the user into authenticating. The server may then bind the attacker’s known token to the victim’s authenticated JWT, allowing the attacker to hijack the session.
Another vector involves misconfigured or permissive JWT parsing in FeathersJS hooks or authentication middleware. For example, if a custom authentication hook reuses an incoming JWT’s jti (JWT ID) or a client-supplied nonce without verifying its provenance, the same token can be replayed across sessions. This is particularly relevant when the token is used for stateful operations (e.g., publishing user-specific events) despite JWTs being inherently stateless. An attacker who can predict or set the jti or a related identifier before login can later replay the token to access protected services.
Additionally, if FeathersJS applications expose an unauthenticated endpoint that returns or sets a JWT-related claim (such as a public key or a negotiated scope) based on user-controlled input without strict validation, this can facilitate token fixation. For instance, an endpoint that accepts a JSON Web Key (JWK) thumbprint from the client and embeds it into subsequent tokens without verifying ownership enables an attacker to force the victim’s JWTs to include attacker-controlled keys.
Real-world attack patterns mirror OWASP API Top 10 2023:1 — Broken Object Level Authorization (BOLA) and Broken User Authentication. A practical CVE example reflecting similar logic is CVE-2020-15167, where JWTs with alg=none were accepted, allowing token manipulation. While not specific to FeathersJS, it illustrates how weak validation around token identity enables fixation.
In summary, the combination of FeathersJS and JWT tokens introduces fixation risks when the application improperly binds user-supplied identifiers to authenticated tokens, fails to validate token provenance, or exposes token metadata via unauthenticated endpoints. Mitigation requires strict separation between authentication-time identifiers and JWT claims, robust validation, and avoidance of client-controlled token metadata.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on ensuring that JWTs are issued with server-controlled, unique identifiers and that client-supplied values are not used to derive token identity. Below are concrete code examples for secure FeathersJS authentication flows.
Example 1: Secure JWT issuance with a server-generated jti
Always generate a fresh, random jti (JWT ID) on the server for each authentication event. Do not accept jti from the client.
const { v4: uuidv4 } = require('uuid');
const jwt = require('jsonwebtoken');
app.use('/authentication', new Authentication({
secret: 'your_jwt_secret',
entity: 'user',
service: 'users',
jwt: {
header: { alg: 'HS256', typ: 'JWT' },
issuer: 'api.yourcompany.com',
audience: 'your-app-client',
jwtid: () => uuidv4(), // server-generated unique identifier
expiresIn: '15m'
}
}));
Example 2: Authentication hook that ignores client-supplied identifiers
In your authentication hook, avoid using any user-provided token or nonce as part of the JWT payload. Validate and transform credentials only.
class CustomAuthentication extends Authentication {
async create(data, params) {
const { email, password } = data;
// Validate credentials via your user service
const user = await this.usersModel.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
throw new Error('Invalid credentials');
}
// Issue a JWT with a server-generated jti; do not forward any client token
const token = this.jwt.sign({
sub: user._id,
jti: uuidv4(), // fresh server-side identifier
role: user.role
}, {
issuer: 'api.yourcompany.com',
audience: 'your-app-client',
expiresIn: '15m'
});
return { accessToken: token };
}
}
Example 3: Validate and restrict token reuse via a server-side store (optional)
If you require token invalidation or replay protection, maintain a server-side allowlist (e.g., Redis) of recently issued jti values and reject tokens with duplicate or unexpected jti. Do not allow clients to set this value.
const jtiBlacklist = new Set(); // in practice, use a distributed cache with TTL
app.hooks.after.push(async context => {
if (context.result && context.result.accessToken) {
const decoded = jwt.decode(context.result.accessToken, { complete: true });
if (decoded && decoded.payload.jti) {
if (jtiBlacklist.has(decoded.payload.jti)) {
throw new Error('Token reuse detected');
}
jtiBlacklist.add(decoded.payload.jti);
// Schedule cleanup of jtiBlacklist based on token expiry
}
}
});
Example 4: Avoid exposing JWT metadata via unauthenticated endpoints
Ensure endpoints that might reflect token-related claims validate the requester’s authorization before returning any JWT introspection data.
app.use('/token-introspect', new Introspect({
onlyAuthenticate: true,
async authenticate(token) {
const decoded = jwt.verify(token, 'your_jwt_secret', { issuer: 'api.yourcompany.com', audience: 'your-app-client' });
// Do not accept any user-supplied token_hint or kid from the client without verification
return { scope: decoded.scope, client_id: decoded.client_id };
}
}));
By coupling these practices—server-generated jti, strict validation, and avoidance of client-controlled claims—you reduce the surface for session fixation in FeathersJS applications using JWT tokens.