Credential Stuffing in Adonisjs with Firestore
Credential Stuffing in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability
Credential stuffing is an automated brute-force technique that relies on lists of known username and password pairs to gain unauthorized access. When an AdonisJS application uses Google Cloud Firestore as its primary user store and lacks adequate rate limiting or authentication controls, the attack surface is expanded in ways that differ from traditional SQL-based backends.
AdonisJS does not inherently protect against credential stuffing unless explicit rate limiting, account lockout, or CAPTCHA mechanisms are implemented. If the application relies on Firestore for authentication, each login attempt translates into a read or query operation against Firestore. Without proper request throttling, an attacker can issue a high volume of requests against a single endpoint, probing for valid credentials while staying under simplistic per-user limits.
A typical vulnerable pattern in AdonisJS is a basic email-based sign-in route that performs a Firestore doc.get() or where('email', '==', email) query without correlating the request to a per-IP or per-account rate limit. Because Firestore charges and quotas are separate from application logic, developers may underestimate how quickly an attacker can exhaust project quotas or trigger defensive responses that degrade service for legitimate users.
The absence of multi-factor authentication (MFA) further compounds the risk. Even if passwords are strong and stored with hashing, credential stuffing succeeds when users reuse passwords across services. Firestore security rules typically focus on document-level permissions rather than request origin or behavior, so they rarely prevent high-frequency authentication attempts unless explicitly designed to do so.
Additionally, error messages returned by AdonisJS during Firestore-based authentication can leak information. Distinguishing between an invalid email and an incorrect password allows attackers to enumerate valid accounts, reducing the effort required for successful credential stuffing. Without consistent, opaque responses and robust rate limiting across the authentication path, the combination of AdonisJS and Firestore remains susceptible to this class of attack.
Firestore-Specific Remediation in Adonisjs — concrete code fixes
To mitigate credential stuffing when using AdonisJS with Firestore, implement layered controls: rate limiting, consistent error handling, optional MFA, and operational monitoring. The following examples demonstrate concrete, production-grade patterns.
Rate limiting with the AdonisJS middleware
Use AdonisJS built-in rate limiter to restrict authentication attempts per IP or per user identifier. Configure the limiter in start/hooks.ts and apply it to your auth routes.
import { RouteMiddleware } from '@adonisjs/core/types/http'
export const authLimiter: RouteMiddleware = async (ctx, next) => {
const ip = ctx.request.ip()
const key = `auth_attempts:${ip}`
const rateLimiter = use('RateLimiter')
const allowed = await rateLimiter.check(key, 5, '1m') // 5 attempts per minute
if (!allowed) {
ctx.response.status(429).json({ error: 'Too many requests. Try again later.' })
return
}
await next()
}
Secure sign-in route with Firestore and consistent responses
Create an authentication route that applies the rate limiter, uses a stable error message, and safely interacts with Firestore. This example uses the official Firebase Admin SDK initialized in a provider.
import Route from '@ioc:Adonis/Core/Route'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { initializeApp, cert } from 'firebase-admin/app'
import { getFirestore, FieldValue } from 'firebase-admin/firestore'
// Initialize once in your app setup or provider
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
})
const db = getFirestore()
Route.post('login', [authLimiter], async ({ request, response }: HttpContextContract) => {
const { email, password } = request.only(['email', 'password'])
try {
const usersSnapshot = await db.collection('users').where('email', '==', email).limit(1).get()
if (usersSnapshot.empty) {
// Always return the same generic message to avoid user enumeration
return response.status(401).json({ error: 'Invalid credentials' })
}
const userDoc = usersSnapshot.docs[0]
const storedHash = userDoc.get('passwordHash')
const isLocked = userDoc.get('lockedUntil') && userDoc.get('lockedUntil').toDate() > new Date()
if (isLocked) {
return response.status(403).json({ error: 'Account temporarily locked. Try again later.' })
}
const isValid = await verifyPassword(password, storedHash)
if (!isValid) {
// Increment failed attempts atomically
await db.collection('users').doc(userDoc.id).update({
failedAttempts: FieldValue.increment(1),
lastFailedAt: FieldValue.serverTimestamp(),
})
return response.status(401).json({ error: 'Invalid credentials' })
}
// Reset on success
await db.collection('users').doc(userDoc.id).update({
failedAttempts: 0,
lockedUntil: null,
})
const token = generateSessionToken(userDoc.id)
return response.json({ token, user: { id: userDoc.id, email: userDoc.get('email') } })
} catch (error) {
// Log for investigation but avoid exposing details
console.error('Auth error', error)
return response.status(500).json({ error: 'Unable to process request' })
}
})
Lockout and security rules in Firestore
Complement application-level controls with Firestore security rules that discourage abuse. While rules cannot enforce rate limits, they can require authentication for sensitive operations and validate data shape to prevent malformed writes.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
// Additional constraints can enforce allowed fields during updates
allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['failedAttempts', 'lockedUntil', 'email']) &&
request.resource.data.failedAttempts is int &&
(request.resource.data.lockedUntil is timestamp || request.resource.data.lockedUntil is null);
}
}
}
Operational practices
- Monitor Firestore read and quota usage to detect abnormal spikes that may indicate credential spraying.
- Consider adding optional MFA for high-risk sign-in contexts, storing second-factor records in a subcollection under the user document.
- Use consistent, non-enumerating error responses across all authentication endpoints.