Complete Guide to Google OAuth 2.0 Authentication in PHP

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.

Google Authentication Guide

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

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

  1. Go to the Google Cloud Console
  2. Click “Select a project” → “New Project”
  3. Enter a project name and click “Create”

Step 2: Enable Google+ API

  1. In your project, go to “APIs & Services” → “Library”
  2. Search for “Google+ API” and click on it
  3. Click “Enable”

Step 3: Create OAuth 2.0 Credentials

  1. Go to “APIs & Services” → “Credentials”
  2. Click “Create Credentials” → “OAuth client ID”
  3. 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
  1. For application type, choose “Web application”
  2. 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
  1. 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:

  1. Authorization Request: User clicks login button → redirect to Google
  2. User Consent: User grants permission on Google’s site
  3. Authorization Code: Google redirects back with a temporary code
  4. Token Exchange: Your server exchanges the code for an access token
  5. User Info: Use the access token to fetch user information
  6. 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

  1. “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 vs https)
  1. “Invalid state parameter”
  • Session might be expiring too quickly
  • Check if sessions are working properly on your server
  1. “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>';
}

on in Blog, Featured | Last modified on

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *