Password Spraying in Adonisjs with Mutual Tls
Password Spraying in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication attack where a single compromised credential is tested against many accounts to avoid account lockouts. When mutual TLS (mTLS) is used in AdonisJS, the presence of client certificates can shift authentication assumptions and inadvertently enable or amplify password spraying risks.
In AdonisJS applications that support both mTLS and traditional username/password flows, developers may assume mTLS fully secures access to authentication endpoints. This can lead to weaker controls on password-based login routes. If an endpoint accepts both a client certificate and a password, the two factors may be combined in a way that does not enforce strict rate limiting or anomaly detection. An attacker with a list of usernames can perform password spraying by presenting a valid client certificate alongside each password attempt, potentially bypassing IP-based rate limits or single-factor lockout policies that only consider passwords.
Mutual TLS binds identity to the connection, but if the application does not treat the certificate as the primary authenticator and still allows password guessing against accounts with valid certificates, the password spray can be executed at the application layer without triggering certificate revocation or connection-level failures. Additionally, if certificate validation is performed after password verification, timing differences or inconsistent error handling may leak whether a username exists, further aiding the attacker’s enumeration. AdonisJS middleware that does not explicitly require mTLS for all authentication paths may allow unauthenticated or partially authenticated requests to reach password verification code, creating an avenue for low-and-slow spraying across accounts that present valid client certificates.
Another subtle risk arises when mTLS is enforced for administrative interfaces but not for user-facing authentication routes. Attackers can focus on the weaker path while relying on stolen or guessed client certificates from compromised internal services. Even when rate limiting is applied, it is often scoped to IP addresses rather than to certificate identities, allowing an attacker to rotate source IPs while using the same certificate for password attempts. In API-driven AdonisJS services that expose OAuth or session-based login alongside mTLS-enabled endpoints, inconsistent enforcement can create a scenario where password spraying is only partially logged, making detection difficult.
To detect these issues, scans such as those provided by middleBrick examine whether authentication controls, including mTLS requirements, rate limiting, and anomaly detection, are consistently applied across all authentication flows. They also verify that error messages do not disclose account existence and that certificate-bound identities are treated as primary authentication factors rather than optional enhancements.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on strict enforcement of mutual TLS, consistent authentication logic, and robust rate limiting tied to certificate identity. The following examples show how to require client certificates, validate them early, and avoid combining weak password checks with strong identity bindings.
Enforce Mutual TLS in AdonisJS HTTP Server
Configure the HTTPS server to require client certificates and validate them before routing requests to application logic.
const fs = require('fs');
const https = require('https');
const { Ignitor } = require('@adonisjs/ignitor');
const server = https.createServer({
key: fs.readFileSync('path/to/server-key.pem'),
cert: fs.readFileSync('path/to/server-cert.pem'),
ca: fs.readFileSync('path/to/ca-cert.pem'),
requestCert: true,
rejectUnauthorized: true,
});
new Ignitor(require('@adonisjs/fold'))
.appRoot(__dirname)
.fireHttpServer()
.then(({ app, server }) => {
server.on('request', app.callback());
});
This configuration ensures that only clients with a certificate signed by the trusted CA can establish a TLS connection. Setting requestCert and rejectUnauthorized ensures the server validates the client certificate chain.
Verify Certificate Identity in Middleware
Create middleware that extracts and validates certificate details, binding them to the authentication context before any password checks are considered.
// start/hooks/verify-client-cert.js
const certificateFingerprint = (cert) => {
const crypto = require('crypto');
return crypto.createHash('sha256').update(cert).digest('hex');
};
const verifyClientCert = async ({ request, response, auth }, next) => {
const { client } = request;
if (!client || !client.verified) {
return response.status(403).send('Client certificate required');
}
const cert = client.authorizedCertificate;
const fingerprint = certificateFingerprint(cert.export({ type: 'spki', format: 'pem' }));
const allowedFingerprints = new Set([
'a1b2c3d4e5f67890...', // precomputed allowed fingerprints
]);
if (!allowedFingerprints.has(fingerprint)) {
return response.status(403).send('Certificate not authorized');
}
// Attach identity to request for downstream use
request.authIdentity = { type: 'certificate', fingerprint };
await next();
};
module.exports = verifyClientCert;
Register this middleware globally or on authentication routes to ensure certificate identity is established before password logic runs.
Separate Authentication Paths and Apply Rate Limiting by Certificate
Define distinct routes for certificate-bound access and password-based login, and apply rate limiting using the certificate fingerprint as part of the key.
// start/routes.js
const Route = use('Route');
const rateLimiter = use('RateLimiter');
const verifyClientCert = use('App/Helpers/verifyClientCert');
// Certificate-bound secure endpoint
Route.get('/secure/data', verifyClientCert, async (ctx) => {
const { authIdentity } = ctx.request;
// Proceed with certificate-verified logic
ctx.response.send({ user: authIdentity.fingerprint, data: 'protected' });
});
// Password login with rate limiting keyed by certificate fingerprint and username
Route.post('/login', async (ctx) => {
const { username, password } = ctx.request.all();
const fingerprint = ctx.request.authIdentity?.fingerprint;
const key = fingerprint ? `${fingerprint}:${username}` : `legacy:${username}`;
const attempts = await rateLimiter.consume(key, 5, 300); // 5 attempts per 300s
if (!attempts) {
return ctx.response.status(429).send('Too many attempts');
}
// Perform password verification only after rate check and certificate identity is available
const user = await User.findBy('username', username);
if (!user || user.password !== password) {
return ctx.response.status(401).send('Invalid credentials');
}
ctx.session.put('userId', user.id);
ctx.response.send({ ok: true });
});
By separating flows and tying rate limits to the certificate fingerprint plus username, password spraying that uses a valid certificate is constrained per identity, not just per IP.
Consistent Error Handling and Logging
Ensure error responses do not reveal whether a username exists and log authentication attempts with certificate metadata for audit and detection.
Route.post('/login', async (ctx) => {
const { username } = ctx.request.all();
const fingerprint = ctx.request.authIdentity?.fingerprint || 'none';
try {
// ... authentication logic
ctx.response.status(401).send('Invalid credentials');
} catch (err) {
console.warn('Auth attempt', { username, fingerprint, timestamp: new Date().toISOString() });
ctx.response.status(401).send('Invalid credentials');
}
});
Use middleBrick scans to validate that authentication endpoints enforce mTLS, apply certificate-aware rate limiting, and do not leak user existence through error messages or timing differences.