OAuth Providers
This guide covers setting up Google OAuth authentication with Better Auth, including obtaining credentials, configuration, profile mapping, and handling OAuth flows.
Google OAuth Setup
Step 1: Create Google Cloud Project
- Go to Google Cloud Console
- Create a new project or select an existing one
- Navigate to "APIs & Services" > "Credentials"
Step 2: Configure OAuth Consent Screen
- Click "OAuth consent screen" in the left sidebar
- Select "External" user type (or "Internal" for Google Workspace)
- Fill in application information:
- App name: Your application name
- User support email: Your support email
- Developer contact: Your email
- Add scopes (optional for basic auth):
./auth/userinfo.email./auth/userinfo.profile
- Add test users if in testing mode
- Save and continue
Step 3: Create OAuth Client ID
- Go to "Credentials" tab
- Click "Create Credentials" > "OAuth client ID"
- Select "Web application"
- Configure:
- Name: Your app name (e.g., "SaaS Boilerplate Web")
- Authorized JavaScript origins:
http://localhost:5173 http://localhost:3000 https://your-production-domain.com - Authorized redirect URIs:
http://localhost:5173/api/auth/callback/google http://localhost:3000/api/auth/callback/google https://your-production-domain.com/api/auth/callback/google
- Click "Create"
- Copy the Client ID and Client Secret
Step 4: Configure Environment Variables
Add to your .env.local:
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
Better Auth Configuration
Google Provider Setup
File: core/lib/auth.ts
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
mapProfileToUser: (profile: GoogleProfile) => {
// Google provides given_name and family_name separately
const firstName = profile.given_name || profile.name.split(' ')[0] || '';
const lastName = profile.family_name || profile.name.split(' ').slice(1).join(' ') || '';
return {
email: profile.email,
name: profile.name,
firstName: firstName,
lastName: lastName,
language: I18N_CONFIG.defaultLocale,
role: USER_ROLES_CONFIG.defaultRole,
image: profile.picture,
emailVerified: profile.email_verified || false,
};
},
},
}
Google Profile Structure
interface GoogleProfile {
email: string; // User's email address
name: string; // Full name
given_name?: string; // First name
family_name?: string; // Last name
picture?: string; // Profile picture URL
email_verified?: boolean; // Email verification status
}
Profile Mapping Explained
The mapProfileToUser function transforms Google's profile data to match your user model:
- firstName: Uses
given_nameif available, otherwise splitsname - lastName: Uses
family_nameif available, otherwise gets remaining parts ofname - name: Full name for Better Auth compatibility
- language: Assigns default locale from config
- role: Assigns default role from config
- image: Google profile picture
- emailVerified: Already verified by Google
OAuth Flow
Sign In Process
1. User clicks "Sign in with Google" button
↓
2. Redirected to Google consent screen
↓
3. User authorizes application
↓
4. Google redirects to callback URL with authorization code
↓
5. Better Auth exchanges code for access token
↓
6. Better Auth fetches user profile from Google
↓
7. mapProfileToUser transforms profile data
↓
8. User account created/linked in database
↓
9. Session created and user redirected to dashboard
Implementation
Sign In Button Component
'use client'
import { authClient } from '@/core/lib/auth-client'
export function GoogleSignInButton() {
const handleGoogleSignIn = async () => {
try {
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
})
} catch (error) {
console.error('Google sign in failed:', error)
}
}
return (
<button
onClick={handleGoogleSignIn}
className="flex items-center gap-2 px-4 py-2 border rounded-lg hover:bg-gray-50"
>
<GoogleIcon />
<span>Continue with Google</span>
</button>
)
}
With Redirect Options
await authClient.signIn.social({
provider: 'google',
callbackURL: '/dashboard',
errorCallbackURL: '/login?error=oauth_failed',
})
Account Linking
When Email Already Exists
Better Auth automatically handles account linking when a user signs in with Google using an email that already exists in the system:
Scenario 1: Email/Password Account Exists
1. User signed up with email/password
2. User tries to sign in with Google using same email
3. Better Auth links Google account to existing user
4. User can now sign in with either method
Scenario 2: Multiple OAuth Providers
1. User signed in with Google
2. User tries to sign in with another provider (if added) using same email
3. Better Auth links accounts
4. User has multiple sign-in options
Account Table Structure
CREATE TABLE "account" (
"id" TEXT PRIMARY KEY,
"accountId" TEXT NOT NULL, -- Google user ID
"providerId" TEXT NOT NULL, -- "google"
"userId" TEXT NOT NULL, -- Your user ID
"accessToken" TEXT, -- OAuth access token
"refreshToken" TEXT, -- OAuth refresh token
UNIQUE("providerId", "accountId")
);
Trusted Origins
Configure allowed origins for OAuth redirects:
trustedOrigins: [
process.env.BETTER_AUTH_URL,
process.env.NEXT_PUBLIC_APP_URL,
'http://localhost:5173',
'http://localhost:3000',
'http://127.0.0.1:3000',
].filter(Boolean)
Important: All OAuth callback URLs must be in trustedOrigins.
Security Considerations
State Parameter
Better Auth automatically includes a state parameter in OAuth requests to prevent CSRF attacks:
https://accounts.google.com/o/oauth2/v2/auth?
client_id=...
&redirect_uri=...
&state=random_secure_token ← CSRF protection
&scope=...
Token Storage
OAuth tokens are stored securely:
- Access tokens: Stored in
accounttable (not exposed to client) - Refresh tokens: Stored encrypted (if applicable)
- Session tokens: Stored in secure httpOnly cookies
Email Verification
Google OAuth users have emailVerified: true automatically since Google has already verified the email.
Handling OAuth Errors
Common Errors
Error: "Redirect URI mismatch"
Solution: Ensure callback URL in Google Cloud Console matches exactly:
http://localhost:5173/api/auth/callback/google
Error: "Invalid client ID"
Solution: Verify GOOGLE_CLIENT_ID environment variable is set correctly.
Error: "Access denied"
Solution: User cancelled authorization. Handle gracefully:
const searchParams = new URLSearchParams(window.location.search)
const error = searchParams.get('error')
if (error === 'access_denied') {
// User cancelled OAuth flow
showMessage('Sign in cancelled')
}
Error Handling in Component
'use client'
import { useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
export function OAuthErrorHandler() {
const searchParams = useSearchParams()
const error = searchParams.get('error')
useEffect(() => {
if (error === 'oauth_account_not_linked') {
showToast('Please sign in with your original method')
} else if (error === 'oauth_failed') {
showToast('Authentication failed. Please try again.')
}
}, [error])
return null
}
Testing OAuth Flow
Local Testing
- Start development server:
pnpm dev - Navigate to login page
- Click "Sign in with Google"
- Use a test Google account
- Verify redirect to dashboard
- Check database for user and account records
Test Accounts
For development, add test users in Google Cloud Console:
- Go to "OAuth consent screen"
- Scroll to "Test users"
- Add email addresses
- These users can sign in while app is in testing mode
Custom OAuth Scopes
If you need additional Google APIs:
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scopes: [
'openid',
'email',
'profile',
// Add additional scopes as needed
'https://www.googleapis.com/auth/calendar.readonly'
],
mapProfileToUser: (profile) => {
// ...
},
},
}
Note: Additional scopes require app verification by Google for production use.
Production Checklist
Before deploying to production:
- Set production redirect URIs in Google Cloud Console
- Configure
BETTER_AUTH_URLenvironment variable - Add production domain to
trustedOrigins - Submit app for Google OAuth verification (if using sensitive scopes)
- Test OAuth flow in production environment
- Monitor OAuth errors in logs
Debugging
Enable Debugging
// Add to Better Auth configuration (development only)
advanced: {
debug: process.env.NODE_ENV === 'development',
}
Check OAuth Callback
// Create debug endpoint to inspect callback data
// app/api/auth/debug/callback/route.ts
export async function GET(request: Request) {
const url = new URL(request.url)
const params = Object.fromEntries(url.searchParams)
return Response.json({
params,
headers: Object.fromEntries(request.headers)
})
}
Verify Profile Mapping
mapProfileToUser: (profile) => {
console.log('Google profile:', profile) // Debug log
const mapped = {
email: profile.email,
// ... rest of mapping
}
console.log('Mapped user:', mapped) // Debug log
return mapped
}
Next Steps
- API Key Management - Alternative authentication method
- Session Management - How sessions work
- Permissions and Roles - Role assignment after OAuth
💡 Tip: Google OAuth is already configured in the boilerplate. You only need to obtain your Google Cloud credentials and set the environment variables to enable it.