Google Integration & API Services

Google Login Integration with Laravel 12 & Vue.js 3 – Secure OAuth Authentication Guide

E ebcoderbaseAugust 2, 20266 min read
Google Login Integration with Laravel 12 & Vue.js 3 – Secure OAuth Authentication Guide

Full tutorial · Laravel 12 · Vue 3 · Sanctum · Socialite

Google Login with Laravel 12 + Vue 3

A decoupled SPA setup: Vue 3 renders Google's own sign-in button, Laravel verifies the identity token with Socialite, and Sanctum issues the API token that keeps the session alive.

Architecture overview

1. Vue 3 sends the user to Google's OAuth consent screen.
2. Google redirects back with an auth code, or issues an ID token directly.
3. Laravel exchanges it for the user's Google profile, via Socialite.
4. Laravel creates or logs in the user and returns a Sanctum token.
5. Vue stores the token and treats the user as authenticated.

Backend — Laravel

Step 01

Create the Laravel 12 project

composer create-project laravel/laravel google-login-backend
cd google-login-backend

Install Socialite and Sanctum:

composer require laravel/socialite
composer require laravel/sanctum
php artisan install:api

install:api in Laravel 12 sets up Sanctum, publishes config, and adds the api.php routes file if it's missing.

Step 02

Configure Google OAuth credentials

Go to Google Cloud Console → APIs & Services → Credentials → Create OAuth Client ID (Web application).

Authorized redirect URI:
http://localhost:8000/auth/google/callback

.env
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_REDIRECT_URI=http://localhost:8000/auth/google/callback
 
FRONTEND_URL=http://localhost:5173
SESSION_DOMAIN=localhost
SANCTUM_STATEFUL_DOMAINS=localhost:5173
config/services.php
'google' => [
    'client_id' => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    'redirect' => env('GOOGLE_REDIRECT_URI'),
],

Step 03

Update the user migration

Add a google_id column and make password nullable, since social users won't have one.

php artisan make:migration add_google_id_to_users_table --table=users
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('google_id')->nullable()->after('id');
        $table->string('avatar')->nullable();
        $table->string('password')->nullable()->change();
    });
}
php artisan migrate

Step 04

Create the auth controller

php artisan make:controller Auth/GoogleAuthController
app/Http/Controllers/Auth/GoogleAuthController.php
<?php
 
namespace App\Http\Controllers\Auth;
 
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
 
class GoogleAuthController extends Controller
{
    // Step 1: Redirect to Google
    public function redirect()
    {
        return Socialite::driver('google')
            ->stateless()
            ->redirect();
    }
 
    // Step 2: Handle Google callback
    public function callback(Request $request)
    {
        try {
            $googleUser = Socialite::driver('google')->stateless()->user();
        } catch (\Exception $e) {
            return redirect(env('FRONTEND_URL') . '/login?error=google_auth_failed');
        }
 
        $user = User::updateOrCreate(
            ['email' => $googleUser->getEmail()],
            [
                'name' => $googleUser->getName(),
                'google_id' => $googleUser->getId(),
                'avatar' => $googleUser->getAvatar(),
                'password' => Hash::make(Str::random(24)),
                'email_verified_at' => now(),
            ]
        );
 
        $token = $user->createToken('google-auth-token')->plainTextToken;
 
        return redirect(env('FRONTEND_URL') . '/auth/callback?token=' . $token);
    }
 
    // Optional: accept an ID token from a frontend Google button
    public function loginWithIdToken(Request $request)
    {
        $request->validate(['id_token' => 'required|string']);
 
        $googleUser = Socialite::driver('google')
            ->stateless()
            ->userFromToken($request->id_token);
 
        $user = User::updateOrCreate(
            ['email' => $googleUser->getEmail()],
            [
                'name' => $googleUser->getName(),
                'google_id' => $googleUser->getId(),
                'avatar' => $googleUser->getAvatar(),
                'password' => Hash::make(Str::random(24)),
                'email_verified_at' => now(),
            ]
        );
 
        return response()->json([
            'token' => $user->createToken('google-auth-token')->plainTextToken,
            'user' => $user,
        ]);
    }
 
    public function user(Request $request)
    {
        return response()->json($request->user());
    }
 
    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => 'Logged out']);
    }
}

Two approaches are shown above. The redirect flow (redirect / callback) does a full-page redirect to Google — simple, works everywhere. The ID token flow (loginWithIdToken) works with Google's own JS button rendered directly inside Vue, no redirect needed — this is the smoother SPA approach and the one the frontend below is built around.

Step 05

Routes

routes/web.php — for the redirect flow
use App\Http\Controllers\Auth\GoogleAuthController;
 
Route::get('/auth/google/redirect', [GoogleAuthController::class, 'redirect']);
Route::get('/auth/google/callback', [GoogleAuthController::class, 'callback']);
routes/api.php — for the ID token flow and protected routes
use App\Http\Controllers\Auth\GoogleAuthController;
 
Route::post('/auth/google', [GoogleAuthController::class, 'loginWithIdToken']);
 
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', [GoogleAuthController::class, 'user']);
    Route::post('/logout', [GoogleAuthController::class, 'logout']);
});

Step 06

CORS configuration

config/cors.php
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => [env('FRONTEND_URL', 'http://localhost:5173')],
'supports_credentials' => true,

Run the backend:

php artisan serve

Frontend — Vue

Step 07

Create the Vue 3 frontend

npm create vite@latest google-login-frontend -- --template vue
cd google-login-frontend
npm install
npm install axios vue-router pinia

Step 08

Axios setup

src/lib/axios.js
import axios from 'axios'
 
const api = axios.create({
  baseURL: 'http://localhost:8000/api',
})
 
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})
 
export default api

Step 09

Load the Google Identity Services script

index.html
<head>
  ...
  <script src="https://accounts.google.com/gsi/client" async defer></script>
</head>

Step 10

Auth store (Pinia)

src/stores/auth.js
import { defineStore } from 'pinia'
import api from '@/lib/axios'
 
export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: JSON.parse(localStorage.getItem('user')) || null,
    token: localStorage.getItem('token') || null,
  }),
  actions: {
    async loginWithGoogle(idToken) {
      const { data } = await api.post('/auth/google', { id_token: idToken })
      this.setSession(data.token, data.user)
    },
    setSession(token, user) {
      this.token = token
      this.user = user
      localStorage.setItem('token', token)
      localStorage.setItem('user', JSON.stringify(user))
    },
    async fetchUser() {
      const { data } = await api.get('/user')
      this.user = data
      localStorage.setItem('user', JSON.stringify(data))
    },
    async logout() {
      await api.post('/logout')
      this.token = null
      this.user = null
      localStorage.removeItem('token')
      localStorage.removeItem('user')
    },
  },
})

Step 11

Login component — Google button

src/components/GoogleLoginButton.vue
<template>
  <div id="google-signin-button"></div>
</template>
 
<script setup>
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useRouter } from 'vue-router'
 
const auth = useAuthStore()
const router = useRouter()
 
const GOOGLE_CLIENT_ID = 'YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com'
 
async function handleCredentialResponse(response) {
  try {
    await auth.loginWithGoogle(response.credential)
    router.push('/dashboard')
  } catch (e) {
    console.error('Google login failed', e)
  }
}
 
onMounted(() => {
  window.google.accounts.id.initialize({
    client_id: GOOGLE_CLIENT_ID,
    callback: handleCredentialResponse,
  })
 
  window.google.accounts.id.renderButton(
    document.getElementById('google-signin-button'),
    { theme: 'outline', size: 'large', width: 280 }
  )
})
</script>

Important: response.credential is a Google ID token, a JWT — exactly what Socialite::driver('google')->userFromToken() expects on the backend.

Step 12

Login page

src/views/Login.vue
<template>
  <div class="login-page">
    <h1>Sign in</h1>
    <GoogleLoginButton />
  </div>
</template>
 
<script setup>
import GoogleLoginButton from '@/components/GoogleLoginButton.vue'
</script>

Step 13

Router with auth guard

src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import Login from '@/views/Login.vue'
import Dashboard from '@/views/Dashboard.vue'
 
const routes = [
  { path: '/login', component: Login },
  { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } },
]
 
const router = createRouter({
  history: createWebHistory(),
  routes,
})
 
router.beforeEach((to) => {
  const auth = useAuthStore()
  if (to.meta.requiresAuth && !auth.token) {
    return '/login'
  }
})
 
export default router

Step 14

Dashboard — protected page

src/views/Dashboard.vue
<template>
  <div v-if="auth.user">
    <img :src="auth.user.avatar" width="60" />
    <h2>Welcome, {{ auth.user.name }}</h2>
    <p>{{ auth.user.email }}</p>
    <button @click="logout">Logout</button>
  </div>
</template>
 
<script setup>
import { onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useRouter } from 'vue-router'
 
const auth = useAuthStore()
const router = useRouter()
 
onMounted(() => {
  if (!auth.user) auth.fetchUser()
})
 
async function logout() {
  await auth.logout()
  router.push('/login')
}
</script>

Step 15

main.js

src/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
 
createApp(App).use(createPinia()).use(router).mount('#app')

Run the frontend:

npm run dev

Verify

Step 16

Testing the flow

1. Visit http://localhost:5173/login
2. Click the Google button, then pick an account
3. Google returns an ID token to the Vue callback
4. Vue sends it to POST /api/auth/google
5. Laravel verifies it via Socialite, creates or finds the user, returns a Sanctum token
6. Vue stores the token and redirects to /dashboard
7. Dashboard calls GET /api/user with Authorization: Bearer <token> to confirm auth

Notes and gotchas

->stateless() is required for Socialite in API/SPA contexts, since there's no shared session between Laravel and a separately-hosted Vue app.

Prefer cookie-based Sanctum SPA auth instead of bearer tokens? Call /sanctum/csrf-cookie first and rely on SANCTUM_STATEFUL_DOMAINS, keeping frontend and backend on the same top-level domain in production. The token approach above is simpler when they're fully decoupled or on different domains.

In production, restrict Google's "Authorized JavaScript origins" and "Authorized redirect URIs" to your real domains.

Security: never expose GOOGLE_CLIENT_SECRET to the frontend. Only GOOGLE_CLIENT_ID is public.

Laravel 12 · Vue 3 · Socialite · Sanctum
Share this article
E
ebcoderbase

Insights and guides from the Encoderbase team on web, apps, software and SEO.

Have a project in mind?

Let's build something great together.

Get in Touch