Authentication in a Laravel + Vue + Inertia application is still a server-side concern.
Vue renders the interface. Inertia carries page visits and form submissions. Laravel owns routes, controllers, sessions, guards, authorization, and CSRF protection.
That division is the main pattern to preserve.
You are building a modern interface on top of Laravel’s web stack. You are not building a separate token-driven frontend by default. Unlike an application that needs to build a REST API with PHP, an Inertia application usually needs no token plumbing.
Start with the session guard
Laravel’s default web guard uses sessions and cookies. After a successful login, Laravel stores the authenticated user in the session. The browser sends the session cookie with later requests.
Inertia does not replace this mechanism. It uses the same authentication services as a traditional Laravel application.
Keep your Inertia routes in routes/web.php. They should use the web middleware group, which provides sessions and CSRF verification.
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::middleware('auth')->group(function () {
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->name('dashboard');
});
The auth middleware checks the configured default guard. You can be explicit when your application has multiple guards.
Route::middleware('auth:web')->group(function () {
// Session-authenticated Inertia routes...
});
A common mistake is applying auth:api to an Inertia page. That tells Laravel to look for API credentials. Your browser session may then be ignored.
The result is often a confusing 401 response. Check the route file, middleware group, and guard before changing frontend code.
The Laravel authentication documentation covers guard configuration in more detail.
Let the Vue starter kit establish the flow
Laravel’s Vue starter kit provides a useful authentication foundation. It includes Inertia pages, form handling, login routes, registration, password resets, and logout.
The backend uses Laravel Fortify for the authentication logic. The Vue layer renders the forms and submits them through Inertia.
Review the Laravel starter kits documentation before replacing this flow. The generated code belongs to your application. You can adjust it without treating the starter kit as a black box.
A typical login flow looks like this:
- A guest visits the login page.
- Vue submits credentials with an Inertia
POST. - Laravel validates the request.
- The session guard authenticates the user.
- Laravel redirects to the intended page.
- Inertia updates the current page without a full browser reload.
The important part is step four. The authenticated state lives in Laravel’s session, not in a Vue store.

Share the user through HandleInertiaRequests
Vue needs to know whether a user is authenticated. It may also need the user’s name, avatar, role, or selected permissions.
The standard place to share this data is HandleInertiaRequests. Its share() method runs for Inertia requests and provides props to every page.
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => fn () => $request->user()?->only([
'id',
'name',
'email',
]),
],
]);
}
}
This produces a shared prop at auth.user. The closure keeps the user lookup lazy. It also returns null for guests.
Keep shared user data deliberately small. Do not expose password-related fields, internal flags, or sensitive relationships.
You can also call Inertia::share() from a service provider for genuinely global values.
use Inertia\Inertia;
Inertia::share('appName', config('app.name'));
For authentication, the middleware remains the better choice. It has direct access to the current request and its authenticated user.
Laravel’s authorization and Inertia guidance also shows how to share permission data alongside the user.
Gate the interface without trusting it
The frontend can use shared props to avoid displaying controls that the user cannot use. This improves clarity and prevents unnecessary actions.
It does not provide security.
The server must repeat every important authorization check. A hidden button is not an access control.
<script setup lang="ts">
import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3'
type User = {
id: number
name: string
}
const page = usePage<{
auth: {
user: User | null
permissions?: {
reports?: {
view: boolean
}
}
}
}>()
const user = computed(() => page.props.auth.user)
const canViewReports = computed(() =>
Boolean(user.value && page.props.auth.permissions?.reports?.view)
)
</script>
<template>
<nav v-if="user">
<a v-if="canViewReports" href="/reports">
Reports
</a>
</nav>
</template>
For more complex applications, share permission values rather than entire policy objects. For example, expose reports.view or teams.manage.
These props help Vue render the right navigation. They do not decide whether /reports is accessible.
Protect routes and actions separately
Route middleware answers an authentication question.
Is this request associated with a logged-in user?
Policies and gates answer an authorization question.
May this authenticated user perform this action?
Use auth to protect a page. Use a policy or gate to protect a resource.
use App\Models\Post;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::middleware('auth')->group(function () {
Route::get('/posts/{post}/edit', function (Post $post) {
return Inertia::render('Posts/Edit', [
'post' => $post,
]);
})->can('update', 'post');
});
The can middleware invokes the relevant policy. Laravel returns 403 when the user is authenticated but lacks permission.
You can also authorize inside a controller.
use Illuminate\Support\Facades\Gate;
public function update(Request $request, Post $post)
{
Gate::authorize('update', $post);
$post->update($request->validated());
return to_route('posts.show', $post);
}
The server-side check remains authoritative, even when the frontend hides the edit button.

Return clean 401 and 403 responses
401 and 403 describe different states.
-
401means the request is not authenticated. -
403means the user is authenticated but not allowed. -
419usually means the CSRF token is missing or invalid.
For normal browser routes, Laravel’s auth middleware commonly redirects guests to /login. That 302 response is usually the best experience for an Inertia page.
Use a direct 401 response when the client needs an authentication failure. This is more common for API endpoints or explicit session-expiry handling.
Use 403 for policy failures. Do not redirect an authenticated user to the login page when they lack permission.
A dedicated Inertia error component keeps these responses consistent. In Laravel’s exception configuration, render the component while preserving the original status code.
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Http\Request;
use Inertia\Inertia;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->respond(function ($response) {
$status = $response->getStatusCode();
if (in_array($status, [401, 403, 404, 500, 503])) {
return Inertia::render('Errors/Http', [
'status' => $status,
])->toResponse(request())->setStatusCode($status);
}
return $response;
});
})
Keep the exact exception hook aligned with your Laravel and Inertia versions. The Laravel error handling documentation covers the available rendering APIs.
Your Vue component can map statuses to short, useful messages.
<script setup lang="ts">
import { Link } from '@inertiajs/vue3'
defineProps<{
status: number
}>()
const messages: Record<number, string> = {
401: 'Please sign in to continue.',
403: 'You do not have permission to view this page.',
404: 'The page could not be found.',
}
</script>
<template>
<main>
<h1>{{ status }}</h1>
<p>{{ messages[status] ?? 'Something went wrong.' }}</p>
<Link href="/">Return home</Link>
</main>
</template>
In production, avoid exposing exception details. Keep Laravel’s detailed debug output for local development only.
Log out with POST
Logout changes server-side state. It should not be a GET request.
The starter kit normally provides a named logout route using POST. Submit it through Inertia.
<script setup lang="ts">
import { router } from '@inertiajs/vue3'
function logout() {
router.post('/logout')
}
</script>
<template>
<button type="button" @click="logout">
Log out
</button>
</template>
A successful logout should invalidate the session and regenerate the session token. The next response should redirect to a guest-friendly page.
Do not implement logout by deleting a token from local storage. That pattern belongs to a different authentication architecture.
Keep CSRF boring
Session authentication depends on CSRF protection. Keep Laravel’s CSRF middleware enabled for your web routes.
Inertia form submissions should use the application’s configured client and same-origin web routes. The browser receives the relevant CSRF cookie, and the client sends the token with state-changing requests.
When using a separate SPA domain with Sanctum, configuration becomes more involved. You may need stateful domains, credentialed CORS, shared cookie settings, and Axios options such as withCredentials and withXSRFToken.
The Sanctum documentation explains that its SPA mode still uses cookie-based sessions. Its API token mode is a separate concern.
A 419 response usually points to CSRF or session configuration. It does not mean the user lacks authorization.
Check these areas first:
- The route uses
webmiddleware. - The session cookie reaches the browser.
- The request uses the correct application domain.
- CORS allows credentials for separate subdomains.
- The client sends the XSRF header when required.
- Session and application domains match your environment.

Common pitfalls worth avoiding
Using API tokens for an Inertia interface
Do not add Bearer tokens because Vue feels like a separate application. Inertia keeps Laravel’s routing and session model in place.
Choose tokens for mobile clients, third-party consumers, or a genuine API boundary.
Sharing too much user data
The authenticated user is available globally. That does not mean every column belongs in every page response.
Return only what the interface needs.
Treating frontend checks as authorization
A v-if improves the interface. It cannot protect a route or database record.
Use policies, gates, and can middleware on the server.
Redirecting every failure to login
Guests may need a login redirect. Authenticated users need a clear 403.
These are different user experiences and different security states.
Making logout a GET
State-changing actions belong behind POST, PUT, PATCH, or DELETE. Logout is no exception.
The clean pattern is straightforward: Laravel owns identity and permission, Inertia transports the request, and Vue renders the result.
That separation gives a PHP web framework the structure it is known for without sacrificing a responsive frontend. You get sessions instead of token plumbing, policies instead of scattered checks, and error pages that respect the actual HTTP status.
Authentication stays simple when each layer keeps its job.