# 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.