DEV Community

Cover image for Laravel - Access and Refresh Tokens in Sanctum
Darius Dauskurdis
Darius Dauskurdis

Posted on

Laravel - Access and Refresh Tokens in Sanctum

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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']);
Enter fullscreen mode Exit fullscreen mode

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...
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Request Examples and Responses

Here’s how to use each endpoint with request and response examples.

  • Login

Request

POST /api/login
Enter fullscreen mode Exit fullscreen mode
{
    "email": "test@example.com",
    "password": "password"
}
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • Accessing Protected Endpoints

Use the access token in the Authorization header for any protected routes.
Request

GET /api/user
Enter fullscreen mode Exit fullscreen mode

Header

Authorization: Bearer your-access-token
Enter fullscreen mode Exit fullscreen mode

Response

{
    "data": {
        "name": "John",
        "email": "test@test.com",
        "created_at": "2024-11-14T10:11:52.000000Z"
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Refreshing the Token

When the access token expires, use the refresh token to get a new set of tokens.

Request

POST /api/refresh
Enter fullscreen mode Exit fullscreen mode

Header

Authorization: Bearer your-refresh-token
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode
  • Logging Out

Send a POST request to /api/logout with the access token.

Request

POST /api/logout
Enter fullscreen mode Exit fullscreen mode

Header

Authorization: Bearer your-access-token
Enter fullscreen mode Exit fullscreen mode

Response

{
    "message": "Logged out successfully"
}
Enter fullscreen mode Exit fullscreen mode

Testing the Implementation

To generate a test file, use:

php artisan make:test AuthTest --unit
Enter fullscreen mode Exit fullscreen mode

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']);
    }
}
Enter fullscreen mode Exit fullscreen mode

Run your tests with:

php artisan test
Enter fullscreen mode Exit fullscreen mode

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)