Brute Force Attack in Adonisjs with Firestore
Brute Force Attack in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability
A brute force attack against an AdonisJS application using Firestore as the backend typically targets the authentication endpoint. When login routes are not protected by effective rate limiting or account lockout, an attacker can submit a high volume of password guesses against a specific user or email. Because AdonisJS often exposes a REST or JSON API, automated scripts can make thousands of requests per minute. Firestore’s default security rules may allow public read or write access to user documents if not explicitly restricted, which can enable an attacker to enumerate valid user identifiers (e.g., by observing differences between “user not found” and “incorrect password”). This information asymmetry reduces the effort required to guess credentials. Even when Firestore rules restrict access, misconfigured rules might permit unauthenticated queries on the users collection, exposing metadata that aids enumeration. In server-side sessions or token-based flows, weak session storage or predictable tokens can further weaken the overall posture. The combination of AdonisJS application logic and Firestore as the data store can inadvertently amplify brute force risk when protections such as rate limiting, account lockout, and secure rule configuration are incomplete.
Firestore-Specific Remediation in Adonisjs — concrete code fixes
To mitigate brute force risks, combine AdonisJS application-level protections with secure Firestore rules and data modeling. Implement rate limiting on authentication routes using AdonisJS middleware. For example, in start/hooks.ts, register a rate limiter hook:
import { Exception } from '@poppinss/utils'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export const rateLimiter = {
async handle({ request, auth, response }: HttpContextContract, next: () => Promise) {
const email = request.input('email')
const key = `login_attempts:${email}`
const count = await use('redis').get(key)
if (count && Number(count) >= 10) {
throw new Exception('Too many attempts, try again later', 429, 'E_TOO_MANY_REQUESTS')
}
await next()
// increment on success only
if (/* login success */) {
await use('redis').set(key, 0, { expiresIn: '1h' })
} else {
await use('redis').increment(key, 1, { expiresIn: '1h' })
}
},
}
Secure your Firestore rules to prevent public enumeration and enforce per-user limits. Require authentication for user document access and avoid broad allow-read rules. Example Firestore rules to restrict user document reads to the owner and admins:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
// Optionally allow admin reads via custom claims
allow read: if request.auth != null && get(/databases/$(database)/documents/admins/$(request.auth.uid)).exists;
}
// Deny public enumeration by preventing list operations on users
match /users/{userId=**} {
allow list: if false;
}
}
}
In AdonisJS, structure your login function to use consistent timing and responses to avoid user enumeration. Use the Firestore SDK safely with hashed lookups. Example login handler in a controller:
import { schema } from '@ioc:Adonis/Core/Validator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { Firestore, getFirestore } from '@google-cloud/firestore'
import { compareSync } from 'bcryptjs'
export default class AuthController {
private db: Firestore
constructor() {
this.db = getFirestore()
}
public async login({ request, auth, response }: HttpContextContract) {
const payload = request.validate({
schema: schema.create({
email: schema.string({ trim: true, normalize: true }),
password: schema.string.optional(),
}),
})
// Use a stable delay to reduce timing differences
await new Promise((r) => setTimeout(r, 300))
const userDoc = await this.db.collection('users').where('email', '==', payload.email).limit(1).get()
const user = userDoc.empty ? null : userDoc.docs[0].data()
// Always run hash comparison to avoid timing leaks
const hashed = user ? user.passwordHash : '$2a$10$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
const valid = compareSync(payload.password, hashed)
if (!valid) {
return response.badRequest({ message: 'Invalid credentials' })
}
// Issue token or session
const token = auth.use('api').generate(user)
return response.ok({ token })
}
}
Additionally, monitor authentication endpoints via the middleBrick dashboard or CLI to detect abnormal request patterns. The Pro plan supports continuous monitoring and can integrate with GitHub Action to fail builds if risk scores degrade, helping you catch insecure configurations before deployment. These steps reduce brute force success by increasing attacker effort and limiting useful feedback from the API.