# Lecture 07 — Laravel Sanctum Cookie Authentication with Next.js **Duration:** ~3 hours **Audience:** Beginners to Intermediate **Backend:** Laravel 12 + Sanctum **Frontend:** Next.js (JavaScript, App Router) --- ## 🎯 Objectives By the end of this lecture trainees will: 1. Understand what cookies are and why **HttpOnly cookies** are more secure. 2. Learn how to configure Laravel Sanctum to return a token in a cookie. 3. Build and register a custom Laravel middleware (**CookieFilter**) that moves token from cookie → Authorization header. 4. Set middleware alias and priority in Laravel 12’s `bootstrap/app.php`. 5. Re‑implement logout to also remove the cookie. 6. Learn artisan commands for clearing cache and checking routes. 7. Use **Axios** with `withCredentials` in Next.js for secure cookie-based requests. 8. Implement login and experts page in Next.js using Axios instance. 9. Test cookies via browser DevTools and understand HttpOnly. --- ## Section 1 — Cookies and HttpOnly - **Cookie:** Small piece of text stored in the browser, sent with every request to the same domain. - **HttpOnly:** Cannot be read by JavaScript (`document.cookie` won’t show it). Only sent automatically in HTTP requests. - Why secure? Prevents XSS from stealing your token. --- ## Section 2 — Laravel Login Returns Sanctum Token as Cookie ### AuthController@login ```php public function login(Request $request) { $request->validate([ 'email' => 'required|email', 'password' => 'required|string', ]); $user = User::where('email', $request->email)->first(); if (!$user || !Hash::check($request->password, $user->password)) { return response()->json(['success' => false, 'message' => 'Invalid credentials'], 401); } $token = $user->createToken('experts_token')->plainTextToken; $cookie = cookie( 'access_token', $token, 60, // minutes '/', null, false, // secure: true in production true, // HttpOnly false, 'lax' ); return response()->json(['success' => true, 'message' => 'Logged in'])->cookie($cookie); } ``` ### CORS Config `config/cors.php` ```php return [ 'paths' => ['api/*', 'auth/*'], 'allowed_methods' => ['*'], 'allowed_origins' => ['http://localhost:3000'], 'allowed_headers' => ['*'], 'supports_credentials' => true, ]; ``` --- ## Section 3 — CookieFilter Middleware ### Generate ```bash php artisan make:middleware CookieFilter ``` ### Code — `app/Http/Middleware/CookieFilter.php` ```php class CookieFilter { public function handle(Request $request, Closure $next) { if (!$request->headers->has('Authorization')) { $token = $request->cookie('access_token'); if ($token) { $request->headers->set('Authorization', 'Bearer '.$token); } } return $next($request); } } ``` ### Register Alias & Priority — `bootstrap/app.php` ```php return Application::configure(basePath: dirname(__DIR__)) ->withMiddleware(function (Middleware $middleware) { $middleware->alias([ 'cookie.filter' => \App\Http\Middleware\CookieFilter::class, ]); $middleware->priority([ \App\Http\Middleware\CookieFilter::class, \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, ]); }) ->create(); ``` ### Routes `routes/api.php` ```php Route::middleware(['cookie.filter', 'auth:sanctum'])->group(function () { Route::get('/users/me', [UserController::class, 'me']); Route::get('/users', [UserController::class, 'index']); Route::get('/experts', [ExpertController::class, 'index']); Route::post('/bookings', [BookingController::class, 'store']); Route::get('/bookings/me', [BookingController::class, 'myBookings']); }); Route::post('/auth/login', [AuthController::class, 'login']); Route::post('/auth/logout', [AuthController::class, 'logout']); ``` --- ## Section 4 — Logout with Cookie Clearing ```php public function logout(Request $request) { $request->user()?->tokens()?->delete(); return response()->json(['success' => true, 'message' => 'Logged out']) ->withoutCookie('access_token', '/', null, false, true, false, 'lax'); } ``` --- ## Section 5 — Useful Artisan Commands ```bash php artisan route:list php artisan route:clear php artisan config:clear php artisan cache:clear php artisan optimize:clear ``` --- ## Section 6 — Frontend with Axios ### Axios instance — `lib/my-axios.js` ```js import axios from "axios"; const myAxios = axios.create({ baseURL: "http://localhost:8000/api", timeout: 10000, headers: { "Content-Type": "application/json", Accept: "application/json" }, withCredentials: true, }); export default myAxios; ``` **Axios vs fetch** - Axios auto-parses JSON, includes timeout, interceptors. - fetch needs manual `res.json()` and `res.ok` checks. --- ## Section 7 — Next.js Login Page ### `app/login/page.jsx` ```jsx "use client"; import { useState } from "react"; import { useRouter } from "next/navigation"; import myAxios from "@/lib/my-axios"; export default function LoginPage() { const router = useRouter(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [error, setError] = useState(""); const handleLogin = async (e) => { e.preventDefault(); setError(""); try { const res = await myAxios.post("/auth/login", { email: username, password, }); console.log("Login response", res.data); router.push("/experts"); } catch (err) { console.error("Login error", err); setError(err?.response?.data?.message || "Login failed"); } }; return ( <div> <h1>Login</h1> <form onSubmit={handleLogin}> <input type="email" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="Email" /><br /> <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" /><br /> <button type="submit">Login</button> </form> {error && <p style={{ color: "red" }}>{error}</p>} </div> ); } ``` --- ## Section 8 — Experts Page (Client-side Guard) ### `app/experts/page.jsx` ```jsx "use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import myAxios from "@/lib/my-axios"; export default function ExpertsPage() { const router = useRouter(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); useEffect(() => { const load = async () => { try { await myAxios.get("/users/me"); // 401 if not logged in const res = await myAxios.get("/users"); setUsers(res.data.data || res.data); } catch (e) { if (e?.response?.status === 401) { router.push("/login"); return; } setError(e?.response?.data?.message || "Failed to load"); } finally { setLoading(false); } }; load(); }, [router]); if (loading) return <p>Loading...</p>; if (error) return <p style={{ color: "red" }}>{error}</p>; return ( <div> <h1>Experts</h1> <ul> {users.map((u) => ( <li key={u.id}>{u.name} — {u.email}</li> ))} </ul> </div> ); } ``` --- ## Section 9 — Bookings Example ### `CreateBookingForm` ```jsx "use client"; import { useState } from "react"; import myAxios from "@/lib/my-axios"; export default function CreateBookingForm() { const [expertId, setExpertId] = useState(""); const [hours, setHours] = useState(1); const [scheduledAt, setScheduledAt] = useState(""); const [msg, setMsg] = useState(""); const submit = async (e) => { e.preventDefault(); setMsg(""); try { await myAxios.post("/bookings", { expert_id: expertId, hours, scheduled_at: scheduledAt }); setMsg("Booking created!"); } catch (e) { setMsg("Failed to create booking"); } }; return ( <form onSubmit={submit}> <input placeholder="Expert ID" value={expertId} onChange={(e) => setExpertId(e.target.value)} /><br /> <input type="number" value={hours} onChange={(e) => setHours(e.target.value)} /><br /> <input placeholder="YYYY-MM-DD HH:mm" value={scheduledAt} onChange={(e) => setScheduledAt(e.target.value)} /><br /> <button type="submit">Book</button> {msg && <p>{msg}</p>} </form> ); } ``` --- ## Section 10 — Recap - **Laravel:** Login returns Sanctum token in cookie, `CookieFilter` copies cookie → Authorization header. - **Next.js:** Axios with `withCredentials: true` allows browser to include cookie in cross-origin requests. - **Experts Page:** Client checks `/users/me` → if 401 redirect to login. - **Logout:** Deletes cookie + tokens. --- ## Next Lecture (08) - Add Tailwind CSS v4 for UI styling. - Styled forms, tables, and error states. - Show how to protect UX with loaders and nice error messages.