Introduction
Social authentication simplifies user onboarding by leveraging existing accounts from providers like Google, Facebook, Apple, and GitHub. Users avoid creating another password, and you gain access to verified email addresses. This guide covers implementing OAuth-based social login across major providers, with practical examples using Laravel Socialite.
OAuth Flow Overview
1. User clicks "Sign in with Google"
2. Redirect to Google's authorization URL
3. User authenticates with Google
4. Google redirects back with authorization code
5. Exchange code for access token
6. Fetch user profile with token
7. Create/login user in your system
Laravel Socialite Setup
Installation
composer require laravel/socialite
Configuration
// config/services.php
return [
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT_URI'),
],
'facebook' => [
'client_id' => env('FACEBOOK_CLIENT_ID'),
'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
'redirect' => env('FACEBOOK_REDIRECT_URI'),
],
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('GITHUB_REDIRECT_URI'),
],
'apple' => [
'client_id' => env('APPLE_CLIENT_ID'),
'client_secret' => env('APPLE_CLIENT_SECRET'),
'redirect' => env('APPLE_REDIRECT_URI'),
],
];
Google Sign-In
Set Up Google OAuth
- Go to Google Cloud Console
- Create a project
- Enable "Google+ API" or "Google Identity Platform"
- Go to Credentials → Create OAuth Client ID
- Configure authorized redirect URIs:
https://yourapp.com/auth/google/callback
Controller Implementation
// routes/web.php
Route::get('/auth/{provider}', [SocialAuthController::class, 'redirect']);
Route::get('/auth/{provider}/callback', [SocialAuthController::class, 'callback']);
// app/Http/Controllers/SocialAuthController.php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Str;
class SocialAuthController extends Controller
{
protected $providers = ['google', 'facebook', 'github', 'apple'];
public function redirect($provider)
{
if (!in_array($provider, $this->providers)) {
abort(404);
}
return Socialite::driver($provider)->redirect();
}
public function callback($provider)
{
if (!in_array($provider, $this->providers)) {
abort(404);
}
try {
$socialUser = Socialite::driver($provider)->user();
} catch (\Exception $e) {
return redirect('/login')->with('error', 'Authentication failed');
}
$user = $this->findOrCreateUser($socialUser, $provider);
Auth::login($user, true);
return redirect()->intended('/dashboard');
}
protected function findOrCreateUser($socialUser, $provider)
{
// Check if the user exists with this provider.
$socialAccount = \App\Models\SocialAccount::where('provider', $provider)
->where('provider_user_id', $socialUser->getId())
->first();
if ($socialAccount) {
return $socialAccount->user;
}
// Check if the user exists with the same email.
$user = User::where('email', $socialUser->getEmail())->first();
if (!$user) {
// Create a new user.
$user = User::create([
'name' => $socialUser->getName(),
'email' => $socialUser->getEmail(),
'email_verified_at' => now(),
'password' => bcrypt(Str::random(24)),
'avatar' => $socialUser->getAvatar(),
]);
}
// Link social account.
$user->socialAccounts()->create([
'provider' => $provider,
'provider_user_id' => $socialUser->getId(),
'token' => $socialUser->token,
'refresh_token' => $socialUser->refreshToken,
]);
return $user;
}
}
Database Setup
// Migration for the social_accounts table.
Schema::create('social_accounts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('provider');
$table->string('provider_user_id');
$table->text('token')->nullable();
$table->text('refresh_token')->nullable();
$table->timestamps();
$table->unique(['provider', 'provider_user_id']);
});
// app/Models/SocialAccount.php
class SocialAccount extends Model
{
protected $fillable = [
'user_id',
'provider',
'provider_user_id',
'token',
'refresh_token',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
// app/Models/User.php
class User extends Authenticatable
{
public function socialAccounts()
{
return $this->hasMany(SocialAccount::class);
}
}
Facebook Login
Set Up Facebook OAuth
- Go to Facebook Developers
- Create an app
- Add Facebook Login product
- Configure Valid OAuth Redirect URIs
- Get App ID and App Secret from Settings
Additional Scopes
public function redirect($provider)
{
$driver = Socialite::driver($provider);
if ($provider === 'facebook') {
$driver->scopes(['email', 'public_profile']);
}
return $driver->redirect();
}
Apple Sign-In
Set Up Apple OAuth
- Go to Apple Developer Console
- Create an App ID with Sign in with Apple capability
- Create a Services ID
- Configure Return URLs
- Generate a client secret (requires private key)
Apple-Specific Handling
Apple requires special handling because it only returns user info on first authentication:
public function callback($provider)
{
if ($provider === 'apple') {
return $this->handleAppleCallback();
}
// ... standard handling
}
protected function handleAppleCallback()
{
$socialUser = Socialite::driver('apple')->user();
// Apple only sends the name on first auth.
$name = $socialUser->getName();
if (!$name) {
// Try to get it from previously stored data or request it.
$name = request('user.name.firstName') . ' ' . request('user.name.lastName');
}
// Email might be a private relay.
$email = $socialUser->getEmail();
// ... create/find user
}
GitHub Login
Set Up GitHub OAuth
- Go to GitHub Developer Settings
- Create an OAuth App
- Set Authorization callback URL
- Get Client ID and Client Secret
Request Additional Scopes
public function redirect($provider)
{
$driver = Socialite::driver($provider);
if ($provider === 'github') {
$driver->scopes(['user:email', 'read:user']);
}
return $driver->redirect();
}
Frontend Integration
Login Buttons
<div class="social-login">
<a href="/auth/google" class="btn btn-google">
<img src="/icons/google.svg" alt="">
Continue with Google
</a>
<a href="/auth/facebook" class="btn btn-facebook">
<img src="/icons/facebook.svg" alt="">
Continue with Facebook
</a>
<a href="/auth/apple" class="btn btn-apple">
<img src="/icons/apple.svg" alt="">
Continue with Apple
</a>
<a href="/auth/github" class="btn btn-github">
<img src="/icons/github.svg" alt="">
Continue with GitHub
</a>
</div>
Vue.js Component
<template>
<div class="social-auth">
<button
v-for="provider in providers"
:key="provider.name"
@click="authenticate(provider.name)"
:class="`btn-${provider.name}`"
:disabled="loading"
>
<component :is="provider.icon" />
{{ loading ? 'Connecting...' : `Continue with ${provider.label}` }}
</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const loading = ref(false);
const providers = [
{ name: 'google', label: 'Google' },
{ name: 'facebook', label: 'Facebook' },
{ name: 'apple', label: 'Apple' },
];
function authenticate(provider) {
loading.value = true;
window.location.href = `/auth/${provider}`;
}
</script>
SPA/API Authentication
For single-page applications, use stateless OAuth:
// Return token instead of session.
public function callback($provider)
{
$socialUser = Socialite::driver($provider)->stateless()->user();
$user = $this->findOrCreateUser($socialUser, $provider);
// Create API token.
$token = $user->createToken('social-auth')->plainTextToken;
// Redirect to the frontend with token.
return redirect(config('app.frontend_url') . '/auth/callback?token=' . $token);
}
Security Considerations
State Parameter
Socialite handles CSRF protection with the state parameter automatically. For stateless requests:
// Generate and verify state manually for SPAs.
$state = Str::random(40);
session(['oauth_state' => $state]);
return Socialite::driver($provider)
->with(['state' => $state])
->redirect();
Email Verification
Social providers typically verify emails, but verify the provider's guarantees:
$user = User::create([
'email' => $socialUser->getEmail(),
'email_verified_at' => $this->providerVerifiesEmail($provider)
? now()
: null,
]);
protected function providerVerifiesEmail($provider)
{
return in_array($provider, ['google', 'apple', 'github']);
}
Account Linking
Allow users to link multiple providers:
public function link($provider)
{
// Must be authenticated.
$user = auth()->user();
$socialUser = Socialite::driver($provider)->user();
// Check if already linked to another account.
$existing = SocialAccount::where('provider', $provider)
->where('provider_user_id', $socialUser->getId())
->first();
if ($existing && $existing->user_id !== $user->id) {
return back()->with('error', 'This account is linked to another user');
}
$user->socialAccounts()->updateOrCreate(
['provider' => $provider],
[
'provider_user_id' => $socialUser->getId(),
'token' => $socialUser->token,
]
);
return back()->with('success', 'Account linked successfully');
}
Error Handling
public function callback($provider)
{
try {
$socialUser = Socialite::driver($provider)->user();
} catch (\Laravel\Socialite\Two\InvalidStateException $e) {
return redirect('/login')
->with('error', 'Session expired. Please try again.');
} catch (\GuzzleHttp\Exception\ClientException $e) {
return redirect('/login')
->with('error', 'Authentication was denied.');
} catch (\Exception $e) {
Log::error('Social auth error', [
'provider' => $provider,
'error' => $e->getMessage(),
]);
return redirect('/login')
->with('error', 'Authentication failed. Please try again.');
}
// ... continue with user creation
}
Direct JavaScript SDK Integration
For cases where you need more control or aren't using Laravel Socialite, you can integrate directly with provider SDKs.
Google Sign-In with JavaScript
<div id="g_id_signin"></div>
<script>
const clientId = '<google client ID>'
const backendUrl = '<oauth backend URL>'
var script = document.createElement('script')
script.defer = true
script.async = true
script.src = 'https://accounts.google.com/gsi/client'
script.addEventListener("load", function() {
google.accounts.id.initialize({
client_id: clientId,
callback: (response) => {
if (response?.credential) {
fetch(backendUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: response.credential }),
})
.then(res => res.json())
.then(data => {
// Process user authentication
localStorage.setItem('auth_token', data.token);
window.location.href = '/dashboard';
})
.catch(err => console.error('Auth failed:', err));
}
}
});
google.accounts.id.renderButton(
document.getElementById('g_id_signin'),
{ theme: 'outline', size: 'large', logo_alignment: 'center' }
);
google.accounts.id.prompt(); // Enable One Tap
});
document.head.appendChild(script);
</script>
Google Token Validation (PHP Backend)
use Illuminate\Support\Facades\Http;
$token = $request->input('token');
// Validate token with Google.
$response = Http::post("https://oauth2.googleapis.com/tokeninfo", [
'id_token' => $token,
]);
if ($response->status() !== 200) {
return response()->json(['error' => 'Invalid token'], 401);
}
$userInfo = $response->json();
// Validate required fields.
if (empty($userInfo['sub']) || empty($userInfo['email'])) {
return response()->json(['error' => 'Missing user info'], 400);
}
$user = User::firstOrCreate(
['email' => $userInfo['email']],
[
'name' => $userInfo['name'],
'email_verified_at' => now(),
'password' => bcrypt(Str::random(24)),
]
);
$token = $user->createToken('auth-token')->plainTextToken;
return response()->json(['token' => $token, 'user' => $user]);
Facebook Login with JavaScript SDK
<div class="fb-login-button"
data-size="large"
data-button-type="login_with"
data-scope="email"
></div>
<div id="fb-root"></div>
<script>
const appId = '<facebook app ID>'
const backendUrl = '<oauth backend URL>'
window.fbAsyncInit = function() {
FB.init({
appId: appId,
cookie: false,
xfbml: true,
version: 'v18.0',
});
FB.Event.subscribe('auth.authResponseChange', function(response) {
if (response?.status === 'connected') {
fetch(backendUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: response.authResponse.accessToken }),
})
.then(res => res.json())
.then(data => {
localStorage.setItem('auth_token', data.token);
window.location.href = '/dashboard';
});
}
});
FB.AppEvents.logPageView();
};
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = 'https://connect.facebook.net/en_US/sdk.js';
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
Facebook Token Validation (PHP Backend)
$token = $request->input('token');
$response = Http::get("https://graph.facebook.com/me", [
'access_token' => $token,
'fields' => 'id,name,email',
]);
if ($response->status() !== 200) {
return response()->json(['error' => 'Invalid token'], 401);
}
$userInfo = $response->json();
$user = User::firstOrCreate(
['email' => $userInfo['email']],
[
'name' => $userInfo['name'],
'email_verified_at' => now(),
'password' => bcrypt(Str::random(24)),
]
);
// Store Facebook ID for future logins.
$user->socialAccounts()->updateOrCreate(
['provider' => 'facebook'],
['provider_user_id' => $userInfo['id'], 'token' => $token]
);
return response()->json([
'token' => $user->createToken('auth-token')->plainTextToken,
'user' => $user
]);
Apple Sign-In with JavaScript
Apple Sign-In requires special handling, as user info is only provided on first authentication:
<div id="appleid-signin" class="signin-button"
data-color="black" data-border="true" data-type="sign-in"></div>
<script>
const clientId = '<apple service ID identifier>'
const backendUrl = '<oauth backend URL>'
var script = document.createElement('script')
script.src = 'https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js'
script.addEventListener("load", function() {
window.AppleID.auth.init({
clientId: clientId,
scope: 'name email',
redirectURI: window.location.href,
state: 'csrf' + Math.random(),
usePopup: true,
});
document.addEventListener('AppleIDSignInOnSuccess', (event) => {
fetch(backendUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: event.detail?.authorization?.id_token,
// Apple only provides name on first auth
name: event.detail?.user?.name?.firstName + ' ' +
event.detail?.user?.name?.lastName,
}),
})
.then(res => res.json())
.then(data => {
localStorage.setItem('auth_token', data.token);
window.location.href = '/dashboard';
});
});
});
document.head.appendChild(script);
</script>
Apple Token Validation (PHP Backend)
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
$token = $request->input('token');
$fullName = $request->input('name');
$clientId = config('services.apple.client_id');
// Fetch Apple's public keys.
$keysResponse = Http::get("https://appleid.apple.com/auth/keys");
$publicKeys = $keysResponse->json();
try {
$userInfo = (array) JWT::decode($token, JWK::parseKeySet($publicKeys));
} catch (\Exception $e) {
return response()->json(['error' => 'Invalid token'], 401);
}
// Validate token claims.
if ($userInfo['exp'] < time() ||
$userInfo['aud'] !== $clientId ||
$userInfo['iss'] !== 'https://appleid.apple.com') {
return response()->json(['error' => 'Token validation failed'], 401);
}
$user = User::firstOrCreate(
['email' => $userInfo['email']],
[
'name' => $fullName ?: 'Apple User',
'email_verified_at' => now(),
'password' => bcrypt(Str::random(24)),
]
);
return response()->json([
'token' => $user->createToken('auth-token')->plainTextToken,
'user' => $user
]);
Conclusion
Social authentication improves user experience and can increase conversion rates. Implement multiple providers to give users choice, handle edge cases like email conflicts gracefully, and always consider security implications. Whether using Laravel Socialite or direct SDK integration, the patterns in this guide provide a foundation for robust social authentication across major providers.