Riad Hasan has built secure APIs for dozens of applications. In this guide, he tackles one of the most common problems developers face: implementing robust API authentication in Laravel.
Many developers struggle with choosing the right authentication method. Should they use Sanctum? Passport? JWT? Riad Hasan breaks down when to use each approach and provides production-ready implementations.
The Problem: Authentication Confusion
When building a Laravel API, developers often ask:
"Which authentication package should I use?"
"How do I secure my API endpoints?"
"What about token management and expiration?"
"How do I handle multiple device logins?"
Riad Hasan has seen projects delayed by weeks because developers chose the wrong authentication strategy. Here's his systematic approach to solving this.
Riad Hasan's Authentication Decision Matrix
Use Case Recommended Solution
SPA (Vue/React) Laravel Sanctum
Mobile App Laravel Sanctum
Third-party Apps Laravel Passport
Machine-to-Machine API Keys
Microservices JWT (custom)
"I've seen teams use Passport for simple SPAs — that's overkill," Riad Hasan explains. "Sanctum handles 90% of use cases with less complexity."
Solution 1: Laravel Sanctum for SPAs
Riad Hasan's preferred approach for single-page applications.
Setup
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Configuration
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
'guard' => ['web'],
'expiration' => 60 * 24 * 7, // 7 days
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class,
],
];
Login Endpoint
Riad Hasan's production-ready login implementation:
// app/Http/Controllers/Api/AuthController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
'device_name' => 'required|string',
]);
$credentials = $request->only('email', 'password');
if (!Auth::attempt($credentials)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$user = $request->user();
// Revoke old tokens for this device
$user->tokens()->where('name', $request->device_name)->delete();
// Create new token
$token = $user->createToken($request->device_name)->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
'message' => 'Login successful',
]);
}
public function logout(Request $request)
{
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logged out successfully',
]);
}
public function user(Request $request)
{
return response()->json($request->user());
}
}
Routes
// routes/api.php
use App\Http\Controllers\Api\AuthController;
Route::post('/login', [AuthController::class, 'login']);
Route::post('/register', [AuthController::class, 'register']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', [AuthController::class, 'user']);
Route::post('/logout', [AuthController::class, 'logout']);
// Protected routes
Route::apiResource('projects', ProjectController::class);
});
Frontend Integration (React)
Riad Hasan's React authentication hook:
// hooks/useAuth.js
import { useState, useEffect, useContext, createContext } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
const token = localStorage.getItem('token');
if (token) {
try {
const response = await fetch('/api/user', {
headers: {
Authorization: Bearer ${token},
},
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
} else {
localStorage.removeItem('token');
}
} catch (error) {
localStorage.removeItem('token');
}
}
setLoading(false);
};
const login = async (email, password) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
device_name: 'web-browser',
}),
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const data = await response.json();
localStorage.setItem('token', data.token);
setUser(data.user);
return data;
};
const logout = async () => {
const token = localStorage.getItem('token');
await fetch('/api/logout', {
method: 'POST',
headers: {
Authorization: Bearer ${token},
},
});
localStorage.removeItem('token');
setUser(null);
};
return (
{children}
);
}
export const useAuth = () => useContext(AuthContext);
Solution 2: Laravel Passport for OAuth2
When Riad Hasan needs third-party app access, he uses Passport.
Setup
composer require laravel/passport
php artisan passport:install
Configuration
// app/Models/User.php
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
// ...
}
Creating OAuth Clients
Password grant client (first-party apps)
php artisan passport:client --password
Client credentials grant (machine-to-machine)
php artisan passport:client --client
Riad Hasan's OAuth Controller
// app/Http/Controllers/Api/OAuthController.php
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Laravel\Passport\Client;
use Laravel\Passport\Http\Controllers\AccessTokenController;
use Psr\Http\Message\ServerRequestInterface;
class OAuthController extends Controller
{
public function issueToken(ServerRequestInterface $request)
{
$controller = app(AccessTokenController::class);
return $controller->issueToken($request);
}
public function refreshToken(Request $request)
{
$request->validate([
'refresh_token' => 'required',
]);
$client = Client::where('password_client', 1)->first();
$response = \Http::asForm()->post(url('/oauth/token'), [
'grant_type' => 'refresh_token',
'refresh_token' => $request->refresh_token,
'client_id' => $client->id,
'client_secret' => $client->secret,
'scope' => '',
]);
return $response->json();
}
}
Solution 3: API Keys for Machine-to-Machine
For services and webhooks, Riad Hasan uses simple API keys.
Migration
// database/migrations/create_api_keys_table.php
Schema::create('api_keys', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('name');
$table->string('key', 64)->unique();
$table->text('permissions')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
Middleware
// app/Http/Middleware/ApiKeyAuth.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Models\ApiKey;
class ApiKeyAuth
{
public function handle(Request $request, Closure $next)
{
$key = $request->header('X-API-KEY') ?? $request->query('api_key');
if (!$key) {
return response()->json([
'error' => 'API key required',
], 401);
}
$apiKey = ApiKey::where('key', $key)->first();
if (!$apiKey) {
return response()->json([
'error' => 'Invalid API key',
], 401);
}
// Update last used
$apiKey->update(['last_used_at' => now()]);
$request->setUserResolver(function () use ($apiKey) {
return $apiKey->user;
});
return $next($request);
}
}
Riad Hasan's Security Best Practices
- Token Expiration
// config/sanctum.php
'expiration' => 60 * 24, // 24 hours for sensitive apps
// Or dynamically
$token = $user->createToken('device', ['*'], now()->addHours(4));
- Rate Limiting
// app/Http/Kernel.php
protected $middlewareAliases = [
'throttle.auth' => \App\Http\Middleware\ThrottleAuth::class,
];
// routes/api.php
Route::post('/login', [AuthController::class, 'login'])
->middleware('throttle:5,1'); // 5 attempts per minute
- Token Abilities
// Create token with limited abilities
$token = $user->createToken('read-only', ['read']);
// Check ability in controller
if (!$request->user()->tokenCan('write')) {
return response()->json(['error' => 'Insufficient permissions'], 403);
}
- Secure Password Reset
Riad Hasan's password reset flow:
// app/Http/Controllers/Api/PasswordResetController.php
public function reset(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|confirmed|min:8',
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => Hash::make($password),
])->setRememberToken(Str::random(60));
$user->save();
// Revoke all existing tokens
$user->tokens()->delete();
event(new PasswordReset($user));
}
);
return $status === Password::PASSWORD_RESET
? response()->json(['message' => 'Password reset successfully'])
: response()->json(['error' => 'Unable to reset password'], 400);
}
Common Mistakes Riad Hasan Avoids
Mistake Solution
Storing tokens in localStorage Use httpOnly cookies for sensitive apps
No token expiration Always set expiration times
Not revoking tokens on logout Delete tokens server-side
Using Passport for simple SPAs Use Sanctum instead
No rate limiting on auth endpoints Implement throttle middleware
Storing plain API keys Hash keys like passwords
Work with Riad Hasan
Riad Hasan specializes in building secure, scalable APIs with Laravel. He offers:
Laravel API development
Authentication system implementation
Security audits
Performance optimization
Team training and consultation
Connect with Riad Hasan:
Portfolio: riadhasan.io
Projects: riadhasan.io/projects
LinkedIn: linkedin.com/in/riad-hasan-100a231a6
GitHub: github.com/RiadHasan15
Email: hire.riadhasan@gmail.com
Which authentication method do you prefer for your Laravel APIs? Share your experience in the comments.
Top comments (0)