JWT Token Security

Medium

Detects weak JWT tokens (HS256, missing exp).

Estimated fix time: 30 minutes

What is JWT Token Security?

JSON Web Tokens (JWT) are commonly used for API authentication. However, weak JWT configurations can lead to serious security vulnerabilities including token forgery, replay attacks, and unauthorized access.

Security Impact

Severity: High

  • Token forgery and manipulation
  • Unauthorized API access
  • Replay attacks
  • Information disclosure
  • Session hijacking

Common JWT Vulnerabilities

  1. Weak Algorithms: Using HS256 with a weak secret
  2. Missing Expiration: Tokens that never expire
  3. No Token Refresh: Long-lived tokens without refresh mechanism
  4. Improper Secret Management: Hardcoded or weak secrets
  5. Algorithm Confusion: Accepting multiple algorithms

How to Fix

1. Use Strong Algorithms

If using Laravel Sanctum (recommended):

// config/sanctum.php
'expiration' => 60, // Token expires in 60 minutes

// Use Sanctum's secure token generation
$token = $user->createToken('api-token')->plainTextToken;

If using tymon/jwt-auth:

// config/jwt.php
'algo' => env('JWT_ALGO', 'HS256'),
'secret' => env('JWT_SECRET'),

// Ensure you use RS256 for production
'algo' => 'RS256',

2. Set Proper Expiration

// config/jwt.php
'ttl' => 60, // 60 minutes

'refresh_ttl' => 20160, // 2 weeks

// Force token refresh
'required_claims' => [
    'iss',
    'iat',
    'exp',
    'nbf',
    'sub',
    'jti',
],

3. Generate Strong Secrets

# Generate a secure JWT secret
php artisan jwt:secret

# Or use a 256-bit random key
php -r "echo base64_encode(random_bytes(32));"

Add to .env:

JWT_SECRET=your-secure-256-bit-secret-key-here
JWT_ALGO=HS256

4. Implement Token Refresh

use Tymon\JWTAuth\Facades\JWTAuth;

public function refresh(Request $request)
{
    try {
        $newToken = JWTAuth::parseToken()->refresh();
        
        return response()->json([
            'token' => $newToken,
            'token_type' => 'bearer',
            'expires_in' => auth()->factory()->getTTL() * 60
        ]);
    } catch (\Exception $e) {
        return response()->json(['error' => 'Cannot refresh token'], 401);
    }
}

5. Blacklist Old Tokens

// Enable blacklist
// config/jwt.php
'blacklist_enabled' => true,
'blacklist_grace_period' => 30,

// Invalidate token on logout
public function logout()
{
    try {
        JWTAuth::parseToken()->invalidate();
        return response()->json(['message' => 'Successfully logged out']);
    } catch (\Exception $e) {
        return response()->json(['error' => 'Logout failed'], 500);
    }
}

Laravel Sanctum is the modern, secure approach:

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
// app/Models/User.php
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens;
}

// Generate token with abilities
$token = $user->createToken('api-token', ['read', 'write'])->plainTextToken;

// Revoke tokens
$user->tokens()->delete();

7. Validate Token Claims

use Tymon\JWTAuth\Facades\JWTAuth;

protected function validateToken(Request $request)
{
    try {
        $token = JWTAuth::parseToken();
        $payload = $token->getPayload();
        
        // Verify issuer
        if ($payload->get('iss') !== config('app.url')) {
            throw new \Exception('Invalid token issuer');
        }
        
        // Verify audience
        if ($payload->get('aud') !== 'your-api') {
            throw new \Exception('Invalid token audience');
        }
        
        return $token->authenticate();
    } catch (\Exception $e) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }
}

Verification Steps

  1. Generate a new token and inspect its contents at jwt.io
  2. Verify the algorithm is HS256 or RS256 (not 'none')
  3. Check that exp claim exists and is reasonable (< 1 hour)
  4. Attempt to use an expired token and verify it's rejected
  5. Try to modify the token and verify signature validation fails

Best Practices

  1. Use Short-Lived Tokens: Keep TTL under 1 hour
  2. Implement Refresh Tokens: Allow extending sessions securely
  3. Store Secrets Securely: Use environment variables, never commit
  4. Use HTTPS Only: Never transmit tokens over HTTP
  5. Implement Token Rotation: Change secrets periodically
  6. Add Custom Claims: Include user roles, permissions
  7. Validate All Claims: Don't trust token content without verification
  8. Consider Sanctum: For Laravel apps, Sanctum is often simpler and more secure

Example: Complete Sanctum Setup

// routes/api.php
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
});

// LoginController.php
public function login(Request $request)
{
    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (! Auth::attempt($credentials)) {
        return response()->json(['message' => 'Invalid credentials'], 401);
    }

    $user = Auth::user();
    $token = $user->createToken('api-token', ['*'], now()->addHour())->plainTextToken;

    return response()->json([
        'token' => $token,
        'user' => $user,
    ]);
}

// Logout
public function logout(Request $request)
{
    $request->user()->currentAccessToken()->delete();
    return response()->json(['message' => 'Logged out']);
}
  • API Rate Limiting
  • Session Configuration
  • Brute Force Protection

Automatically detect this issue

StackShield can automatically scan your Laravel application for this security issue and alert you when it's detected.

Start Free Trial
Was this guide helpful?