DEV Community

ABDALLAH ATGUIRI
ABDALLAH ATGUIRI

Posted on

๐Ÿช Laravel Auth with Secure Cookies and Refresh Tokens โ€” SPA-Ready, No LocalStorage

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

Update config/auth.php:

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” 2. AuthController with Access + Refresh Cookies

php artisan make:controller AuthController
Enter fullscreen mode Exit fullscreen mode
// 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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ง 3. Middleware: UseAccessTokenFromCookie

php artisan make:middleware UseAccessTokenFromCookie
Enter fullscreen mode Exit fullscreen mode
// 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Register in Kernel.php:

protected $middlewareGroups = [
    'api' => [
        \App\Http\Middleware\UseAccessTokenFromCookie::class,
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];
Enter fullscreen mode Exit fullscreen mode

๐Ÿšง 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()]);
});
Enter fullscreen mode Exit fullscreen mode

โš›๏ธ 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;
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”บ 6. Dev Proxy (Vite)

// vite.config.js

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

๐ŸŒ 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/;
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”’ 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 custom UseAccessTokenFromCookie
  • Flexible for any SPA framework

Top comments (0)