Skip to main content

Overview

Step-up authentication requires users to re-verify their identity before performing sensitive operations. PassAgent enforces a configurable time window (default: 5 minutes) during which a user is considered “recently authenticated.” Once the window expires, any privileged action triggers a re-authentication challenge before proceeding. This mechanism is layered on top of session authentication. A valid session token alone is not sufficient for high-risk operations — the user must also have authenticated within the step-up window.

Time-window model

When a user successfully logs in, verifies 2FA, or completes a re-authentication prompt, PassAgent records the timestamp in Redis via markAuthenticated(userId). Every sensitive API route calls requireRecentAuth() to check whether the recorded timestamp falls within the allowed window.
ParameterValue
Default max age300 seconds (5 minutes)
Redis key formatstepup:last_auth:{userId}
Key TTL86,400 seconds (24 hours)
Configurable per-routeYes (maxAgeSeconds parameter)
If no authentication record exists or the record is older than maxAgeSeconds, the API returns a 401 response with the STEP_UP_AUTH_REQUIRED error code and custom headers:
{
  "error": "Re-authentication required for this operation",
  "code": "STEP_UP_AUTH_REQUIRED",
  "maxAgeSeconds": 300
}
The response includes two headers that the client SDK uses to trigger the re-authentication flow:
  • x-require-reauth: true
  • x-reauth-max-age: 300
Every step-up challenge is recorded in the audit log with the action step_up_auth_required, including the operation that triggered it, the user’s IP address, and the elapsed time since last authentication.

Risk-adaptive enforcement

Beyond the fixed time window, PassAgent applies risk-adaptive step-up authentication powered by the anomaly detector. The enforceRiskAdaptiveStepUp() function evaluates behavioral signals in real time and can escalate requirements or block access entirely.

Anomaly signals

The behavior analysis engine scores each request against the user’s established profile. Signals that contribute to the anomaly score include:
SignalDetectionResulting action
Brute force5+ failed auth attempts in 5 minutesblock
Impossible travelGeo-IP distance / time implies physically impossible movementstep_up_auth
Suspicious travelUnusual but not impossible location changewarn
New deviceUnrecognized device fingerprintstep_up_auth
Revoked devicePreviously trusted device that was explicitly revokedblock
Bulk operations50+ vault reads/exports in 60 secondsstep_up_auth
IP spraySingle IP targeting multiple accountsblock
Access anomalyBehavioral score exceeds thresholdstep_up_auth or block
When the anomaly detector returns a block action, the request is denied with a 403 status regardless of how recently the user authenticated. The user must wait for the block to expire or contact support.

Tightened window

When a risk signal triggers step-up auth, the allowed window is reduced from the default 300 seconds to 60 seconds. This means the user must have re-authenticated in the last minute, not the last five minutes. The response includes an additional header x-risk-adaptive-step-up: true so the client can display appropriate context to the user.

2FA verification layer

For users with two-factor authentication enabled, PassAgent enforces an additional verification layer through the require2FA() guard. This operates independently of the step-up time window and uses a signed cookie to track verification state.
1

User triggers sensitive action

The API route calls require2FA(userId) after confirming session auth.
2

Check 2FA status

If the user has not enabled 2FA (user_2fa.enabled = false), the guard passes through. Backward compatibility is maintained for accounts without 2FA.
3

Validate cookie

The pa_2fa_verified cookie is checked. It contains a base64url-encoded payload signed with HMAC-SHA256 using a dedicated secret (TWOFA_VERIFIED_COOKIE_SECRET).
4

Server-side verification

Even if the cookie signature is valid, PassAgent verifies that a corresponding Redis record (2fa:verified:{userId}) still exists. This allows server-side invalidation on password change or logout.
5

Grant or deny

If all checks pass, the request proceeds. Otherwise, a 403 with code 2FA_REQUIRED is returned.
ParameterValue
Cookie namepa_2fa_verified
Cookie TTL15 minutes
Cookie flagshttpOnly, secure (production), sameSite: strict
Signature algorithmHMAC-SHA256
Server-side recordRedis key 2fa:verified:{userId} with matching TTL
The 2FA cookie uses dual validation: the HMAC signature prevents client-side tampering, while the Redis record allows immediate server-side revocation. Calling invalidate2FAVerification(userId) clears the Redis record, rendering the cookie useless even if it has not expired.

Operations requiring step-up auth

The following operations require recent authentication. Some routes use the default 5-minute window; others apply risk-adaptive enforcement with a tighter window.
  • Decrypt and view a stored password
  • Export vault data
  • Bulk credential operations
  • Delete vault items
  • Change master password
  • Enable or disable 2FA
  • Manage trusted devices
  • Transfer family admin privileges
  • Share credentials with family members
  • Initiate or complete guardian recovery
  • Create or revoke family invitations

API usage

To protect an API route with step-up authentication, call requireRecentAuth() early in the handler:
import { requireRecentAuth } from "@/lib/security/step-up-auth"

export async function POST(request: NextRequest) {
  // ... session auth ...
  const stepUp = await requireRecentAuth(userId, 300, {
    action: "vault_export",
    ipAddress: clientIp,
    userAgent: request.headers.get("user-agent") || undefined,
  })
  if (stepUp) return stepUp

  // Proceed with sensitive operation
}
For risk-adaptive enforcement, use enforceRiskAdaptiveStepUp():
import { enforceRiskAdaptiveStepUp } from "@/lib/security/risk-adaptive-step-up"

const riskCheck = await enforceRiskAdaptiveStepUp({
  userId,
  request,
  resourceType: "vault_export",
  resourceId: exportId,
  sessionId: session.id,
  stepUpMaxAgeSeconds: 60,
})
if (riskCheck) return riskCheck

Security properties

  • Defense in depth: step-up auth, 2FA verification, and anomaly detection operate as independent layers. Compromising one does not bypass the others.
  • Fail-closed: if the Redis timestamp is missing or unreadable, the system treats the user as unauthenticated and requires re-verification.
  • Audit trail: every step-up challenge and anomaly detection event is recorded with full context (IP, user agent, action, elapsed time).
  • Server-side revocation: 2FA verification can be invalidated instantly from the server without waiting for cookie expiration.