Credential Stuffing in Feathersjs with Jwt Tokens
Credential Stuffing in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Credential stuffing is a brute-force technique in which attackers use lists of previously breached username and password pairs to gain unauthorized access. When an application uses JWT tokens for authentication in a FeathersJS service, the framework’s typical flow—receiving credentials, verifying them, and returning a signed token—can expose endpoints to automated, high-volume login attempts. If rate limiting is absent or misconfigured, attackers can submit many credential guesses against the authentication endpoint without meaningful throttling. Even though JWTs are cryptographically signed and tamper-evident, the vulnerability lies in the authentication handler before token issuance, not in the token format itself.
FeathersJS does not enforce authentication by default; it relies on explicit configuration and hooks. If an authentication hook directly validates a user’s credentials and issues a JWT without additional protections, each request becomes a potential login attempt. Attackers may target the endpoint with credential pairs derived from known breaches, hoping to find valid accounts. Successful authentication results in a valid JWT, which effectively grants access as if the user had logged in legitimately. Because JWTs can carry claims such as roles and permissions, a compromised credential can lead to privilege escalation if authorization checks are weak elsewhere in the service.
Another contributing factor is the handling of token validity and error messages. Inconsistent responses—such as distinguishing between ‘user not found’ and ‘invalid password’—can aid attackers in enumerating valid usernames. If JWT signing keys are weak or improperly stored, tokens may be predictable or subject to other implementation flaws, but credential stuffing primarily exploits the absence of protective controls around the login pathway rather than the JWT algorithm itself. Without mechanisms such as rate limiting, account lockout policies, or multi-factor authentication, a FeathersJS service using JWT tokens remains susceptible to automated credential reuse attacks.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on hardening the authentication flow and reducing the effectiveness of credential stuffing. Implement robust rate limiting at the service or global level to restrict the number of login attempts per identifier within a time window. Standard JWT validation and signing practices should be followed to avoid introducing new weaknesses. The following examples illustrate a secured FeathersJS authentication setup.
Example 1: Rate-limited authentication hook with JWT
// src/hooks/authentication.js
const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication');
const { iff, isProvider } = require('@feathersjs/hooks-common');
const rateLimit = require('feathers-rate-limit');
const authentication = new AuthenticationService(app);
authentication.use('jwt', new JWTStrategy());
// Apply rate limiting to the local (email/password) authentication endpoint
app.service('authentication').hooks({
before: {
create: [
iff(isProvider('external'), authentication.hooks.authenticate(['jwt'])),
rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // Maximum 5 attempts per window per key
keyExtractor: (context) => {
// Use email or a composite identifier if available from body
return (context.data && context.data.email) ||
(context.params.query && context.params.query.email) ||
context.params.remoteAddress;
}
})
]
}
});
module.exports = {
authentication
};
Example 2: Secure authentication service configuration and JWT payload handling
// src/authentication.js
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
module.exports = function (app) {
const authConfig = app.get('authentication');
// Set up JWT with a strong secret from environment variables
app.configure(authentication(authConfig));
app.configure(jwt({
secret: process.env.JWT_SECRET,
expiresIn: '1h',
header: { typ: 'JWT' },
algorithms: ['HS256']
}));
// Ensure the user entity does not return sensitive fields in the JWT payload
app.service('users').hooks({
before: {
create: [authentication.hooks.hashPassword({ passwordField: 'password' })],
patch: [authentication.hooks.hashPassword({ passwordField: 'password' })]
},
after: {
create(context) {
const { password, ...safeUser } = context.result || {};
context.result = safeUser;
return context;
}
}
});
};
Example 3: Consistent error handling and additional protections
// src/hooks/errors.js
// Use a generic authentication failure message to prevent user enumeration
module.exports = {
errorHandler(err, context) {
if (context.result && context.result.error && context.result.error.message) {
const message = context.result.error.message;
if (message.toLowerCase().includes('not found') || message.toLowerCase().includes('invalid')) {
// Return a generic message for authentication-related errors
context.result.error.message = 'Invalid credentials';
}
}
return context;
}
};
Compliance mappings
| Control | OWASP API Top 10 | PCI-DSS |
|---|---|---|
| Rate limiting on authentication | Broken Object Level Authorization (BOLA) | 8.1.5 |
| Generic error messages | Improper Inventory Management | 7.2 |