Struggling with messy auth, CORS issues, and token storage on the frontend? Letโs fix that with a clean, secure solution using Laravel, JWTs, and HTTP-only cookiesโplus refresh tokens for long sessions and a custom middleware to integrate smoothly with Laravelโs auth:api
.
โ Highlights
- Auth with
access_token
andrefresh_token
as secure, HTTP-only cookies - Custom middleware to populate
Authorization
header from cookie - Supports Laravelโs native
auth:api
middleware - Works with React, Vue, or any SPA framework
- Prevents XSS and CSRF, no CORS headache
๐งฑ 1. Laravel Setup
composer create-project laravel/laravel cookie-auth
cd cookie-auth
composer require tymon/jwt-auth
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret
Update config/auth.php
:
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
๐ 2. AuthController with Access + Refresh Cookies
php artisan make:controller AuthController
// app/Http/Controllers/AuthController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Tymon\JWTAuth\JWTAuth;
use Tymon\JWTAuth\Facades\JWTFactory;
class AuthController extends Controller
{
protected $jwt;
public function __construct(JWTAuth $jwt)
{
$this->jwt = $jwt;
}
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
if (!$accessToken = auth('api')->attempt($credentials)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$refreshPayload = JWTFactory::sub(auth()->id())->make([
'exp' => now()->addDays(7)->timestamp,
]);
$refreshToken = $this->jwt->encode($refreshPayload);
return response()->json(['success' => true])
->cookie('access_token', $accessToken, 60, '/', null, true, true, false, 'Strict')
->cookie('refresh_token', $refreshToken, 10080, '/', null, true, true, false, 'Strict');
}
public function logout()
{
return response()->json(['success' => true])
->cookie('access_token', '', -1)
->cookie('refresh_token', '', -1);
}
public function refresh(Request $request)
{
$refreshToken = $request->cookie('refresh_token');
if (!$refreshToken) {
return response()->json(['message' => 'No refresh token'], 401);
}
try {
$payload = $this->jwt->setToken($refreshToken)->getPayload();
$user = auth()->getProvider()->retrieveById($payload['sub']);
if (!$user) {
return response()->json(['message' => 'Invalid user'], 401);
}
$accessToken = auth('api')->login($user);
return response()->json(['refreshed' => true])
->cookie('access_token', $accessToken, 60, '/', null, true, true, false, 'Strict');
} catch (\Exception $e) {
return response()->json(['message' => 'Invalid refresh token'], 401);
}
}
}
๐ง 3. Middleware: UseAccessTokenFromCookie
php artisan make:middleware UseAccessTokenFromCookie
// app/Http/Middleware/UseAccessTokenFromCookie.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Log;
class UseAccessTokenFromCookie
{
public function handle($request, Closure $next)
{
$token = $request->cookie('access_token');
if ($token) {
$request->headers->set('Authorization', "Bearer $token");
} else {
Log::info('No Token Found');
}
return $next($request);
}
}
Register in Kernel.php
:
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\UseAccessTokenFromCookie::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
๐ง 4. Routes
use App\Http\Controllers\AuthController;
Route::post('/login', [AuthController::class, 'login']);
Route::post('/logout', [AuthController::class, 'logout']);
Route::post('/refresh', [AuthController::class, 'refresh']);
Route::middleware('auth:api')->get('/me', function () {
return response()->json(['user' => auth()->user()]);
});
โ๏ธ 5. Frontend (React Example)
import { useEffect, useState } from 'react';
function App() {
const [user, setUser] = useState(null);
const getUser = async () => {
let res = await fetch('/api/me', {
credentials: 'include',
});
if (res.status === 401) {
const refresh = await fetch('/api/refresh', {
method: 'POST',
credentials: 'include',
});
if (refresh.ok) {
res = await fetch('/api/me', {
credentials: 'include',
});
}
}
const data = await res.json();
setUser(data.user || null);
};
useEffect(() => {
getUser();
}, []);
return (
<div>
<h1>{user ? `Welcome ${user.email}` : 'Not logged in'}</h1>
</div>
);
}
export default App;
๐บ 6. Dev Proxy (Vite)
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
๐ 7. Nginx for Production
server {
listen 443 ssl;
server_name yourapp.com;
location / {
proxy_pass http://frontend:5173;
}
location /api/ {
proxy_pass http://backend:8000/;
}
}
๐ Security Checklist
โ Feature | Description |
---|---|
HTTP-only cookies | Prevents JavaScript access |
SameSite=strict |
Blocks CSRF from other domains |
secure: true |
Enforces HTTPS only |
Middleware auto-adds Bearer | Smooth integration with auth:api
|
Refresh token rotation ready | Prevents session expiration issues |
๐ง Summary
- Secure, persistent auth using cookies
- Access token for quick auth, refresh token for long sessions
- Frontend never stores tokens
- Works with Laravel
auth:api
middleware using your customUseAccessTokenFromCookie
- Flexible for any SPA framework
Top comments (0)