Configure social login with Google, GitHub, Microsoft, Facebook, and Discord.
| Provider | Status | Documentation |
|---|---|---|
| ✅ STABLE | Console | |
| GitHub | ⚠️ DRAFT | Developer Settings |
| Microsoft | ⚠️ DRAFT | Azure Portal |
| ⚠️ DRAFT | Meta Developers | |
| Discord | ⚠️ DRAFT | Discord Apps |
| Twitter/X | ❌ Not implemented | - |
| Apple | ❌ Not implemented | - |
Note: Only Google OAuth has been fully tested and is production-ready. Other providers (GitHub, Microsoft, Facebook, Discord) are implemented but need more testing. Twitter/X and Apple are not yet implemented.
# config/packages/better_auth.yaml
better_auth:
oauth:
providers:
google:
enabled: true
client_id: '%env(GOOGLE_CLIENT_ID)%'
client_secret: '%env(GOOGLE_CLIENT_SECRET)%'
redirect_uri: '%env(APP_URL)%/auth/oauth/google/callback'
github:
enabled: true
client_id: '%env(GITHUB_CLIENT_ID)%'
client_secret: '%env(GITHUB_CLIENT_SECRET)%'
redirect_uri: '%env(APP_URL)%/auth/oauth/github/callback'
# .env
APP_URL=https://myapp.com
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxx
GITHUB_CLIENT_ID=xxx
GITHUB_CLIENT_SECRET=xxx
// Redirect to OAuth provider
async function loginWithGoogle() {
const response = await fetch('/auth/oauth/google');
const { url } = await response.json();
window.location.href = url;
}
https://yourapp.com/auth/oauth/google/callbackGOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxx
https://yourapp.comhttps://yourapp.com/auth/oauth/github/callbackGITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxx
https://yourapp.com/auth/oauth/microsoft/callbackMICROSOFT_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
MICROSOFT_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxx
FACEBOOK_CLIENT_ID=1234567890
FACEBOOK_CLIENT_SECRET=xxxxxxxxxxxxxxxx
https://yourapp.com/auth/oauth/discord/callbackDISCORD_CLIENT_ID=1234567890
DISCORD_CLIENT_SECRET=xxxxxxxxxxxxxxxx
curl -X GET http://localhost:8000/auth/oauth/google
Response:
{
"url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=xxx&redirect_uri=xxx&scope=email%20profile&response_type=code&state=xxx",
"state": "abc123xyz"
}
Redirect user to the url from the response.
After user authorizes, provider redirects to:
https://yourapp.com/auth/oauth/google/callback?code=xxx&state=abc123xyz
BetterAuth handles this automatically and returns:
{
"access_token": "v4.local.eyJ...",
"refresh_token": "rt_abc123...",
"user": {
"id": "019ab13e-40f1-7b21-a672-f403d5277ec7",
"email": "user@gmail.com",
"username": "John Doe",
"emailVerified": true
}
}
// components/SocialLogin.tsx
import { useState } from 'react';
const providers = [
{ id: 'google', name: 'Google', icon: '🔷' },
{ id: 'github', name: 'GitHub', icon: '🐙' },
{ id: 'microsoft', name: 'Microsoft', icon: '🪟' },
];
export function SocialLogin() {
const [loading, setLoading] = useState<string | null>(null);
const handleLogin = async (provider: string) => {
setLoading(provider);
try {
const response = await fetch(`/auth/oauth/${provider}`);
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('OAuth error:', error);
setLoading(null);
}
};
return (
<div className="social-login">
{providers.map(({ id, name, icon }) => (
<button
key={id}
onClick={() => handleLogin(id)}
disabled={loading !== null}
>
{loading === id ? 'Redirecting...' : `${icon} Sign in with ${name}`}
</button>
))}
</div>
);
}
// pages/OAuthCallback.tsx
import { useEffect } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
export function OAuthCallback() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
useEffect(() => {
const code = searchParams.get('code');
const state = searchParams.get('state');
const provider = window.location.pathname.split('/').pop();
if (code) {
// The backend handles the callback automatically
// Tokens are returned in the response
handleCallback(provider, code, state);
}
}, [searchParams]);
const handleCallback = async (provider: string, code: string, state: string | null) => {
try {
const response = await fetch(
`/auth/oauth/${provider}/callback?code=${code}&state=${state}`
);
const data = await response.json();
if (data.access_token) {
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);
navigate('/dashboard');
}
} catch (error) {
console.error('Callback error:', error);
navigate('/login?error=oauth_failed');
}
};
return <div>Processing authentication...</div>;
}
When a user logs in with OAuth:
To allow users to link additional OAuth providers:
// Controller
#[Route('/auth/link/{provider}', methods: ['GET'])]
#[IsGranted('ROLE_USER')]
public function linkProvider(string $provider): JsonResponse
{
$user = $this->getUser();
// Get OAuth URL with linking flag
$result = $this->oauthManager->getAuthorizationUrl($provider, [
'link_to_user' => $user->getId(),
]);
return $this->json([
'url' => $result['url'],
'state' => $result['state'],
]);
}
When a user with 2FA enabled logs in via OAuth:
{
"requires2fa": true,
"message": "Two-factor authentication required",
"user": {
"id": "xxx",
"email": "user@gmail.com"
}
}
The frontend should then show the 2FA form and call /auth/login/2fa.
BetterAuth uses CSRF protection via the state parameter:
OAuth tokens from providers are NOT stored. Only:
Ensure the redirect URI in your config matches exactly what's configured in the provider's dashboard.
The state parameter didn't match. This can happen if:
User already has an account with that email. Options:
How can I help you explore Laravel packages today?