---
title: Authentication Codes Must Expire Quickly
impact: MEDIUM
impactDescription: reduces the opportunity for an attacker to use intercepted codes
tags: authentication, codes, expiry, otp, security, php
---

## Authentication Codes Must Expire Quickly

Any temporary authentication identifiers—such as MFA codes, password reset tokens, or email verification links—must have a strict and limited lifespan. The longer a code is valid, the more time an attacker has to guess or intercept it.

**Incorrect (codes last too long or never expire):**

```php
// Verification code without expiry
$user->verification_code = '123456';
$user->save(); // Stays valid forever!

// Reset link valid for 7 days
$resetUrl = generateResetLink($user, 7 * 24 * 60 * 60);
```

**Correct (short lifespan and single-use logic):**

```php
// 1. Using Redis for TTL (Recommended for OTPs)
$otp = (string)random_int(100000, 999999);
$cacheKey = "auth_otp:{$user->id}";

// Store with 5-minute expiry (300 seconds)
Redis::setex($cacheKey, 300, json_encode([
    'code' => $otp,
    'attempts' => 0
]));

// 2. Verified and Single Use
public function verify(string $input) {
    $data = json_decode(Redis::get($cacheKey), true);
    
    if (!$data) return false;

    // Check attempts to prevent brute-force
    if ($data['attempts'] > 3) {
        Redis::del($cacheKey);
        throw new Exception("Too many attempts.");
    }

    if ($input === $data['code']) {
        Redis::del($cacheKey); // DELETE IMMEDIATELY AFTER USE
        return true;
    }
    
    // Increment attempts
    $data['attempts']++;
    Redis::setex($cacheKey, 300, json_encode($data));
    return false;
}
```

**Recommended Expiry Times:**
- **2FA/OTP (Short code)**: 5 - 10 minutes.
- **Magic Links**: 15 minutes.
- **Password Reset (Long token)**: 60 minutes.
- **Email Verification**: 24 hours.

**Best Practices:**
- **Single Use**: Invalidating the code immediately after a successful *or* too many failed attempts is mandatory.
- **Secure Generation**: Use `random_int()` or `random_bytes()` for generation (see rule **S010**).
- **Rate Limiting**: Limit how many times a code can be requested per user/IP.

**Tools:** Laravel `signed` routes, Redis `SETEX`, PHPUnit (for expiry tests)
