Overview
Laravel Breeze API + Next.js uses Laravel Sanctum for API authentication. Sanctum provides a lightweight authentication system for SPAs (Single Page Applications) using cookie-based authentication, which offers better security than traditional token-based approaches.
Laravel Sanctum Configuration
Stateful Domains
Sanctum is configured to treat specific domains as “stateful,” meaning they can authenticate using cookies:
Backend/config/sanctum.php
return [
'stateful' => explode ( ',' , env ( 'SANCTUM_STATEFUL_DOMAINS' , sprintf (
'%s%s%s' ,
'localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,127.0.0.1:8000,::1' ,
Sanctum :: currentApplicationUrlWithPort (),
env ( 'FRONTEND_URL' ) ? ',' . parse_url ( env ( 'FRONTEND_URL' ), PHP_URL_HOST ) : ''
))),
'guard' => [ 'web' ],
'expiration' => null ,
'middleware' => [
'authenticate_session' => Laravel\Sanctum\Http\Middleware\ AuthenticateSession :: class ,
'encrypt_cookies' => Illuminate\Cookie\Middleware\ EncryptCookies :: class ,
'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ ValidateCsrfToken :: class ,
],
];
The stateful configuration tells Sanctum which domains should receive session cookies for authentication.
Authentication Flow
SPA Authentication Process
Request CSRF Token
Before making any authentication request, the frontend must obtain a CSRF token: const csrf = () => axios . get ( '/sanctum/csrf-cookie' )
This endpoint sets a XSRF-TOKEN cookie that protects against CSRF attacks.
Submit Credentials
The user submits their credentials (email and password) to the backend: Frontend/src/hooks/auth.ts
const login = async ( data : {
email : string
password : string
remember : boolean
}) => {
try {
await csrf ()
await axios . post ( '/login' , data )
mutate ()
} catch ( error ) {
throw error
}
}
Backend Validates Credentials
Laravel validates the credentials and creates a session: Backend/app/Http/Controllers/Auth/AuthenticatedSessionController.php
public function store ( LoginRequest $request ) : Response
{
$request -> authenticate ();
$request -> session () -> regenerate ();
return response () -> noContent ();
}
Session Cookie Set
Laravel sets an encrypted session cookie that will be automatically included in subsequent requests.
Frontend Updates State
The frontend fetches the authenticated user data and updates the application state.
Authentication Hook
The useAuth hook provides a convenient interface for authentication operations:
Frontend/src/hooks/auth.ts
import useSWR from 'swr'
import axios from '@/lib/axios'
import { useEffect } from 'react'
import { useRouter , useParams } from 'next/navigation'
export const useAuth = ({
middleware ,
redirectIfAuthenticated ,
} : {
middleware ?: string
redirectIfAuthenticated ?: string
}) => {
const router = useRouter ()
const params = useParams ()
const {
data : user ,
error ,
mutate ,
} = useSWR ( '/api/user' , () =>
axios
. get ( '/api/user' )
. then ( res => res . data )
. catch ( error => {
if ( error . response . status !== 409 ) throw error
router . push ( '/verify-email' )
}),
)
const csrf = () => axios . get ( '/sanctum/csrf-cookie' )
const login = async ( data : {
email : string
password : string
remember : boolean
}) => {
try {
await csrf ()
await axios . post ( '/login' , data )
mutate ()
} catch ( error ) {
throw error
}
}
const logout = async () => {
if ( ! error ) {
await axios . post ( '/logout' ). then (() => mutate ())
}
window . location . pathname = '/login'
}
useEffect (() => {
if ( middleware === 'guest' && redirectIfAuthenticated && user ) {
router . push ( redirectIfAuthenticated )
}
if ( middleware === 'auth' && error ) logout ()
}, [ user , error , middleware , redirectIfAuthenticated ])
return {
user ,
register ,
login ,
forgotPassword ,
resetPassword ,
resendEmailVerification ,
logout ,
}
}
Hook Features
Automatic User Fetching Uses SWR to automatically fetch and cache user data
Middleware Protection Supports auth and guest middleware for route protection
Auto Redirect Automatically redirects based on authentication state
CSRF Handling Automatically handles CSRF token requests
Backend Authentication Routes
All authentication routes are defined in the backend:
Route :: post ( '/register' , [ RegisteredUserController :: class , 'store' ])
-> middleware ( 'guest' )
-> name ( 'register' );
Route :: post ( '/login' , [ AuthenticatedSessionController :: class , 'store' ])
-> middleware ( 'guest' )
-> name ( 'login' );
Route :: post ( '/forgot-password' , [ PasswordResetLinkController :: class , 'store' ])
-> middleware ( 'guest' )
-> name ( 'password.email' );
Route :: post ( '/reset-password' , [ NewPasswordController :: class , 'store' ])
-> middleware ( 'guest' )
-> name ( 'password.store' );
Route :: get ( '/verify-email/{id}/{hash}' , VerifyEmailController :: class )
-> middleware ([ 'auth' , 'signed' , 'throttle:6,1' ])
-> name ( 'verification.verify' );
Route :: post ( '/email/verification-notification' , [ EmailVerificationNotificationController :: class , 'store' ])
-> middleware ([ 'auth' , 'throttle:6,1' ])
-> name ( 'verification.send' );
Route :: post ( '/logout' , [ AuthenticatedSessionController :: class , 'destroy' ])
-> middleware ( 'auth' )
-> name ( 'logout' );
User Registration
The registration process creates a new user and automatically logs them in:
Frontend Implementation
Frontend/src/hooks/auth.ts
const register = async ( data : {
name : string
email : string
password : string
password_confirmation : string
}) => {
try {
await csrf ()
await axios . post ( '/register' , data )
mutate ()
} catch ( error ) {
throw error
}
}
Backend Implementation
Backend/app/Http/Controllers/Auth/RegisteredUserController.php
public function store ( Request $request ) : Response
{
$request -> validate ([
'name' => [ 'required' , 'string' , 'max:255' ],
'email' => [ 'required' , 'string' , 'lowercase' , 'email' , 'max:255' , 'unique:' . User :: class ],
'password' => [ 'required' , 'confirmed' , Rules\ Password :: defaults ()],
]);
$user = User :: create ([
'name' => $request -> name ,
'email' => $request -> email ,
'password' => Hash :: make ( $request -> string ( 'password' )),
]);
event ( new Registered ( $user ));
Auth :: login ( $user );
return response () -> noContent ();
}
Login with Rate Limiting
The login process includes built-in rate limiting to prevent brute force attacks:
Backend/app/Http/Requests/Auth/LoginRequest.php
public function authenticate () : void
{
$this -> ensureIsNotRateLimited ();
if ( ! Auth :: attempt ( $this -> only ( 'email' , 'password' ), $this -> boolean ( 'remember' ))) {
RateLimiter :: hit ( $this -> throttleKey ());
throw ValidationException :: withMessages ([
'email' => __ ( 'auth.failed' ),
]);
}
RateLimiter :: clear ( $this -> throttleKey ());
}
public function ensureIsNotRateLimited () : void
{
if ( ! RateLimiter :: tooManyAttempts ( $this -> throttleKey (), 5 )) {
return ;
}
event ( new Lockout ( $this ));
$seconds = RateLimiter :: availableIn ( $this -> throttleKey ());
throw ValidationException :: withMessages ([
'email' => trans ( 'auth.throttle' , [
'seconds' => $seconds ,
'minutes' => ceil ( $seconds / 60 ),
]),
]);
}
After 5 failed login attempts, the user will be temporarily locked out. This protects against brute force attacks.
Logout Process
Logout invalidates the session and clears authentication cookies:
Frontend
Frontend/src/hooks/auth.ts
const logout = async () => {
if ( ! error ) {
await axios . post ( '/logout' ). then (() => mutate ())
}
window . location . pathname = '/login'
}
Backend
Backend/app/Http/Controllers/Auth/AuthenticatedSessionController.php
public function destroy ( Request $request ) : Response
{
Auth :: guard ( 'web' ) -> logout ();
$request -> session () -> invalidate ();
$request -> session () -> regenerateToken ();
return response () -> noContent ();
}
Protected Routes
Protected API routes require authentication via Sanctum middleware:
Route :: middleware ([ 'auth:sanctum' ]) -> get ( '/user' , function ( Request $request ) {
return $request -> user ();
});
Frontend Route Protection
The frontend uses layout middleware to protect authenticated routes:
Frontend/src/app/(authenticated)/layout.tsx
'use client'
import { ReactNode } from 'react'
import { useAuth } from '@/hooks/auth'
import Navigation from '@/components/Layouts/Navigation'
const AppLayout = ({ children } : { children : ReactNode }) => {
const { user } = useAuth ({ middleware: 'auth' })
return (
< div className = "min-h-screen bg-gray-100" >
< Navigation user = { user } />
< main > { children } </ main >
</ div >
)
}
export default AppLayout
The middleware: 'auth' parameter automatically redirects unauthenticated users to the login page.
Cookie vs Token Authentication
Why Cookie-Based?
Laravel Sanctum uses cookie-based authentication for SPAs instead of traditional bearer tokens:
HTTP-only cookies cannot be accessed by JavaScript, protecting against XSS attacks.
Automatic CSRF Protection
Laravel’s CSRF protection works seamlessly with cookie-based authentication.
Cookies enable proper session management with session regeneration on login.
Browsers automatically handle cookie storage and transmission.
Cookie Configuration
Ensure the Axios client is configured to send cookies:
Frontend/src/lib/axios.ts
const axios : AxiosInstance = Axios . create ({
baseURL: process . env . NEXT_PUBLIC_BACKEND_URL ,
headers: {
'X-Requested-With' : 'XMLHttpRequest' ,
},
withCredentials: true , // Enable sending cookies
withXSRFToken: true , // Automatically send XSRF token
})
Password Reset Flow
The password reset flow involves multiple steps:
Request Reset Link
User submits email to receive a password reset link. const forgotPassword = async ( data : { email : string }) => {
await csrf ()
return await axios . post ( '/forgot-password' , data )
}
Receive Email
Laravel sends an email with a signed URL containing a reset token.
Submit New Password
User clicks the link and submits a new password. const resetPassword = async ( data : {
email : string
password : string
password_confirmation : string
}) => {
await csrf ()
const response = await axios . post ( '/reset-password' , {
... data ,
token: params . token ,
})
router . push ( '/login?reset=' + btoa ( response . data . status ))
}
Password Updated
Laravel validates the token and updates the password.
Email Verification
Email verification ensures users have access to their registered email:
Frontend/src/hooks/auth.ts
const resendEmailVerification = async () => {
try {
return await axios . post ( '/email/verification-notification' )
} catch ( error ) {
throw error
}
}
The useAuth hook automatically redirects unverified users:
useSWR ( '/api/user' , () =>
axios
. get ( '/api/user' )
. then ( res => res . data )
. catch ( error => {
if ( error . response . status !== 409 ) throw error
router . push ( '/verify-email' ) // Redirect to verification page
}),
)
Best Practices
Always Use HTTPS Use HTTPS in production to protect cookies and credentials in transit.
Configure CORS Properly Ensure allowed_origins and stateful domains match your frontend URL.
Handle Token Refresh Implement token refresh logic for long-lived sessions.
Secure Password Requirements Use Laravel’s password validation rules for strong passwords.
Troubleshooting
Verify CORS configuration in config/cors.php
Check SANCTUM_STATEFUL_DOMAINS environment variable
Ensure cookies are being sent with withCredentials: true
Call /sanctum/csrf-cookie before authentication requests
Verify withXSRFToken: true in Axios configuration
Check that cookies are enabled in the browser
Verify SESSION_DOMAIN matches your frontend domain
Check that SESSION_DRIVER is set to cookie or database
Ensure supports_credentials: true in CORS config
Next Steps
Architecture Overview Understand the full-stack architecture
Project Structure Explore the codebase organization