Building Secure JWT Authentication in Laravel with Refresh Tokens
Introduction
JWT (JSON Web Tokens) authentication is a popular choice for API authentication, but implementing it securely requires careful consideration of token expiration, refresh mechanisms, and security best practices. In this guide, we'll build a complete JWT authentication system in Laravel with refresh tokens to maintain security while providing seamless user experience.
Setting Up JWT in Laravel
First, install the tymon/jwt-auth package:
composer require tymon/jwt-auth
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secretConfigure your config/jwt.php file to use shorter access token lifetimes:
'ttl' => 15, // 15 minutes for access tokens
'refresh_ttl' => 20160, // 2 weeks for refresh tokens
'blacklist_enabled' => true,Database Migration for Refresh Tokens
Create a migration to store refresh tokens:
php artisan make:migration create_refresh_tokens_tableid();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('token', 500)->unique();
$table->timestamp('expires_at');
$table->ipAddress('ip_address')->nullable();
$table->string('user_agent')->nullable();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('refresh_tokens');
}
}Creating the RefreshToken Model
'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function isExpired(): bool
{
return $this->expires_at->isPast();
}
}JWT Service Class
Create a service class to handle JWT operations:
generateRefreshToken($user, $request);
return [
'access_token' => $accessToken,
'refresh_token' => $refreshToken,
'token_type' => 'bearer',
'expires_in' => config('jwt.ttl') * 60
];
}
private function generateRefreshToken(User $user, $request = null): string
{
// Clean up expired tokens
$user->refreshTokens()->where('expires_at', '<', now())->delete();
// Limit refresh tokens per user (optional security measure)
$maxTokens = 5;
$tokenCount = $user->refreshTokens()->count();
if ($tokenCount >= $maxTokens) {
$user->refreshTokens()
->oldest()
->limit($tokenCount - $maxTokens + 1)
->delete();
}
$token = Str::random(64);
RefreshToken::create([
'user_id' => $user->id,
'token' => hash('sha256', $token),
'expires_at' => Carbon::now()->addMinutes(config('jwt.refresh_ttl')),
'ip_address' => $request?->ip(),
'user_agent' => $request?->userAgent()
]);
return $token;
}
public function refreshAccessToken(string $refreshToken): ?array
{
$hashedToken = hash('sha256', $refreshToken);
$tokenRecord = RefreshToken::where('token', $hashedToken)
->where('expires_at', '>', now())
->with('user')
->first();
if (!$tokenRecord) {
return null;
}
$user = $tokenRecord->user;
$accessToken = JWTAuth::fromUser($user);
return [
'access_token' => $accessToken,
'token_type' => 'bearer',
'expires_in' => config('jwt.ttl') * 60
];
}
public function revokeRefreshToken(string $refreshToken): bool
{
$hashedToken = hash('sha256', $refreshToken);
return RefreshToken::where('token', $hashedToken)->delete() > 0;
}
}Authentication Controller
validate([
'email' => 'required|email',
'password' => 'required|string|min:6',
]);
if (!Auth::attempt($credentials)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$user = Auth::user();
$tokens = $this->jwtService->generateTokens($user, $request);
return response()->json([
'user' => $user,
...$tokens
]);
}
public function refresh(Request $request)
{
$request->validate([
'refresh_token' => 'required|string'
]);
$tokens = $this->jwtService->refreshAccessToken(
$request->refresh_token
);
if (!$tokens) {
return response()->json([
'message' => 'Invalid or expired refresh token'
], 401);
}
return response()->json($tokens);
}
public function logout(Request $request)
{
if ($refreshToken = $request->refresh_token) {
$this->jwtService->revokeRefreshToken($refreshToken);
}
Auth::logout();
return response()->json(['message' => 'Successfully logged out']);
}
}Security Best Practices
When implementing JWT authentication, consider these security measures:
- Short-lived access tokens: Use 15-30 minute expiration times
- Secure refresh token storage: Hash refresh tokens in the database
- Token rotation: Issue new refresh tokens periodically
- Rate limiting: Implement rate limits on authentication endpoints
- Device tracking: Store IP addresses and user agents for security monitoring
Frontend Integration
Here's how to handle token refresh on the frontend:
// Token refresh utility
class TokenManager {
static async refreshToken() {
const refreshToken = localStorage.getItem('refresh_token');
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken })
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('access_token', data.access_token);
return data.access_token;
}
} catch (error) {
// Redirect to login
window.location.href = '/login';
}
return null;
}
}Conclusion
This implementation provides a robust JWT authentication system with refresh tokens that balances security and user experience. The short-lived access tokens minimize exposure risk, while refresh tokens enable seamless token renewal. Remember to monitor token usage patterns and implement additional security measures like device fingerprinting and anomaly detection for production applications.
Related Posts
Advanced GraphQL Schema Design Patterns for Scalable APIs
Master GraphQL schema design patterns that scale with your application growth and team complexity.
Building a Real-time Chat System with Node.js and Socket.io: Complete Implementation Guide
Learn to build a production-ready real-time chat application with Node.js, Socket.io, and Redis for scalable message handling.
Building Secure JWT Authentication with NestJS Guards and Decorators
Learn to implement robust JWT authentication in NestJS using custom guards, decorators, and best security practices.