This guide demonstrates how to implement access and refresh tokens with Laravel Sanctum for secure, token-based authentication.
Note: For installing Sanctum, refer to the official Laravel Sanctum documentation. Laravel Sanctum is also installed automatically when using Laravel Jetstream.
Auth controller
We'll create an AuthController to handle login, refresh, and logout functions for token management.
php artisan make:controller AuthController
Below is the full app/Http/Controllers/AuthController.php code:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use Laravel\Sanctum\PersonalAccessToken;
use Carbon\Carbon;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (!Auth::attempt($request->only('email', 'password'))) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$user = Auth::user();
$user->tokens()->delete(); // Clear existing tokens for a fresh session
// Define token expiration times
$accessTokenExpiresAt = Carbon::now()->addDays(1);
$refreshTokenExpiresAt = Carbon::now()->addDays(7);
// Create access and refresh tokens
$accessToken = $user->createToken('access_token', ['*'], $accessTokenExpiresAt)->plainTextToken;
$refreshToken = $user->createToken('refresh_token', ['refresh'], $refreshTokenExpiresAt)->plainTextToken;
return response()->json([
'access_token' => $accessToken,
'access_token_expires_at' => $accessTokenExpiresAt,
'refresh_token' => $refreshToken,
'refresh_token_expires_at' => $refreshTokenExpiresAt,
'token_type' => 'Bearer',
]);
}
public function refreshToken(Request $request)
{
$currentRefreshToken = $request->bearerToken();
$refreshToken = PersonalAccessToken::findToken($currentRefreshToken);
if (!$refreshToken || !$refreshToken->can('refresh') || $refreshToken->expires_at->isPast()) {
return response()->json(['error' => 'Invalid or expired refresh token'], 401);
}
$user = $refreshToken->tokenable;
$refreshToken->delete();
$accessTokenExpiresAt = Carbon::now()->addDays(1);
$refreshTokenExpiresAt = Carbon::now()->addDays(7);
$newAccessToken = $user->createToken('access_token', ['*'], $accessTokenExpiresAt)->plainTextToken;
$newRefreshToken = $user->createToken('refresh_token', ['refresh'], $refreshTokenExpiresAt)->plainTextToken;
return response()->json([
'access_token' => $newAccessToken,
'access_token_expires_at' => $accessTokenExpiresAt,
'refresh_token' => $newRefreshToken,
'refresh_token_expires_at' => $refreshTokenExpiresAt,
'token_type' => 'Bearer',
]);
}
public function logout(Request $request)
{
$request->user()->tokens()->delete();
return response()->json(['message' => 'Logged out successfully'], 200);
}
}
Explanation of Methods
-
login
: Validates credentials, clears existing tokens, and issues both an access token (valid for 1 day) and a refresh token (valid for 7 days). -
refreshToken
: Verifies the refresh token, deletes it, and issues new access and refresh tokens. -
logout
: Revokes all tokens associated with the user, effectively logging them out.
Defining API Routes
Add these routes in routes/api.php:
<?php
use App\Http\Controllers\AuthController;
Route::post('/login', [AuthController::class, 'login']);
Route::post('/refresh', [AuthController::class, 'refreshToken']);
Route::middleware('auth:sanctum')->post('/logout', [AuthController::class, 'logout']);
To protect additional routes, use the auth:sanctum middleware:
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return new UserResource($request->user());
});
// other protected routes...
});
Limiting Returned User Information with UserResource
To control what user information is returned, create a app/Http/Resources/UserResource.php. Run:
php artisan make:resource UserResource
Define only the fields you want in UserResource.php:
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
];
}
}
Request Examples and Responses
Here’s how to use each endpoint with request and response examples.
- Login
Request
POST /api/login
{
"email": "test@example.com",
"password": "password"
}
Response
{
"access_token": "your-access-token",
"access_token_expires_at": "2024-11-10T10:15:30.000000Z",
"refresh_token": "your-refresh-token",
"refresh_token_expires_at": "2024-11-16T10:15:30.000000Z",
"token_type": "Bearer"
}
- Accessing Protected Endpoints
Use the access token in the Authorization header for any protected routes.
Request
GET /api/user
Header
Authorization: Bearer your-access-token
Response
{
"data": {
"name": "John",
"email": "test@test.com",
"created_at": "2024-11-14T10:11:52.000000Z"
}
}
- Refreshing the Token
When the access token expires, use the refresh token to get a new set of tokens.
Request
POST /api/refresh
Header
Authorization: Bearer your-refresh-token
Response
{
"access_token": "new-access-token",
"access_token_expires_at": "2024-11-11T10:15:30.000000Z",
"refresh_token": "new-refresh-token",
"refresh_token_expires_at": "2024-11-17T10:15:30.000000Z",
"token_type": "Bearer"
}
- Logging Out
Send a POST request to /api/logout with the access token.
Request
POST /api/logout
Header
Authorization: Bearer your-access-token
Response
{
"message": "Logged out successfully"
}
Testing the Implementation
To generate a test file, use:
php artisan make:test AuthTest --unit
Add tests for login, refresh, and logout endpoints in tests/Unit/AuthTest.php:
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Models\User;
class AuthTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_login_and_receive_tokens()
{
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password')
]);
$response = $this->postJson('/api/login', [
'email' => 'test@example.com',
'password' => 'password'
]);
$response->assertStatus(200)
->assertJsonStructure([
'access_token',
'access_token_expires_at',
'refresh_token',
'refresh_token_expires_at',
'token_type',
]);
}
public function test_user_can_refresh_tokens()
{
$user = User::factory()->create();
$loginResponse = $this->postJson('/api/login', [
'email' => $user->email,
'password' => 'password'
]);
$refreshToken = $loginResponse->json('refresh_token');
$refreshResponse = $this->withHeader('Authorization', 'Bearer ' . $refreshToken)
->postJson('/api/refresh');
$refreshResponse->assertStatus(200)
->assertJsonStructure([
'access_token',
'access_token_expires_at',
'refresh_token',
'refresh_token_expires_at',
'token_type',
]);
}
public function test_user_can_logout()
{
$user = User::factory()->create();
$loginResponse = $this->postJson('/api/login', [
'email' => $user->email,
'password' => 'password'
]);
$accessToken = $loginResponse->json('access_token');
$logoutResponse = $this->withHeader('Authorization', 'Bearer ' . $accessToken)
->postJson('/api/logout');
$logoutResponse->assertStatus(200)
->assertJson(['message' => 'Logged out successfully']);
}
}
Run your tests with:
php artisan test
Conclusion
Implementing access and refresh tokens with Laravel Sanctum provides a secure and flexible approach to manage user authentication in your application. By using short-lived access tokens alongside longer-lived refresh tokens, you create a balance between security and user convenience, reducing the risk of unauthorized access while minimizing the need for frequent re-authentication.
In this guide, we covered how to create an AuthController to handle token issuance and renewal, how to protect routes using Sanctum's middleware, and how to test each part of the authentication flow. By leveraging Laravel's built-in capabilities, you can easily implement robust token-based authentication that enhances both the security and the user experience of your Laravel applications.
This approach is especially valuable for applications that require session management across multiple devices or need a straightforward way to secure APIs with minimal setup. With Sanctum, extending authentication with token-based access and refresh tokens is both powerful and intuitive.
Top comments (0)