Integrity Failures in Feathersjs with Jwt Tokens
Integrity Failures in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for creating JavaScript APIs with a consistent service interface. When using JWT tokens for authentication, integrity failures occur when the application accepts tokens that are malformed, tampered with, or otherwise invalid, and still proceeds to authorize actions as if the identity and permissions encoded in the token were trustworthy. This specific combination exposes several common weaknesses.
One typical pattern is a service that initializes authentication via feathers-authentication and related hooks, but does not enforce strict verification of the token signature or claims. For example, if the secret or public key used to verify the token is missing, misconfigured, or accidentally omitted in a custom hook, the server may treat unsigned tokens or tokens with a missing signature as valid. An attacker who can craft a token without a valid signature can gain unauthorized access because the server fails the integrity check that the token was issued by a trusted authority.
Another integrity failure scenario involves token replay or lack of token invalidation mechanisms. If a FeathersJS service does not validate the jti (JWT ID) claim or maintain a mechanism to revoke compromised tokens, a stolen token remains usable until expiration. This violates the integrity principle that a token should be usable only by the intended recipient and only for the intended session. Compromised tokens can be reused to impersonate users or escalate privileges across services that share the same token validation logic.
A third dimension is claims manipulation. If the server decodes the token payload without verifying the signature, it may trust values such as sub, role, or custom permissions embedded in the token. An attacker who can modify the base64 payload (if the token is only signed and not encrypted, or if the verification key is weak) can change role claims from user to admin. Because FeathersJS services often use role-based authorization in hooks or custom logic, the application may inadvertently grant elevated permissions based on unchecked token content.
These integrity failures map to the broader OWASP API Security Top 10, particularly API2:2023 Broken Authentication and API1:2023 Broken Object Level Authorization. Insecure token handling can also intersect with BOLA/IDOR issues if the token identifies a resource owner but the server does not re-check ownership on each request. middleBrick detects these classes of issues by analyzing the authentication configuration and runtime requests, highlighting cases where token validation steps are incomplete or inconsistent across services.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
To remediate integrity failures, ensure that every JWT token is cryptographically verified before any authorization decision is made. In FeathersJS, this is typically done through authentication configuration and hooks. Below are concrete, working examples that demonstrate secure handling of JWT tokens.
Secure JWT Setup with feathers-authentication
Use feathers-authentication and feathers-authentication-jwt with explicit secret or public key verification. Never omit the secret or key, and avoid using weak or empty strings.
// app.js (or authentication setup file)
const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const app = feathers();
app.configure(authentication({
secret: process.env.JWT_SECRET,
entity: 'user',
service: 'users'
}));
app.configure(jwt());
// Ensure the secret is present and non-empty. Example environment check:
if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) {
throw new Error('JWT_SECRET must be a strong, non-empty secret of at least 32 bytes');
}
Custom Hook to Enforce Token Integrity and Claims Validation
Add a hook that verifies the token on each request and validates critical claims such as exp, nbf, and jti. This prevents acceptance of expired or replayed tokens.
// src/hooks/verify-jwt.js
module.exports = function verifyJwt() {
return async context => {
const { authentication } = context;
if (!authentication || !authentication.token) {
throw new Error('Authentication token is required');
}
const { jwt } = authentication;
// The framework should already verify the signature, but you can add extra checks:
if (!jwt.payload || !jwt.payload.sub) {
throw new Error('Invalid token payload');
}
// Example: enforce token usage only within allowed audiences if applicable
const allowedAudiences = ['myapp-api'];
if (jwt.payload.aud && !allowedAudiences.includes(jwt.payload.aud)) {
throw new Error('Invalid token audience');
}
// Optionally, check a custom revocation list or jti cache here
// if (await isTokenRevoked(jwt.payload.jti)) {
// throw new Error('Token has been revoked');
// }
return context;
};
};
// Attach the hook to relevant services
app.service('users').hooks({
before: {
all: [verifyJwt()]
}
});
Role and Permission Checks After Token Verification
Even after verifying the token, do not trust the payload for authorization. Always re-check roles or permissions against a server-side data store. This prevents attackers from tampering with the token to elevate privileges.
// src/hooks/authorize-role.js
module.exports = function authorizeRole(requiredRole) {
return async context => {
const { user } = context.params;
if (!user || !user.role) {
throw new Error('User role is missing');
}
if (user.role !== requiredRole) {
throw new Error('Insufficient permissions');
}
return context;
};
};
// Example usage on a sensitive service
app.service('admin-dashboard').hooks({
before: {
all: [
verifyJwt(),
authorizeRole('admin')
]
}
});
Stateless Token Revocation Pattern
To address replay and integrity across sessions, implement a short-lived token strategy combined with a revocation list stored in a fast store (e.g., Redis). Validate the token’s jti against this list on each request.
// src/hooks/revocation-check.js
const redisClient = require('./redis'); // your Redis client setup
module.exports = async function revocationCheck(context) {
const { jti, exp } = context.params.user.tokenPayload;
if (!jti) return context;
const revoked = await redisClient.get(`revoked:jti:${jti}`);
if (revoked) {
throw new Error('Token has been revoked');
}
// Optional: refresh sliding window for long sessions if needed
return context;
};