Laravel 12 with Vue.js 3 Authentication Complete Guide Step By Step

In this Laravel 12 tutorial titled “Laravel 12 with Vue.js 3 Authentication Complete guide Step by Step”, you will learn how to build a modern Single Page Application (SPA) authentication system using Laravel and Vue.js.

This guide demonstrates how to integrate Laravel Sanctum for secure authentication while using Vue.js 3 to build a dynamic frontend interface.

Technologies Used

  • Laravel 12 as the backend framework for building RESTful APIs
  • Laravel Sanctum for secure session-based authentication
  • Vue.js 3 as the frontend framework for the SPA
  • Axios for handling HTTP requests between the frontend and backend
  • Bootstrap for responsive and clean user interface components
  • Session-based authentication, providing a simple and secure authentication mechanism for SPA applications

Features Implemented

Throughout this tutorial, you will build the following features:

  • User Registration
  • User Login
  • Authentication using Laravel Sanctum
  • Retrieve the authenticated user profile
  • Logout functionality
  • By the end of this tutorial, you will have a fully functional Laravel 12 + Vue.js 3 authentication system with Sanctum and a practical foundation for building secure SPA applications.

Why Choose Laravel Sanctum?

Laravel Sanctum is a lightweight authentication system designed to make API and SPA authentication simple and secure. It supports two primary authentication approaches:

  • SPA Authentication (Session & Cookies) – Ideal for Single Page Applications such as Vue.js or React. This is the approach we will implement in this tutorial.
  • API Token Authentication – Commonly used for mobile applications or external services that interact with your API.

Why SPA Authentication Works Well with Vue

For Vue-based Single Page Applications, Sanctum’s session-based authentication offers several practical advantages:

  • No need to manually manage tokens on the frontend.
  • Secure cookie-based authentication, which helps protect against common security risks.
  • Quick and straightforward setup compared to more complex authentication systems.
  • Recommended by the Laravel team for SPA applications.

Simply put: Laravel Sanctum keeps authentication clean, secure, and easy to maintain—allowing developers to focus more on building application features rather than dealing with complicated authentication logic.

Prerequisites

Before starting this tutorial, make sure the following tools and setup are already installed on your system:

  • PHP (8.2 or higher)
  • Composer
  • Node.js and NPM
  • Laravel 12 project already installed
  • Vue.js 3 installed in the project

If you have not completed the basic setup yet, you may find the following tutorials helpful:

Step 1: Install Laravel Sanctum

In Laravel 12, installing Sanctum is very simple using the built-in Artisan command:

php artisan install:api

This command automatically performs several tasks for you:

  • Installs the Laravel Sanctum package
  • Creates the api.php routes file
  • Publishes the Sanctum configuration
  • Generates the personal_access_tokens migration table

After running the command, execute the migrations to create the required database tables:

php artisan migrate

Once the migration is completed, Sanctum will be properly installed and ready to use for API authentication.

Read Also : Laravel 12 Reset Password Using OTP Sanctum API Example

Step 2: Configure Laravel Sanctum (Important)

After installing Sanctum, the next step is configuring it correctly so your Vue.js SPA can authenticate using cookies and sessions.

Open the Sanctum configuration file: config/sanctum.php

Make sure the stateful domains include your application domain:

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),

This tells Sanctum which frontend domains are allowed to use session-based authentication.

Update .env Configuration

Update Environment Configuration

SANCTUM_STATEFUL_DOMAINS=127.0.0.1:8000
SESSION_DRIVER=cookie
SESSION_DOMAIN=127.0.0.1

Important: If Sanctum is not configured correctly, login may appear successful but every authenticated request will return a 401 Unauthorized response.

For production environments, it is recommended to define SANCTUM_STATEFUL_DOMAINS as a comma-separated list of your allowed domains.

Example:

SANCTUM_STATEFUL_DOMAINS=yourdomain.com,www.yourdomain.com

Step 3: Enable Sanctum Middleware (Laravel 12)

Finally, ensure Sanctum middleware is enabled so your SPA can authenticate using cookies.

Open bootstrap/app.php, Add the following middleware if it is not already present:

use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
  $middleware->append([
            EnsureFrontendRequestsAreStateful::class,
        ]);

It Look Like this

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->append([
            EnsureFrontendRequestsAreStateful::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

This middleware ensures that requests coming from your frontend application are treated as stateful, allowing Vue.js to authenticate using Laravel sessions and cookies.

Step 4: Create Authentication Controller

Run the command:

php artisan make:controller AuthController

Now open the controller:

<?php
 
namespace App\Http\Controllers;

use Illuminate\Database\QueryException;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use App\Models\User;

class AuthController extends Controller
{
    /**
     * Register api
     *
     * @return \Illuminate\Http\Response
     */
    public function register(Request $request)
    {
        $request->validate([
            'name' => 'required',
            'email' => 'required|email',
            'password' => 'required|string|min:8',
            'password_confirmation' => 'required|same:password',
        ]);

        try{
 
        $data = $request->all();
        $data['password'] = bcrypt($data['password']);
        $user = User::create($data);
 
        Auth::login($user);
    
        return response()->json([
        'user' => $user
        ], 200);
 
     
      
        } catch (QueryException  $e) {
              return response()->json([
                'status' => false,
                'message' => $e->getMessage(),
            ],500);
        }
    }
    
    /**
     * Login api
     *
     * @return \Illuminate\Http\Response
     */
    public function login(Request $request)
    {
        $request->validate([
            'email' => 'required|email',
            'password' => 'required|string',
        ]);

        if(Auth::attempt(['email' => $request->email, 'password' => $request->password])){ 
            $user = Auth::user(); 
           
            return response()->json([
                'user' => $user
            ], 200);
             
        } 
        else{ 
             return response()->json(['message'=>'Invalid credentials.'],401);
        } 
    }
 
 
      /**
     * User Profile API
     */
    public function profile(Request $request)
    {
         return response()->json([
                'message' => 'User data fetched successfully.',
                'data' => $request->user()
            ], 200);
 
       
    }
 
    /**
     * Logout API
     */
    public function logout(Request $request)
    {
         Auth::guard('web')->logout();
 
         return response()->json([
                'message' => 'User logged out successfully.',
            ], 200);
         
    }
}

Step 5: Create API Routes

Open routes/api.php

<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController;

// Public routes
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
 
// Protected routes
Route::middleware('auth:sanctum')->group(function () {
    Route::get('profile', [AuthController::class, 'profile']);
    Route::post('logout', [AuthController::class, 'logout']);
});

In this Write protected routes inside auth:sanctum

Step 6: Configure Vue for Authentication

To allow Vue.js to communicate properly with the Laravel backend, Axios must be configured correctly. This ensures that authentication cookies and requests work with Laravel Sanctum.

Open the following file: resources/js/bootstrap.js, Then import Axios and configure it as shown below:

import 'bootstrap';

import axios from 'axios';
window.axios = axios;

axios.defaults.withCredentials = true;
axios.defaults.baseURL = 'http://127.0.0.1:8000';

Explanation:

  • baseURL defines the Laravel backend URL that Axios will use for API requests.
  • withCredentials allows the browser to send authentication cookies with each request, which is required for Sanctum’s session-based authentication.

Before sending any login or registration request, the application must first obtain a CSRF cookie from Laravel.

await axios.get('/sanctum/csrf-cookie');

This step is required because Laravel Sanctum uses CSRF protection to secure authentication requests. Without retrieving this cookie, login or registration requests will fail.

In short, always request the CSRF cookie first, then send your authentication request.

Step 7: Create Vue Components

Create the component resources/js/components/auth/Auth.vue

<template>

<div class="row justify-content-center mt-3">
<div class="col-md-6"  v-if="is_auth">


    <ul class="nav nav-tabs nav-justified mb-3"  >

        <li class="nav-item">
            <button
                class="nav-link"
                :class="{active: activeTab==='registerForm'}"
                @click="activeTab='registerForm'"
            >
                Register
            </button>
        </li>

        <li class="nav-item">
            <button
                class="nav-link btn-info"
                :class="{active: activeTab==='loginForm'}"
                @click="activeTab='loginForm'"
            >
                Login
            </button>
        </li>

    </ul>


    <div class="card p-4 shadow-sm" >
       <p v-if="auth_error" class="text-danger">
                 {{ auth_error }}
            </p>
        <!-- REGISTER -->
        <template v-if="activeTab==='registerForm'">
           
            <h4 class="mb-3">Register</h4>

             <p v-if="register_errors?.message" class="text-danger">
            {{ register_errors.message }}
            </p>


            <div class="mb-3">
                <label class="form-label">Name</label>
                 <input type="text" name="name" class="form-control" v-model="register.name" :class=" register_errors?.name ? 'is-invalid': '' "/>
                <span v-if="register_errors?.name" class="invalid-feedback">{{ register_errors.name[0] }}  </span>
            </div>

            <div class="mb-3">
                <label class="form-label">Email</label>
                <input type="text" class="form-control" name="email" v-model="register.email" :class=" register_errors?.email ? 'is-invalid': ''"/>
           
               <span v-if="register_errors?.email" class="invalid-feedback">{{ register_errors.email[0] }}  </span>
            </div>

            <div class="mb-3">
                <label class="form-label">Password</label>
                <input type="password" name="password" class="form-control" v-model="register.password" :class=" register_errors?.password ? 'is-invalid': ''" />
               <span v-if="register_errors?.password" class="invalid-feedback">{{ register_errors.password[0] }}  </span>
            </div>
             <div class="mb-3">
                <label class="form-label">Confirm Password</label>
                <input type="password"  name="password_confirmation" class="form-control" v-model="register.password_confirmation"  :class="register_errors?.password_confirmation ? 'is-invalid' : ''"    />
                
                <span v-if="register_errors?.password_confirmation" class="invalid-feedback">{{ register_errors.password_confirmation[0] }}  </span>
                
            </div>

            <button class="btn btn-primary w-100" @click="registerUser" :disabled="is_register">
                Register Now!
                 <span v-if="is_register">
                <i class="fa fa-spinner fa-spin"></i>
                </span>
            </button>

        </template>


        <!-- LOGIN -->
        <template v-if="activeTab==='loginForm'" >

            <h4 class="mb-3">Login</h4>  

             
         
          
            <div class="mb-3">
                <label class="form-label">Email</label>
                <input type="text" class="form-control" name="email" v-model="login.email" :class=" login_errors?.email ? 'is-invalid': ''"/>           
               <span v-if="login_errors?.email" class="invalid-feedback">{{ login_errors.email[0] }}  </span>
            </div>

            <div class="mb-3">
                <label class="form-label">Password</label>
                <input type="password" name="password" class="form-control" v-model="login.password" :class=" login_errors?.password ? 'is-invalid': ''" />
               <span v-if="login_errors?.password" class="invalid-feedback">{{ login_errors.password[0] }}  </span>
            </div>

            <button class="btn btn-success w-100" @click="loginUser" :disabled="is_login">
                Login
                <span v-if="is_login">
                <i class="fa fa-spinner fa-spin"></i>
                </span>
            </button>

        </template>

    </div>
    </div>
    <div class="col-md-6"  v-if="!is_auth">
      
            <a class="btn btn-sm btn-info" href="javascript:void(0)" @click="logout">Logout</a>
        
    <h1> Welcome {{profile.name}}!</h1>

</div>
</div>

</template>


<script setup>
import { ref,onMounted  } from 'vue'


const activeTab = ref('registerForm')
const register = ref({name:"",email:"",password:"",password_confirmation:""})
const login = ref({email:"",password:""})
const register_errors = ref({})
const login_errors = ref({})
const is_register = ref(false)
const is_login = ref(false)
const is_auth  = ref(true)
const profile  = ref({})
const auth_error  = ref("")


//Methods
 const registerUser = async () => {
    
         is_register.value = true
         await axios.get('/sanctum/csrf-cookie');
         await axios.post(`/api/register`, register.value).then(({data})=>{
                getUser()
                
            }).catch((error)=>{         
                if (error.response?.status === 422) {
                     auth_error.value = ""
                    Object.keys(register_errors).forEach(key => delete register_errors[key])
                    Object.assign(register_errors, error.response.data.errors)
                } else {    
                     Object.keys(register_errors).forEach(key => delete register_errors[key])               
                    auth_error.value = error.response?.data?.message
                }      
            }).finally(()=>{
                 is_register.value = false  // STOP LOADING 
                 //reset(register,register_errors)
            })
        
      
    
  
 }

 const loginUser = async() =>{

         is_login.value = true
         await axios.get('/sanctum/csrf-cookie');
         await axios.post(`/api/login`, login.value).then(({data})=>{
                getUser()
                
                
            }).catch((error)=>{  
                if (error.response?.status === 422) {
                    auth_error.value  = ""
                    Object.keys(login_errors).forEach(key => delete login_errors[key])
                    Object.assign(login_errors, error.response.data.errors)
                } else {
                     Object.keys(login_errors).forEach(key => delete login_errors[key])
                    auth_error.value = error.response?.data?.message
                }              
                                
            }).finally(()=>{
                 is_login.value = false  // STOP LOADING 
                 //reset(login,login_errors)
            })
        
   
    
 }

const getUser = async () => {
           
    await axios.get(`/api/profile`).then(({data})=>{
        // user logged in
        is_auth.value = false 
        profile.value = data.data
        localStorage.setItem("User", JSON.stringify(data.data));
        
        
    })
   
}

const logout = async () => {
    await axios.post('api/logout/').then(({data})=>{
        
        is_auth.value = true
        profile.value = {}
        localStorage.removeItem("User");
        reset()
    }).catch((error)=>{        
        console.log(error.response)
    })
}

const reset = () =>{
  auth_error.value = ""
  Object.keys(login_errors).forEach(key => delete login_errors[key])
  Object.keys(register_errors).forEach(key => delete register_errors[key])
   login.value = {
    email: "",
    password: ""
    }

    register.value = {
    name: "",
    email: "",
    password: "",
    password_confirmation: ""
    }

}

const checklogin =() =>{
   let user = localStorage.getItem("User");
   
   if(user){
    profile.value = JSON.parse(user)
    is_auth.value = false
   }
}

onMounted(() => {
  checklogin()
})
</script>

Vue Authentication UI Explanation

Template Section

  • Uses Bootstrap layout to center the authentication card.
  • v-if=”is_auth” : shows Register/Login forms if user is not authenticated.
  • v-if=”!is_auth” : shows welcome message and logout button after login.

Tabs Navigation

  • Two tabs: Register and Login.
  • activeTab controls which form is displayed.
  • Clicking tab buttons switches between forms.

Register Form

  • Inputs for Name, Email, Password, Confirm Password.
  • Uses v-model to bind input data.
  • Displays validation errors from API.
  • Register button calls registerUser() method.
  • Shows loading spinner when request is processing.

Login Form

  • Inputs for Email and Password.
  • Uses v-model for data binding.
  • Shows validation errors if login fails.
  • Login button calls loginUser() method.

Logged-in View

  • Shows Welcome message with user name.
  • Provides Logout button.

Vue Authentication Methods

registerUser()

  • Requests Sanctum CSRF cookie.
  • Sends registration request to /api/register.
  • If successful calls getUser().
  • Handles validation errors.

loginUser()

  • Gets CSRF cookie.
  • Sends login request to /api/login.
  • If successful fetches user profile.

getUser()

  • Calls /api/profile.
  • Stores user data.
  • Updates UI to logged-in state.
  • Saves user in localStorage.

logout()

  • Sends request to /api/logout.
  • Clears user data.
  • Removes user from localStorage.
  • Resets form state.

reset()

  • Clears form fields.
  • Removes validation errors.

checklogin()

  • Checks if user exists in localStorage.
  • If found automatically logs user in.

onMounted()

  • Runs checklogin() when component loads.

Step 7: Update app.js

Open resources/js/app.js and import the components

import './bootstrap';
import { createApp } from 'vue';



const app = createApp({});


import Auth from './components/auth/Auth.vue';

app.component('Auth', Auth);

app.mount('#app');

Step 8: Update Welcome Blade

<!DOCTYPE html>
<html>
<head>
    <title>Laravel 12 with Vue.js 3 Authentication Complete Guide Step By Step</title>     
     @vite(['resources/css/app.css', 'resources/js/app.js'])
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
</head>
<body>
    <div class="container mt-5">
        <div class="card">
            <h5 class="card-header bg-primary text-white">Laravel 12 with Vue.js 3 Authentication Complete Guide Step By Step - ItStuffSolutiotions</h5>
          
            <div id="app">
               
                <Auth />
            </div>
        </div>
    </div>
  
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>

   
</body>

</html>

Explanation

  • CDN links (Bootstrap and Font Awesome) are used to quickly add styling and icons without installing them locally.
  • The id=”app” div is the mounting point for Vue.js, where Vue renders components.
  • The <Auth /> is a Vue component that gets displayed inside the app div.
  • @vite loads the compiled Laravel + Vue CSS and JavaScript files.

Step 9: Run The Application

We have successfully completed all the steps. Now it’s time to run the Laravel application and test the authentication system.

First, start the Laravel development server by running the following command:

php artisan serve

This will start the Laravel server at: http://127.0.0.1:8000

Next, start the Vite development server for compiling Vue.js assets.

Run the following command:

npm run dev

After both servers are running, open your browser and visit: http://127.0.0.1:8000

You can now test the Login/Register, Profile, Logout implemented in this tutorial. After login, the application automatically calls the /api/profile endpoint to fetch the logged-in user information.

Vue Js Register Form Preview
Vue Js Register Form Preview
Laravel 12 with Vue.js 3 Authentication Complete Guide Step By Step
Vue Js Login Form Preview

Click the Logout button to destroy the session and return to the login/register screen.

If everything is configured correctly, you now have a fully functional Laravel 12 + Vue.js 3 authentication system using Laravel Sanctum.

Common Mistakes

When implementing Laravel Sanctum authentication with Vue.js, developers often face some common issues. Below are the most frequent mistakes and their solutions.

401 Unauthorized Error After Login

If login seems successful but protected routes return 401 Unauthorized, it usually means Sanctum is not properly configured.

Make sure your .env file configure correctly.

Also confirm that the Sanctum middleware is enabled in bootstrap/app.php.

Sanctum requires a CSRF cookie before sending login or register requests.

Always call this before authentication requests:

await axios.get('/sanctum/csrf-cookie');

If this step is skipped, authentication requests will fail.

Axios Not Sending Cookies

If authentication does not persist, check your Axios configuration.

Make sure this line exists in bootstrap.js:

axios.defaults.withCredentials = true;

This allows cookies to be sent with requests.

Protected Routes Missing Middleware

Your protected API routes must use the auth:sanctum middleware.

Without this middleware, authentication will not work correctly.

Wrong API Base URL

If requests fail, verify the Axios base URL.

Make sure it matches the Laravel server URL.

Conclusion

In this tutorial, we built a complete authentication system using Laravel 12, Vue.js 3, and Laravel Sanctum.

We covered the entire process step-by-step, including:

  • Installing Laravel Sanctum
  • Configuring Sanctum for SPA authentication
  • Creating authentication APIs
  • Protecting routes with Sanctum middleware
  • Configuring Axios for cookie-based authentication
  • Building Vue.js components for login and registration
  • Fetching authenticated user profile
  • Implementing logout functionality

Laravel Sanctum provides a clean and secure authentication system for Single Page Applications, making it an excellent choice for projects built with Vue.js or React.

FAQs

Why use Sanctum instead of JWT?

Sanctum is simpler to implement for SPA applications because it uses Laravel sessions and cookies instead of manually managing tokens.

Do I need Laravel Breeze or Jetstream for this setup?

No. In this tutorial, we built a custom authentication system using Sanctum and Vue.js without Breeze or Jetstream.