HIGH time of check time of useadonisjsjwt tokens

Time Of Check Time Of Use in Adonisjs with Jwt Tokens

Time Of Check Time Of Use in Adonisjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability

Time of Check Time of Use (TOCTOU) is a class of race condition where a system checks a condition (such as permissions or token validity) and later relies on that condition when performing an action, but the state can change between the check and the use. In AdonisJS applications that use JWT tokens for authentication, TOCTOU can arise when authorization logic depends on token claims that may become stale or invalid between verification and usage.

Consider a typical pattern: a middleware verifies a JWT token, attaches a payload (e.g., user ID and role) to the request object, and a downstream handler performs an operation such as updating a user record. If the token was issued with a long lifetime or contains mutable claims (like roles), an attacker who can influence the subject of the token (for example, through account takeover or token replacement if leakage occurs) or can cause the backend state to change (such as role revocation or permission changes in the database) may exploit the window between check and use. AdonisJS does not inherently protect against this window; it is up to the developer to ensure that critical decisions are made with fresh, authoritative checks rather than relying solely on decoded token claims.

With JWT tokens, the server typically does not maintain session state, so the check often consists of verifying the token signature and expiration, then trusting claims like sub, role, or permissions. If an endpoint uses these claims to authorize an action (for example, checking req.auth.user.role === 'admin') without revalidating against a source of truth at the time of the action, a TOCTOU vulnerability exists. For instance, an attacker who compromises a lower-privilege token could attempt to reuse it in a context where the backend has since elevated the user’s privileges, or an admin whose permissions are revoked might still be authorized via a previously issued token until it expires. Because JWTs are self-contained and often cached client-side, the effective authorization check is deferred, creating a race condition between token validation and resource access.

In AdonisJS, this can manifest in routes that rely on decoded JWT payloads for authorization without performing per-request validation of the underlying data. For example, an endpoint that deletes a record might first check the token’s user ID and then perform the delete based on that ID. If the token was issued when the user had broader permissions, or if the backend state changed (such as ownership reassignment), the check no longer aligns with the use, potentially allowing unauthorized operations. The vulnerability is compounded if the token contains sensitive claims that should not be trusted for authorization without additional verification, such as tenant or scope checks that are enforced at runtime.

To mitigate TOCTOU in this context, authorization decisions should be based on data verified at the moment of the request, not solely on token claims that may be outdated. This means re-fetching and validating relevant state (such as user roles, permissions, or ownership) from the database or an authoritative service within the request handling flow, even when a valid JWT is present. Rate limiting and short token lifetimes reduce the window of exposure, but they do not eliminate the need for authoritative checks at the time of use.

Jwt Tokens-Specific Remediation in Adonisjs — concrete code fixes

Remediation focuses on ensuring that authorization checks are performed with up-to-date, authoritative data rather than relying exclusively on JWT claims. Below are concrete patterns and code examples for AdonisJS that reduce TOCTOU risk when using JWT tokens.

1. Always re-fetch sensitive state before acting

Do not trust claims such as user ID or role for critical operations. Re-fetch the record from the database within the handler or a scoped service to confirm current permissions and ownership.

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'

export default class ProfilesController {
  public async updateProfile({ auth, request, response }: HttpContextContract) {
    const userId = auth.user?.id
    if (!userId) {
      return response.unauthorized()
    }

    // Re-fetch the user to ensure state is current
    const user = await User.find(userId)
    if (!user) {
      return response.notFound()
    }

    // Perform authorization using the fresh instance
    if (user.role !== 'admin' && user.id !== auth.user?.id) {
      return response.forbidden()
    }

    // Proceed with update using user instance
    user.merge(request.only(['bio', 'theme']))
    await user.save()

    return user
  }
}

2. Use route parameters for ownership checks, not token claims

When operating on a resource identified by an ID (e.g., /users/:id/profile), validate that the authenticated subject owns or is permitted to act on that specific ID at request time.

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Profile from 'App/Models/Profile'

export default class ProfilesController {
  public async showProfile({ params, auth, response }: HttpContextContract) {
    const profile = await Profile.findOrFail(params.id)

    // Ensure the profile belongs to the authenticated user
    if (profile.userId !== auth.user?.id) {
      return response.forbidden()
    }

    return profile
  }
}

3. Validate scopes and tenant context at runtime

If your JWT includes scopes or tenant identifiers, re-validate these against the target resource to prevent horizontal or vertical privilege escalation.

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Tenant from 'App/Models/Tenant'

export default class DocumentsController {
  public async deleteDocument({ params, auth, response }: HttpContextContract) {
    const document = await Document.findOrFail(params.id)

    // Re-check tenant association
    const tenant = await Tenant.find(document.tenantId)
    if (!tenant || tenant.id !== auth.user?.tenantId) {
      return response.forbidden()
    }

    await document.delete()
    return { message: 'Document deleted' }
  }
}

4. Prefer short-lived tokens and refresh with re-validation

Use short expiration times for access tokens and implement refresh flows that re-authenticate and re-authorize, reducing the impact of a compromised token and shrinking the TOCTOU window.

// Example token configuration in start/auth.ts
import { AuthConfig } from '@ioc:Adonis/Addons/Auth'

const authConfig: AuthConfig = {
  guard: 'api',
  token: {
    age: '15m', // short lifetime
    allowRememberMe: false,
  },
}

export default authConfig

5. Avoid using mutable claims for authorizationDo not rely on roles or permissions embedded in the JWT for actions that require current backend state. Treat token claims as identity hints only, and verify permissions against the latest data store.

// Bad: relying on token role without re-check
// if (req.auth.user.role !== 'admin') { ... }

// Good: re-fetch or verify role via a service that checks current state
const canManage = await permissionService.can(auth.user?.id, 'manage:users', targetResource)
if (!canManage) {
  return response.forbidden()
}

Frequently Asked Questions

Why is re-fetching user state necessary even when a valid JWT is present in AdonisJS?
JWTs are self-contained and often long-lived; claims such as role or permissions can become stale if the backend state changes (e.g., role revocation or privilege escalation). Re-fetching authoritative data before sensitive operations closes the race condition window between check and use (TOCTOU).
Does using short token lifetimes alone prevent TOCTOU in AdonisJS applications?
Short lifetimes reduce exposure but do not eliminate TOCTOU. An attacker with a valid token can still exploit the gap between validation and action if authorization relies solely on token claims. Authoritative re-checks at the time of use are required regardless of token lifetime.