HIGH credential stuffingadonisjsjwt tokens

Credential Stuffing in Adonisjs with Jwt Tokens

Credential Stuffing in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability

Credential stuffing is an automated attack in which lists of breached username and password pairs are systematically submitted to a login endpoint. When an API uses JWT tokens for stateless authentication in AdonisJS, several specific conditions can amplify risk or create exposure even when traditional session-based protections are absent.

In AdonisJS, JWT handling is commonly implemented via the jwt provider in start/hooks.ts and token generation typically occurs after verifying credentials. If rate limiting is not enforced on the login route, an attacker can submit many credential pairs per session, and each valid pair results in a JWT being issued. Because JWTs are self-contained and do not require server-side session storage, there is no built-in mechanism to track or revoke an issued token after compromise without additional design.

AdonisJS applications that embed excessive claims in a JWT (such as roles or permissions) without additional authorization checks can magnify the impact of a successful credential stuffing attack. An attacker who obtains a valid token can use it directly until expiration, and because the token is signed with a secret or private key, the server may treat it as authentic without re-verifying business-level constraints on each request. If the application also exposes endpoints that perform sensitive operations based solely on token validity—without re-checking the user’s credentials or context—an attacker can escalate misuse across the API surface.

The interaction with refresh token flows can further increase exposure. If refresh tokens are long-lived, stored insecurely, or lack strict binding to the original authentication context, an attacker who obtains a pair from a credential stuffing campaign can use the refresh mechanism to generate new JWTs indefinitely. AdonisJS applications that do not implement device or context binding, one-time use checks for refresh tokens, or strict rotation policies may inadvertently enable persistent access derived from a single credential in a breached list.

Proper threat modeling for this combination requires recognizing that JWTs do not inherently prevent automated credential testing; they shift the boundary to token issuance and usage policies. Defenses must therefore focus on pre-issuance controls (login hardening) and post-issuance constraints (token scope, validation, and revocation strategies). Without these considerations, an AdonisJS API that relies only on JWTs can allow credential stuffing to succeed and abuse to persist across multiple requests.

Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes

Remediation focuses on strengthening the login flow, tightening token contents, and adding usage constraints. The following examples assume you are using the official AdonisJS JWT provider and demonstrate concrete, realistic code changes.

1) Enforce rate limiting on the login route

Apply a rate limiter to the login route to reduce the feasibility of credential stuffing. In start/routes.ts, use an AdonisJS built-in or custom rate limiter to restrict attempts per IP or identifier.

import Route from '@ioc:Adonis/Core/Route'
import { DateTime } from 'luxon'

Route.post('login', 'AuthController.login').rateLimit({
  identifier: (ctx) => ctx.request.ip(),
  limit: 5,
  window: DateTime.now().plus({ minutes: 1 }).toJSDate(),
})

This example limits each IP to 5 login attempts per minute, raising the cost for attackers without affecting legitimate users under normal conditions.

2) Minimize JWT claims and avoid embedding sensitive permissions

Include only essential claims in the JWT. Store roles and permissions server-side or enforce authorization on each request via a policy or guard, rather than trusting the token alone.

import { BaseAuthProvider } from '@ioc:Adonis/Addons/Auth'
import jwt from 'jsonwebtoken'

export default class AuthController {
  public async login({ request, auth, response }) {
    const { email, password } = request.only(['email', 'password'])
    const user = await auth.use('api').verifyCredentials(email, password)

    if (!user) {
      return response.badRequest({ message: 'Invalid credentials' })
    }

    const payload = {
      sub: user.id,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 60 * 30, // 30 minutes
      scopes: ['authenticated'], // minimal scope
    }

    const token = jwt.sign(payload, Env.get('JWT_SECRET'))
    return response.ok({ token })
  }
}

The token here carries only a subject and a short expiration, avoiding inline role data that could be abused if leaked.

3) Enforce refresh token rotation and binding

If your application uses refresh tokens to issue new JWTs, bind them to the original authentication context and rotate on each use. Store refresh tokens with metadata such as user ID, device fingerprint, and issued IP, and validate these on refresh.

import { DateTime } from 'luxon'

// Example validation inside a refresh endpoint
public async refresh({ request, response }) {
  const { refresh_token, current_ip, device_id } = request.only(['refresh_token', 'current_ip', 'device_id'])

  const decoded = jwt.verify(refresh_token, Env.get('REFRESH_SECRET'))
  const stored = await Db.from('refresh_tokens').where({ token: refresh_token }).first()

  if (!stored || stored.user_id !== decoded.sub || stored.ip !== current_ip || stored.device_id !== device_id) {
    return response.unauthorized({ message: 'Invalid refresh token' })
  }

  // Rotate: revoke current and issue new
  await Db.from('refresh_tokens').where({ id: stored.id }).delete()

  const newToken = jwt.sign({
    sub: decoded.sub,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 7, // 7 days
  }, Env.get('JWT_SECRET'))

  const newRefresh = jwt.sign({
    sub: decoded.sub,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days
    ip: current_ip,
    device_id: device_id,
  }, Env.get('REFRESH_SECRET'))

  await Db.table('refresh_tokens').insert({ user_id: decoded.sub, token: newRefresh, ip: current_ip, device_id: device_id })
  return response.ok({ token: newToken, refresh_token: newRefresh })
}

This approach reduces the window for token replay and ensures that stolen refresh tokens are less reusable across contexts.

4) Use short access token lifetimes and implement token revocation

Keep access token lifetimes short and provide a server-side revocation list or introspection for critical operations. This does not require storing every token but can use a denylist for compromised identifiers.

import { DateTime } from 'luxon'

public async logout({ request, response }) {
  const token = request.headers().authorization?.replace('Bearer ', '')
  if (!token) return response.badRequest({ message: 'Missing token' })

  const decoded = jwt.verify(token, Env.get('JWT_SECRET'))
  const exp = decoded.exp * 1000
  const now = Date.now()

  // Store token identifier (e.g., jti) and expiry in a fast store (Redis, etc.)
  await Db.table('token_denylist').insert({
    jti: decoded.jti || `${decoded.sub}:${now}`,
    exp,
  })

  return response.ok({ message: 'Token revoked' })
}

On protected routes, check the denylist before trusting the token’s claims. Combined with short expirations, this limits the usability of tokens obtained via credential stuffing.

Frequently Asked Questions

Does AdonisJS provide built-in protection against credential stuffing when using JWTs?
AdonisJS does not automatically prevent credential stuffing when using JWTs. Protection depends on how you configure routes, rate limiting, token contents, and refresh token handling. You must explicitly add rate limiters, minimize JWT claims, and enforce token binding and rotation to reduce risk.
Can a security scanner help detect credential stuffing risks in an AdonisJS JWT implementation?
A scanner that tests authentication endpoints and analyzes token issuance behavior can identify missing rate limits, excessive claims in tokens, and weak refresh token handling. Findings should be mapped to remediation steps such as adding rate limiting, reducing token lifetimes, and enforcing token binding.