JWT Token Security
MediumDetects weak JWT tokens (HS256, missing exp).
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
- Weak Algorithms: Using HS256 with a weak secret
- Missing Expiration: Tokens that never expire
- No Token Refresh: Long-lived tokens without refresh mechanism
- Improper Secret Management: Hardcoded or weak secrets
- 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);
}
}
6. Use Laravel Sanctum (Recommended)
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
- Generate a new token and inspect its contents at jwt.io
- Verify the algorithm is HS256 or RS256 (not 'none')
- Check that
expclaim exists and is reasonable (< 1 hour) - Attempt to use an expired token and verify it's rejected
- Try to modify the token and verify signature validation fails
Best Practices
- Use Short-Lived Tokens: Keep TTL under 1 hour
- Implement Refresh Tokens: Allow extending sessions securely
- Store Secrets Securely: Use environment variables, never commit
- Use HTTPS Only: Never transmit tokens over HTTP
- Implement Token Rotation: Change secrets periodically
- Add Custom Claims: Include user roles, permissions
- Validate All Claims: Don't trust token content without verification
- 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']);
}
Related Issues
- 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