Authentication in an Inertia.js application is ordinary Laravel session authentication. There are no tokens, no OAuth handshake and no API guard for the web client. The React login form posts to a Laravel route, Laravel validates the credentials, regenerates the session and redirects to the dashboard. This guide shows both paths: the fast one with the Laravel React starter kit, and the manual one for existing applications.
Key takeaway: The short version — use Laravel's session guard, protect routes with the
authmiddleware, share the authenticated user throughHandleInertiaRequests, and authorize actions with policies. Never rely on hiding a button in React as access control.
How authentication works in Inertia
In a classic API + SPA setup the browser stores a token and sends it with every request. In Inertia the browser keeps a session cookie, exactly like a Blade application. The flow is:
- The React login form submits a POST request to
/loginwith the email, password and CSRF token. - Laravel validates the credentials with
Auth::attempt(). - Laravel regenerates the session ID and redirects to the intended URL.
- The next Inertia visit includes the session cookie;
HandleInertiaRequestsshares the authenticated user as a prop. - React reads
usePage().props.auth.userand renders the authenticated layout.
Because Inertia uses the web middleware group, CSRF protection, session encryption and cookie settings work without extra configuration.
- ## Option A: Laravel React starter kit (recommended)
The fastest path is the official React starter kit, which ships with Inertia, authentication and a modern UI.
laravel new my-app --react
cd my-app
composer install
npm install
npm run dev
The starter kit includes
- login, registration and logout;
- password reset and email verification;
- profile management;
- a persistent authenticated layout;
- TypeScript types for shared props.
Use it as the baseline and replace the UI with your brand. Do not rebuild authentication from scratch unless you have a specific requirement the starter kit does not cover.
- ## Option B: manual authentication in an existing app
If you already have a Laravel application, add authentication without replacing the frontend.
1. Create the auth controller
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
class AuthenticatedSessionController extends Controller
{
public function create()
{
return Inertia::render('Auth/Login');
}
public function store(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
$request->session()->regenerate();
return redirect()->intended(route('dashboard'));
}
public function destroy(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
2. Register the routes
use App\Http\Controllers\Auth\AuthenticatedSessionController;
Route::middleware('guest')->group(function () {
Route::get('/login', [AuthenticatedSessionController::class, 'create'])->name('login');
Route::post('/login', [AuthenticatedSessionController::class, 'store']);
});
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
->middleware('auth')
->name('logout');
3. Create the React login page
import { useForm } from '@inertiajs/react';
export default function Login() {
const { data, setData, post, processing, errors, reset } = useForm({
email: '',
password: '',
remember: false,
});
function submit(event) {
event.preventDefault();
post('/login', {
onFinish: () => reset('password'),
});
}
return (
<form onSubmit={submit}>
<input
type="email"
value={data.email}
onChange={(e) => setData('email', e.target.value)}
autoComplete="username"
/>
{errors.email && <p>{errors.email}</p>}
<input
type="password"
value={data.password}
onChange={(e) => setData('password', e.target.value)}
autoComplete="current-password"
/>
{errors.password && <p>{errors.password}</p>}
<label>
<input
type="checkbox"
checked={data.remember}
onChange={(e) => setData('remember', e.target.checked)}
/>
Remember me
</label>
<button disabled={processing}>Log in</button>
</form>
);
}
4. Share the authenticated user
app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user()?->only('id', 'name', 'email'),
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
];
}
5. Protect routes and render the layout
Route::middleware('auth')->group(function () {
Route::get('/dashboard', fn () => Inertia::render('Dashboard'))->name('dashboard');
Route::resource('orders', OrderController::class);
});
export default function AuthenticatedLayout({ children }) {
const { auth } = usePage().props;
return (
<div>
<header>
<span>{auth.user.name}</span>
<Link href="/logout" method="post" as="button">Log out</Link>
</header>
<main>{children}</main>
</div>
);
}
Registration, password reset and email verification
Registration follows the same pattern: validate the input, create the user, log them in and redirect.
public function store(Request $request)
{
$data = $request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create($data);
Auth::login($user);
return redirect()->route('dashboard');
}
For password reset and email verification, use Laravel's built-in notifications and the starter kit's controllers. The React pages are forms that post to the Laravel routes; there is no special Inertia integration required.
Tip: Keep the password reset flow on the server. Do not build a custom token system in React. Laravel's signed URLs, throttling and notifications are already audited and tested.
Authorization
Authentication answers "who are you"; authorization answers "what can you do". In Inertia, authorization must happen on the server.
Policies
public function update(UpdateOrderRequest $request, Order $order)
{
$this->authorize('update', $order);
$order->update($request->validated());
return redirect()->route('orders.show', $order);
}
Shared abilities
Expose only the abilities the UI needs
'auth' => [
'user' => $request->user()?->only('id', 'name', 'email'),
'can' => [
'create_orders' => $request->user()?->can('create', Order::class),
'manage_billing' => $request->user()?->can('manageBilling', Team::class),
],
],
Use them to hide actions, but keep the server-side check. A hidden button is a UX improvement, not a security boundary.
Middleware
Use can middleware for route-level checks
Route::put('/orders/{order}', [OrderController::class, 'update'])
->middleware('can:update,order');
Sanctum or session?
| Scenario | Recommended auth |
|---|---|
| Same-domain Inertia web app | Laravel session |
| Mobile app | Sanctum API tokens |
| Third-party API clients | Sanctum tokens or OAuth |
| Multiple frontends on different domains | Sanctum SPA authentication with CORS |
Inertia uses the session guard. Sanctum is for the moment you extract an API for a second client. Do not add Sanctum to an Inertia-only app "just in case" — it adds configuration and attack surface without a consumer.
Common mistakes
- Storing the user in localStorage. The session cookie is enough; localStorage is vulnerable to XSS.
- Checking permissions only in React. Always authorize on the server.
- Forgetting
session()->regenerate()after login. It prevents session fixation. - Not invalidating the session on logout. Call
invalidate()andregenerateToken(). - Using the API guard for the web client. Inertia expects the
webguard and session middleware. - Sharing too much user data. Share only the fields the UI needs.
- Skipping email verification on sensitive routes. Use the
verifiedmiddleware where it matters. - No rate limiting on login. Add
throttleto slow brute-force attempts.
FAQ
Does Inertia.js need Sanctum?
No. Inertia uses Laravel's session authentication. Sanctum is only needed when you add a mobile app or a third-party API.
How do I get the authenticated user in React?
Share it in HandleInertiaRequests and read usePage().props.auth.user.
How do I protect a route?
Use the auth middleware in Laravel. The React side never decides whether a route is protected.
How do I handle 401 responses?
Inertia visits to protected routes are redirected to the login page by Laravel's auth middleware. For standalone requests, handle 401 in useHttp or fetch.
Can I use OAuth or social login?
Yes. Use Laravel Socialite on the server and redirect back to an Inertia page. The frontend only renders the provider buttons.
How do I implement "remember me"?
Pass the boolean to Auth::attempt($credentials, $remember). Laravel sets the remember cookie.
How do I log out?
Post to a Laravel route that calls Auth::logout(), invalidates the session and regenerates the CSRF token, then redirects.
Sources
- Laravel authentication documentation
- Laravel authorization documentation
- Laravel Sanctum documentation
- Inertia.js authentication documentation
- Laravel React starter kit
Building an authenticated SaaS on Laravel and React? Check our Laravel development services or hire a Laravel developer.
Read more: Inertia.js with Laravel and React, Sanctum SPA vs Inertia session auth, Inertia.js forms and validation and testing Inertia.js applications.