A Laravel + Vue + Inertia application usually does not need JWT authentication.
Inertia keeps your frontend and backend on the same domain. Your Laravel controllers still handle routing, authorization, validation, and responses. Vue provides the interface. Inertia connects both sides without turning your application into a separate API client.
That architecture fits Laravel’s built-in session authentication.
A browser logs in through Laravel. Laravel stores the authenticated user in the session and sends a session cookie. Later requests include that cookie automatically. Middleware restores the user before your controller runs.
This is the idiomatic approach for an Inertia SPA. JWT belongs in a different architecture, such as a stateless API consumed by mobile clients or independent frontends.
Session authentication matches Inertia’s design
Inertia does not require a special authentication protocol. It uses the authentication system provided by your server-side framework.
For Laravel, that usually means the web guard, sessions, and cookies. The browser sends normal requests to Laravel. Laravel identifies the session, resolves the user, and returns an Inertia response.
The flow looks like this:
- A user submits the login form.
- Laravel validates the credentials.
- Laravel regenerates the session.
- Laravel stores the authenticated user in the session.
- The browser receives the session cookie.
- Future Inertia requests authenticate through that cookie.
Laravel’s Authentication documentation describes this browser-based flow in detail.
The result is simple. You do not need to create, refresh, store, or revoke access tokens for your first-party web interface.
Session auth versus JWT
JWT can be useful when an application needs stateless authentication. An independent Vue application, native mobile client, or third-party integration may request a token from a Laravel API and send it with each request.
That is different from an Inertia application.
| Concern | Laravel + Inertia | API-only application |
|---|---|---|
| Authentication | Server-side session | Token, often JWT or Sanctum |
| Browser state | Session cookie | Client-managed token |
| Routes | routes/web.php |
routes/api.php |
| Responses | Inertia pages and redirects | JSON |
| CSRF | Required for cookie-authenticated requests | Usually replaced by token validation |
| Frontend relationship | Laravel and Vue share an application | Frontend and API are separate systems |
JWT is not automatically more secure. It introduces token storage, expiry, refresh flows, revocation decisions, and client-side failure handling.
Use it when your system needs those properties. Do not add it simply because your frontend uses Vue.
If your real requirement is to build a REST API with PHP, evaluate Laravel Sanctum or Passport separately. Laravel’s authentication ecosystem guidance explains when those tools fit.
Start with Laravel’s authentication scaffolding
Laravel’s official starter kits provide a strong starting point for authentication. The Vue starter kit combines Laravel, Inertia, Vue, TypeScript, and Tailwind.
It includes the application code for common flows:
- Login and logout
- Registration
- Password resets
- Email verification
- Password confirmation
- Two-factor authentication
- Login rate limiting
The starter kits use Laravel Fortify for the underlying authentication actions.
Create a new application with the Laravel installer:
composer global require laravel/installer
laravel new account-portal
The installer lets you select the Vue starter kit. Then install frontend dependencies and start the application:
cd account-portal
npm install && npm run build
composer run dev
The Laravel starter kit documentation covers the available Vue stack and its customization points.
You still own the generated code. Read it. Change it. Remove features that your application does not need.
If you build your own login flow, follow the same principles. Validate credentials on the server and regenerate the session after a successful login:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
public function authenticate(Request $request)
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (! Auth::attempt($credentials)) {
return back()->withErrors([
'email' => 'The provided credentials do not match our records.',
]);
}
$request->session()->regenerate();
return redirect()->intended(route('dashboard'));
}
The session regeneration step helps prevent session fixation. Laravel’s built-in authentication services handle the surrounding details for you.
Share the authenticated user through Inertia props
Your server knows who is logged in. Your Vue layout also needs that information.
Inertia’s shared data mechanism lets you expose small, global pieces of data to every page. In Laravel, the usual location is HandleInertiaRequests.
Keep the shared user payload narrow. Do not send password fields, tokens, internal flags, or unrelated private data.
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user()
? $request->user()->only([
'id',
'name',
'email',
])
: null,
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
];
}
}
The Inertia shared data documentation recommends namespacing shared data. It also warns that shared data appears in every response, so keep it focused.

In Vue, read the shared props with usePage:
<script setup lang="ts">
import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3'
const page = usePage()
const user = computed(() => page.props.auth.user)
</script>
<template>
<header v-if="user">
Signed in as {{ user.name }}
</header>
</template>
Use this data for presentation. Display the current user. Show a navigation item. Render an account menu.
Do not treat a hidden button as security. The server must still authorize every protected action.
Protect routes with middleware
Authentication and authorization solve different problems.
Authentication asks, “Who is this user?”
Authorization asks, “May this user perform this action?”
Start with the auth middleware:
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::middleware(['auth'])->group(function () {
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->name('dashboard');
});
Unauthenticated visitors are redirected to the login route. Add verified when the application requires verified email addresses:
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/billing', fn () => Inertia::render('Billing'))
->name('billing');
});
Sensitive areas can also use password.confirm. This is useful for billing settings, security changes, and account deletion.
Middleware protects the route boundary. You must also protect the action itself.
Authorize actions with policies and gates
Laravel provides gates and policies for authorization logic.
Policies work well when an action concerns a model. Suppose users can edit their own posts:
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
Your controller can authorize before changing data:
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
public function update(Request $request, Post $post)
{
Gate::authorize('update', $post);
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'body' => ['required', 'string'],
]);
$post->update($validated);
return to_route('posts.show', $post)
->with('success', 'Post updated.');
}
You can also enforce the policy at the route level:
Route::put('/posts/{post}', [PostController::class, 'update'])
->middleware('auth')
->can('update', 'post');
This creates a clear boundary. The user must be authenticated, and the user must be allowed to update that specific post.

Add role-based access without trusting the frontend
Roles are an authorization concern, not an authentication mechanism.
A simple application might have a role column on the users table:
$table->string('role')->default('member');
You can define a gate for an administrator dashboard:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::define('view-admin', function (User $user): bool {
return $user->role === 'admin';
});
Then protect the route:
Route::get('/admin', AdminDashboardController::class)
->middleware(['auth', 'can:view-admin']);
For resource-specific rules, use policies. For larger permission systems, consider a dedicated permissions package or a domain-level authorization service.
You may share permission hints with Vue so the interface can render appropriate controls:
'auth' => [
'user' => $request->user()?->only('id', 'name', 'email'),
'permissions' => [
'posts' => [
'create' => $request->user()?->can('create', \App\Models\Post::class),
],
],
],
This improves the user experience. It does not replace the server-side check.
A user can modify JavaScript, replay a request, or call an endpoint without using your interface. Every controller action, route, job, and sensitive operation needs its own authorization boundary.
Keep CSRF protection enabled
Session authentication relies on cookies. Browsers send cookies automatically. That convenience creates a CSRF risk.
Laravel protects state-changing requests through the web middleware group. This includes POST, PUT, PATCH, and DELETE requests.
The request must include a valid CSRF token or pass Laravel’s origin checks. Laravel documents both mechanisms in its CSRF protection guide.

When you use a Laravel starter kit, the frontend setup handles the normal same-origin request flow. If you configure your own HTTP client, make sure it sends credentials and the expected XSRF header.
With Axios, Laravel’s XSRF-TOKEN cookie can be read automatically for same-origin requests. With fetch, use credentials explicitly:
await fetch('/profile', {
method: 'PUT',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
},
body: JSON.stringify(payload),
})
Do not disable CSRF protection for Inertia routes. Exclude only routes that genuinely cannot receive a browser session, such as verified payment webhooks.
Logout should invalidate the session and regenerate the CSRF token:
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return to_route('home');
A clean authentication checklist
For a Laravel + Vue + Inertia application:
- Use Laravel’s session guard for the web interface.
- Start with the official Vue starter kit when it fits.
- Share a small
auth.userprop throughHandleInertiaRequests. - Use
auth,verified, andpassword.confirmmiddleware where needed. - Use policies and gates for model and role-based authorization.
- Share permission hints for UI decisions, never for security decisions.
- Keep CSRF protection enabled on state-changing web requests.
- Choose Sanctum, Passport, or JWT for separate API requirements.
- Use Laravel’s PHP developer tools to inspect routes, middleware, policies, and tests.
The cleanest stack is usually the one that matches the application you are building. For an Inertia SPA, session-based authentication keeps Laravel in control and lets Vue focus on the interface.
If you are combining an Inertia web application with a public API, keep the boundaries explicit. Use sessions for the first-party browser experience and choose a token strategy for external clients. We’d love to hear how you structure authentication in your Laravel applications.