Introduction
Google OAuth 2.0 authentication is one of the most popular ways to add secure, user-friendly login functionality to web applications. It eliminates the need for users to remember yet another password and provides developers with verified user information, including email addresses and profile pictures.
In this comprehensive guide, I’ll walk you through implementing Google OAuth 2.0 authentication in PHP from scratch. We’ll cover everything from setting up Google Developer Console credentials to handling the complete authentication flow with proper security measures.

Why This Approach?
This guide provides a pure PHP implementation with zero external dependencies. No Composer, no vendor files, no SDK packages, and no complex setup required. While modern solutions like Supabase Auth, Auth0, or Firebase Authentication offer convenience, this approach gives you complete control over your authentication flow and eliminates vendor lock-in. Perfect for developers who want to understand the OAuth 2.0 process from the ground up or need a lightweight, self-contained authentication solution.
Table of Contents
- Introduction
- Why This Approach?
- Prerequisites
- Setting Up Google Developer Console
- Understanding OAuth 2.0 Flow
- Database Schema
- Implementation
- Adding the Login Button
- Security Considerations
- Testing and Troubleshooting
- Complete Code Example
Prerequisites
Before we begin, make sure you have:
- A web server with PHP 7.4+ installed
- A database (MySQL, SQLite, or PostgreSQL)
- A domain name (for production) or localhost (for development)
- Basic knowledge of PHP and SQL
Setting Up Google Developer Console
Step 1: Create a Google Cloud Project
- Go to the Google Cloud Console
- Click “Select a project” → “New Project”
- Enter a project name and click “Create”
Step 2: Enable Google+ API
- In your project, go to “APIs & Services” → “Library”
- Search for “Google+ API” and click on it
- Click “Enable”
Step 3: Create OAuth 2.0 Credentials
- Go to “APIs & Services” → “Credentials”
- Click “Create Credentials” → “OAuth client ID”
- If prompted, configure the OAuth consent screen:
- Choose “External” user type
- Fill in the required fields (App name, User support email, Developer contact)
- Add your domain to authorized domains
- For application type, choose “Web application”
- Add authorized redirect URIs:
- For development:
http://localhost/your-app/google-auth-generic.php?action=google_callback
- For production:
https://yourdomain.com/google-auth-generic.php?action=google_callback
- Click “Create”
Step 4: Save Your Credentials
You’ll receive a Client ID and Client Secret. Save these securely – you’ll need them for the implementation.
Understanding OAuth 2.0 Flow
The OAuth 2.0 flow consists of these steps:
- Authorization Request: User clicks login button → redirect to Google
- User Consent: User grants permission on Google’s site
- Authorization Code: Google redirects back with a temporary code
- Token Exchange: Your server exchanges the code for an access token
- User Info: Use the access token to fetch user information
- Session Creation: Create or update user account and start session
Database Schema
You’ll need a users
table to store user information. Here are the schemas for different database systems:
MySQL Schema
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255), -- NULL for OAuth users
google_id VARCHAR(255),
auth_provider ENUM('local', 'google') DEFAULT 'local',
profile_picture VARCHAR(500),
profile_public BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
SQLite Schema
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT,
google_id TEXT,
auth_provider TEXT DEFAULT 'local',
profile_picture TEXT,
profile_public INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
PostgreSQL Schema
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255),
google_id VARCHAR(255),
auth_provider VARCHAR(10) DEFAULT 'local',
profile_picture VARCHAR(500),
profile_public BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Implementation
Step 1: Configuration
Create a file called google-auth-generic.php
and start with the configuration section:
<?php
// Google OAuth 2.0 Credentials (from Google Developer Console)
define('GOOGLE_CLIENT_ID', 'your-google-client-id.apps.googleusercontent.com');
define('GOOGLE_CLIENT_SECRET', 'your-google-client-secret');
// Your application's callback URL
define('GOOGLE_REDIRECT_URI', 'https://yourdomain.com/google-auth-generic.php?action=google_callback');
// Database configuration
define('DB_HOST', 'localhost');
define('DB_NAME', 'your_database');
define('DB_USER', 'your_username');
define('DB_PASS', 'your_password');
// Application settings
define('APP_NAME', 'Your App Name');
define('APP_URL', 'https://yourdomain.com');
Step 2: Database Connection Function
function get_db_connection() {
try {
// For MySQL
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
return $pdo;
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
Step 3: Helper Functions
function slugify($text) {
$text = strtolower($text);
$text = preg_replace('/[^a-z0-9\s-]/', '', $text);
$text = preg_replace('/[\s-]+/', '-', $text);
return trim($text, '-');
}
function is_logged_in() {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
Step 4: Main Authentication Logic
The complete implementation handles both the initial login request and the callback:
session_start();
$action = $_GET['action'] ?? '';
try {
switch ($action) {
case 'google_login':
// Generate OAuth state parameter for security
$state = bin2hex(random_bytes(16));
$_SESSION['google_oauth_state'] = $state;
// Build Google OAuth authorization URL
$auth_url = 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query([
'client_id' => GOOGLE_CLIENT_ID,
'redirect_uri' => GOOGLE_REDIRECT_URI,
'response_type' => 'code',
'scope' => 'openid email profile',
'access_type' => 'online',
'prompt' => 'select_account',
'state' => $state,
]);
header('Location: ' . $auth_url);
exit;
case 'google_callback':
// Verify OAuth state parameter
if (!isset($_GET['state']) || !isset($_SESSION['google_oauth_state']) || $_GET['state'] !== $_SESSION['google_oauth_state']) {
echo 'Invalid state parameter. Possible CSRF attack.';
exit;
}
unset($_SESSION['google_oauth_state']);
if (!isset($_GET['code'])) {
echo 'Authorization failed. No code received from Google.';
exit;
}
$code = $_GET['code'];
// Exchange authorization code for access token
$token_response = file_get_contents('https://oauth2.googleapis.com/token', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => http_build_query([
'code' => $code,
'client_id' => GOOGLE_CLIENT_ID,
'client_secret' => GOOGLE_CLIENT_SECRET,
'redirect_uri' => GOOGLE_REDIRECT_URI,
'grant_type' => 'authorization_code',
]),
],
]));
$token_data = json_decode($token_response, true);
$access_token = $token_data['access_token'] ?? null;
if ($access_token) {
// Get user information from Google
$user_info = file_get_contents('https://www.googleapis.com/oauth2/v2/userinfo?access_token=' . urlencode($access_token));
$user = json_decode($user_info, true);
if (isset($user['email'])) {
$pdo = get_db_connection();
// Check if user exists in database
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$user['email']]);
$existing_user = $stmt->fetch();
if ($existing_user) {
// User exists - log them in
$_SESSION['user_id'] = $existing_user['id'];
$_SESSION['email'] = $existing_user['email'];
$_SESSION['username'] = $existing_user['username'];
// Update Google ID if not set
if (empty($existing_user['google_id'])) {
$stmt = $pdo->prepare("UPDATE users SET google_id = ? WHERE id = ?");
$stmt->execute([$user['id'], $existing_user['id']]);
}
} else {
// User doesn't exist - create new account
$username = slugify($user['name'] ?? 'user');
// Ensure username is unique
$original_username = $username;
$counter = 1;
while (true) {
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if (!$stmt->fetch()) {
break;
}
$username = $original_username . $counter;
$counter++;
}
// Insert new user
$stmt = $pdo->prepare("
INSERT INTO users (email, username, google_id, auth_provider, profile_picture, profile_public)
VALUES (?, ?, ?, 'google', ?, 1)
");
$stmt->execute([
$user['email'],
$username,
$user['id'],
$user['picture'] ?? null
]);
$user_id = $pdo->lastInsertId();
$_SESSION['user_id'] = $user_id;
$_SESSION['email'] = $user['email'];
$_SESSION['username'] = $username;
}
header('Location: ' . APP_URL);
exit;
}
}
break;
}
} catch (Exception $e) {
echo 'Authentication error: ' . $e->getMessage();
}
?>
Adding the Login Button
To add the Google login button to your pages, use this HTML:
<a href="google-auth-generic.php?action=google_login" class="btn btn-google">
<img src="https://developers.google.com/identity/images/g-logo.png" alt="Google logo">
Sign in with Google
</a>
And add some CSS for styling:
.btn-google {
background: #fff;
color: #444;
border: 1px solid #ccc;
padding: 0.5em 1em;
border-radius: 4px;
display: inline-block;
text-decoration: none;
font-family: Arial, sans-serif;
}
.btn-google img {
width: 20px;
vertical-align: middle;
margin-right: 8px;
}
.btn-google:hover {
background: #f5f5f5;
border-color: #999;
}
Security Considerations
1. State Parameter
The state parameter prevents CSRF attacks by ensuring the callback comes from the same session that initiated the login.
2. HTTPS in Production
Always use HTTPS in production. Google requires it for OAuth 2.0.
3. Secure Session Management
// Set secure session parameters
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // Only in production with HTTPS
ini_set('session.use_strict_mode', 1);
4. Input Validation
Always validate and sanitize user input, even when it comes from Google.
5. Error Handling
Don’t expose sensitive information in error messages:
} catch (Exception $e) {
// Log the error for debugging
error_log("Google auth error: " . $e->getMessage());
// Show generic message to user
echo 'Authentication failed. Please try again.';
}
Testing and Troubleshooting
Common Issues
- “Invalid redirect_uri”
- Make sure the redirect URI in your code matches exactly what’s configured in Google Developer Console
- Check for trailing slashes and protocol (
http
vshttps
)
- “Invalid state parameter”
- Session might be expiring too quickly
- Check if sessions are working properly on your server
- “Authorization failed”
- Check if the Google+ API is enabled
- Verify your client ID and secret are correct
Testing Checklist
- [ ] Google Developer Console credentials are correct
- [ ] Redirect URI matches exactly
- [ ] Database connection works
- [ ] Sessions are enabled and working
- [ ] HTTPS is used in production
- [ ] Error handling is in place
Complete Code Example
The complete implementation is available in the google-auth-generic.php
file. This includes:
- Full OAuth 2.0 flow implementation
- Database integration for multiple database systems
- Security measures (state parameter, input validation)
- Error handling and user feedback
- Comprehensive documentation
<?php
/**
* Generic Google OAuth 2.0 Authentication Handler
*
* This file provides a complete Google OAuth 2.0 implementation that can be
* easily integrated into any PHP application. It handles both the initial
* authorization request and the callback with user data processing.
*
* Features:
* - OAuth 2.0 state parameter for security
* - Automatic user creation/login
* - Profile picture and email handling
* - Generic database schema support
*
* @author Ciprian Popescu
* @version 1.0.0
* @license MIT
*/
// ============================================================================
// CONFIGURATION - UPDATE THESE VALUES
// ============================================================================
// Google OAuth 2.0 Credentials (from Google Developer Console)
define('GOOGLE_CLIENT_ID', 'your-google-client-id.apps.googleusercontent.com');
define('GOOGLE_CLIENT_SECRET', 'your-google-client-secret');
// Your application's callback URL
define('GOOGLE_REDIRECT_URI', 'https://yourdomain.com/google-auth-generic.php?action=google_callback');
// Database configuration (update with your database connection)
define('DB_HOST', 'localhost');
define('DB_NAME', 'your_database');
define('DB_USER', 'your_username');
define('DB_PASS', 'your_password');
// Application settings
define('APP_NAME', 'Your App Name');
define('APP_URL', 'https://yourdomain.com');
// ============================================================================
// DATABASE SCHEMA (Generic - works with MySQL, SQLite, PostgreSQL)
// ============================================================================
/*
Required database table structure:
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTO_INCREMENT, -- or AUTOINCREMENT for SQLite
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255), -- NULL for OAuth users
google_id VARCHAR(255),
auth_provider ENUM('local', 'google') DEFAULT 'local',
profile_picture VARCHAR(500),
profile_public BOOLEAN DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
For SQLite, use:
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT,
google_id TEXT,
auth_provider TEXT DEFAULT 'local',
profile_picture TEXT,
profile_public INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
For PostgreSQL, use:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255),
google_id VARCHAR(255),
auth_provider VARCHAR(10) DEFAULT 'local',
profile_picture VARCHAR(500),
profile_public BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*/
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Get database connection (update this function for your database system)
*
* @return PDO Database connection
*/
function get_db_connection() {
try {
// For MySQL
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
// For SQLite (uncomment and modify as needed)
// $pdo = new PDO("sqlite:" . __DIR__ . "/database.sqlite");
// For PostgreSQL (uncomment and modify as needed)
// $pdo = new PDO("pgsql:host=" . DB_HOST . ";dbname=" . DB_NAME, DB_USER, DB_PASS);
return $pdo;
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
/**
* Convert text to URL-friendly slug
*
* @param string $text Input text
* @return string URL-friendly slug
*/
function slugify($text) {
$text = strtolower($text);
$text = preg_replace('/[^a-z0-9\s-]/', '', $text);
$text = preg_replace('/[\s-]+/', '-', $text);
return trim($text, '-');
}
/**
* Check if user is logged in
*
* @return bool True if user is logged in
*/
function is_logged_in() {
return isset($_SESSION['user_id']) && !empty($_SESSION['user_id']);
}
// ============================================================================
// MAIN AUTHENTICATION LOGIC
// ============================================================================
// Start session
session_start();
// Get the action from URL parameter
$action = $_GET['action'] ?? '';
try {
switch ($action) {
case 'google_login':
// Step 1: Generate OAuth state parameter for security
$state = bin2hex(random_bytes(16));
$_SESSION['google_oauth_state'] = $state;
// Step 2: Build Google OAuth authorization URL
$auth_url = 'https://accounts.google.com/o/oauth2/v2/auth?' . http_build_query([
'client_id' => GOOGLE_CLIENT_ID,
'redirect_uri' => GOOGLE_REDIRECT_URI,
'response_type' => 'code',
'scope' => 'openid email profile',
'access_type' => 'online',
'prompt' => 'select_account',
'state' => $state,
]);
// Step 3: Redirect user to Google
header('Location: ' . $auth_url);
exit;
case 'google_callback':
// Step 4: Verify OAuth state parameter
if (!isset($_GET['state']) || !isset($_SESSION['google_oauth_state']) || $_GET['state'] !== $_SESSION['google_oauth_state']) {
echo 'Invalid state parameter. Possible CSRF attack.';
echo '<br><br><a href="' . APP_URL . '">Return to home page</a>';
exit;
}
// Clear the state parameter
unset($_SESSION['google_oauth_state']);
// Step 5: Check for authorization code
if (!isset($_GET['code'])) {
echo 'Authorization failed. No code received from Google.';
echo '<br><br><a href="' . APP_URL . '">Return to home page</a>';
exit;
}
$code = $_GET['code'];
// Step 6: Exchange authorization code for access token
$token_response = file_get_contents('https://oauth2.googleapis.com/token', false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-type: application/x-www-form-urlencoded',
'content' => http_build_query([
'code' => $code,
'client_id' => GOOGLE_CLIENT_ID,
'client_secret' => GOOGLE_CLIENT_SECRET,
'redirect_uri' => GOOGLE_REDIRECT_URI,
'grant_type' => 'authorization_code',
]),
],
]));
$token_data = json_decode($token_response, true);
$access_token = $token_data['access_token'] ?? null;
if ($access_token) {
// Step 7: Get user information from Google
$user_info = file_get_contents('https://www.googleapis.com/oauth2/v2/userinfo?access_token=' . urlencode($access_token));
$user = json_decode($user_info, true);
if (isset($user['email'])) {
$pdo = get_db_connection();
// Step 8: Check if user exists in database
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$user['email']]);
$existing_user = $stmt->fetch();
if ($existing_user) {
// Step 9a: User exists - log them in
$_SESSION['user_id'] = $existing_user['id'];
$_SESSION['email'] = $existing_user['email'];
$_SESSION['username'] = $existing_user['username'];
// Update Google ID if not set
if (empty($existing_user['google_id'])) {
$stmt = $pdo->prepare("UPDATE users SET google_id = ? WHERE id = ?");
$stmt->execute([$user['id'], $existing_user['id']]);
}
} else {
// Step 9b: User doesn't exist - create new account
$username = slugify($user['name'] ?? 'user');
// Ensure username is unique
$original_username = $username;
$counter = 1;
while (true) {
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ?");
$stmt->execute([$username]);
if (!$stmt->fetch()) {
break;
}
$username = $original_username . $counter;
$counter++;
}
// Insert new user
$stmt = $pdo->prepare("
INSERT INTO users (email, username, google_id, auth_provider, profile_picture, profile_public)
VALUES (?, ?, ?, 'google', ?, 1)
");
$stmt->execute([
$user['email'],
$username,
$user['id'],
$user['picture'] ?? null
]);
$user_id = $pdo->lastInsertId();
$_SESSION['user_id'] = $user_id;
$_SESSION['email'] = $user['email'];
$_SESSION['username'] = $username;
}
// Step 10: Redirect to success page
header('Location: ' . APP_URL);
exit;
} else {
echo 'Failed to get user information from Google.';
echo '<br><br><a href="' . APP_URL . '">Return to home page</a>';
exit;
}
} else {
echo 'Failed to get access token from Google.';
echo '<br><br><a href="' . APP_URL . '">Return to home page</a>';
exit;
}
break;
default:
echo 'Invalid action specified.';
echo '<br><br><a href="' . APP_URL . '">Return to home page</a>';
exit;
}
} catch (Exception $e) {
echo 'Authentication error: ' . $e->getMessage();
echo '<br><br><a href="' . APP_URL . '">Return to home page</a>';
}