Introduction
In modern web development, APIs are the backbone of mobile applications, single-page applications (SPAs), and third-party integrations. Securing these APIs with proper authentication is critical to protecting user data and preventing unauthorized access.
OAuth2 is the industry-standard protocol for API authentication, and Laravel Passport provides a full OAuth2 server implementation for Laravel applications. Whether you're building a mobile app backend, a microservices architecture, or exposing APIs to third-party developers, Passport simplifies token-based authentication while maintaining security.
In this comprehensive guide, we'll implement OAuth2 authentication in Laravel 12 using Passport, covering personal access tokens, password grant tokens, authorization codes, scopes, and production security practices.
By the end of this tutorial, you'll have a production-ready API authentication system that follows OAuth2 standards and industry best practices.
Why OAuth2 and Laravel Passport?
The OAuth2 Advantage
OAuth2 solves several authentication challenges:
- Token-Based Authentication – No sessions, perfect for stateless APIs
- Granular Permissions – Control access with scopes
- Third-Party Access – Allow external applications to access your API
- Multiple Clients – Support web, mobile, and desktop applications
- Refresh Tokens – Long-lived authentication without storing passwords
Why Choose Laravel Passport?
Laravel Passport offers:
- Full OAuth2 Server – Implements all OAuth2 grant types
- Simple Setup – Artisan commands handle migrations and keys
- Vue Components – Built-in UI for client management
- Personal Access Tokens – Simple tokens for testing and mobile apps
- Middleware Protection – Easy route authentication
- Token Scopes – Fine-grained permission control
- Laravel 12 Compatible – Fully tested with the latest Laravel version
Installation and Setup
Step 1: Install Laravel Passport
Install Passport via Composer:
composer require laravel/passport
Step 2: Run Migrations
Passport includes migrations for OAuth2 tables:
php artisan migrate
This creates tables for clients, tokens, and authorization codes.
Step 3: Install Passport
Run the installation command to generate encryption keys:
php artisan passport:install
This generates:
- Encryption keys for token signing
- Personal access client
- Password grant client
Important: Save the client IDs and secrets shown in the output. You'll need them for password grant authentication.
Step 4: Configure the User Model
Update your App\Models\User model to use the HasApiTokens trait:
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
Step 5: Configure Authentication Guard
Update config/auth.php to use the Passport token driver:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
Step 6: Register Passport Routes
In your App\Providers\AppServiceProvider, add Passport routes:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Passport\Passport;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Passport::enablePasswordGrant();
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
}
}
Personal Access Tokens
Personal access tokens are the simplest way to authenticate API requests, perfect for mobile apps and testing.
Creating Personal Access Tokens
Create a controller for token management:
php artisan make:controller Api/AuthController
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* Register a new user
*/
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$token = $user->createToken('auth_token')->accessToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'user' => $user,
], 201);
}
/**
* Login user and create token
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
// Create token with scopes
$token = $user->createToken('auth_token', ['read', 'write'])->accessToken;
return response()->json([
'access_token' => $token,
'token_type' => 'Bearer',
'user' => $user,
]);
}
/**
* Get authenticated user
*/
public function user(Request $request)
{
return response()->json($request->user());
}
/**
* Logout user (revoke token)
*/
public function logout(Request $request)
{
$request->user()->token()->revoke();
return response()->json([
'message' => 'Successfully logged out',
]);
}
}
API Routes for Personal Access Tokens
Add routes in routes/api.php:
use App\Http\Controllers\Api\AuthController;
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:api')->group(function () {
Route::get('/user', [AuthController::class, 'user']);
Route::post('/logout', [AuthController::class, 'logout']);
});
Testing Personal Access Tokens
# Register a new user
curl -X POST http://your-app.test/api/register \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"password": "password123",
"password_confirmation": "password123"
}'
# Login and get token
curl -X POST http://your-app.test/api/login \
-H "Content-Type: application/json" \
-d '{
"email": "john@example.com",
"password": "password123"
}'
# Use token to access protected route
curl -X GET http://your-app.test/api/user \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Password Grant Tokens
Password grant is ideal for first-party applications where you trust the client with user credentials.
Configure Password Grant
Add route for token exchange:
Route::post('/oauth/token', function (Request $request) {
$request->validate([
'grant_type' => 'required|in:password,refresh_token',
'client_id' => 'required',
'client_secret' => 'required',
'username' => 'required_if:grant_type,password',
'password' => 'required_if:grant_type,password',
'refresh_token' => 'required_if:grant_type,refresh_token',
'scope' => 'nullable',
]);
$http = new \GuzzleHttp\Client;
try {
$response = $http->post(config('app.url') . '/oauth/token', [
'form_params' => [
'grant_type' => $request->grant_type,
'client_id' => $request->client_id,
'client_secret' => $request->client_secret,
'username' => $request->username,
'password' => $request->password,
'refresh_token' => $request->refresh_token,
'scope' => $request->scope ?? '',
],
]);
return json_decode((string) $response->getBody(), true);
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
return response()->json([
'error' => 'invalid_credentials',
'message' => 'The provided credentials are incorrect.',
], 401);
}
});
Request Password Grant Token
curl -X POST http://your-app.test/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "password",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"username": "john@example.com",
"password": "password123",
"scope": "*"
}'
Refresh Token Flow
curl -X POST http://your-app.test/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "your-refresh-token",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"scope": "*"
}'
Token Scopes
Scopes allow fine-grained permission control for API access.
Define Scopes
In App\Providers\AppServiceProvider:
use Laravel\Passport\Passport;
public function boot(): void
{
Passport::tokensCan([
'read-posts' => 'Read posts',
'create-posts' => 'Create posts',
'update-posts' => 'Update posts',
'delete-posts' => 'Delete posts',
'manage-users' => 'Manage users',
]);
Passport::setDefaultScope([
'read-posts',
]);
}
Protect Routes with Scopes
Route::middleware(['auth:api', 'scope:read-posts'])->group(function () {
Route::get('/posts', [PostController::class, 'index']);
});
Route::middleware(['auth:api', 'scope:create-posts'])->group(function () {
Route::post('/posts', [PostController::class, 'store']);
});
Route::middleware(['auth:api', 'scopes:update-posts,delete-posts'])->group(function () {
Route::put('/posts/{post}', [PostController::class, 'update']);
Route::delete('/posts/{post}', [PostController::class, 'destroy']);
});
Check Scopes in Controllers
public function store(Request $request)
{
if (!$request->user()->tokenCan('create-posts')) {
return response()->json([
'error' => 'Insufficient permissions',
], 403);
}
// Create post logic
}
Authorization Code Grant
Authorization code grant is for third-party applications that need user authorization.
Create OAuth Client
php artisan passport:client
Select "Authorization Code Grant" and provide the redirect URI.
Authorization Request
Direct users to the authorization endpoint:
https://your-app.test/oauth/authorize?
client_id=your-client-id&
redirect_uri=https://client-app.test/callback&
response_type=code&
scope=read-posts create-posts&
state=random-state-string
Exchange Authorization Code for Token
curl -X POST http://your-app.test/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"redirect_uri": "https://client-app.test/callback",
"code": "authorization-code"
}'
Protecting API Routes
Basic Middleware Protection
Route::middleware('auth:api')->group(function () {
Route::apiResource('posts', PostController::class);
Route::apiResource('comments', CommentController::class);
});
Multiple Guard Protection
Route::middleware(['auth:api', 'throttle:60,1'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
Custom Middleware for Token Validation
php artisan make:middleware ValidateApiToken
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ValidateApiToken
{
public function handle(Request $request, Closure $next): Response
{
if (!$request->user() || !$request->user()->token()) {
return response()->json([
'error' => 'Unauthenticated',
], 401);
}
// Check if token is expired
if ($request->user()->token()->expires_at < now()) {
return response()->json([
'error' => 'Token expired',
], 401);
}
return $next($request);
}
}
Security Best Practices
1. Use HTTPS in Production
Always use HTTPS for API endpoints:
if (app()->environment('production')) {
URL::forceScheme('https');
}
2. Set Token Expiration
Configure appropriate token lifetimes:
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
3. Rate Limiting
Protect against brute force attacks:
Route::middleware(['auth:api', 'throttle:60,1'])->group(function () {
// 60 requests per minute
});
4. Token Revocation
Implement logout to revoke tokens:
public function logout(Request $request)
{
$request->user()->token()->revoke();
// Revoke all tokens
$request->user()->tokens()->delete();
return response()->json(['message' => 'Logged out']);
}
5. CORS Configuration
Configure CORS in config/cors.php:
return [
'paths' => ['api/*', 'oauth/*'],
'allowed_methods' => ['*'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:3000')],
'allowed_headers' => ['*'],
'supports_credentials' => true,
];
6. Encrypt Client Secrets
Store client secrets in environment variables:
PASSPORT_PASSWORD_CLIENT_ID=your-client-id
PASSPORT_PASSWORD_CLIENT_SECRET=your-client-secret
Testing API Authentication
Feature Test Example
namespace Tests\Feature;
use App\Models\User;
use Laravel\Passport\Passport;
use Tests\TestCase;
class ApiAuthenticationTest extends TestCase
{
public function test_user_can_access_protected_route()
{
$user = User::factory()->create();
Passport::actingAs($user, ['read-posts']);
$response = $this->getJson('/api/posts');
$response->assertStatus(200);
}
public function test_unauthenticated_user_cannot_access_protected_route()
{
$response = $this->getJson('/api/posts');
$response->assertStatus(401);
}
public function test_user_without_scope_cannot_create_post()
{
$user = User::factory()->create();
Passport::actingAs($user, ['read-posts']);
$response = $this->postJson('/api/posts', [
'title' => 'Test Post',
'content' => 'Test content',
]);
$response->assertStatus(403);
}
}
Conclusion
Implementing OAuth2 authentication in Laravel 12 with Passport provides enterprise-grade API security with minimal configuration. From personal access tokens for mobile apps to authorization code grants for third-party integrations, Passport covers all OAuth2 use cases while maintaining Laravel's elegant syntax.
Key takeaways:
- Install and configure Laravel Passport for OAuth2 authentication
- Use personal access tokens for first-party applications
- Implement password grant for trusted clients
- Protect API routes with scopes for granular permissions
- Follow security best practices for production deployments
- Test authentication flows thoroughly
Need help building secure APIs for your Laravel application? NeedLaravelSite specializes in developing scalable, secure Laravel APIs with proper authentication. Contact us for expert Laravel development services.
Related Resources: