A complete guide to integrating Google Authenticator using AuthenticatorAPI.com
This guide walks through adding Google Authenticator-compatible two-factor authentication to any PHP application — from a simple login form to a full Laravel or Symfony project. No libraries to install. No Composer packages required. Just PHP and HTTP.
curl extension enabled (standard on most hosts). A MySQL or similar database to store user secret codes.When a user enables 2FA, generate a cryptographically random Base32-encoded secret and store it against their account in your database. The secret should be at least 16 characters of Base32 (A–Z and 2–7).
// Generate a random Base32 secret (160-bit) function generate_2fa_secret(): string { $base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; $secret = ''; for ($i = 0; $i < 32; $i++) { $secret .= $base32Chars[random_int(0, 31)]; } return $secret; } $secret = generate_2fa_secret(); // Example: 'JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP' // Store encrypted in your database $encryptedSecret = openssl_encrypt($secret, 'AES-256-CBC', $_ENV['ENCRYPTION_KEY'], 0, $iv); // Save $encryptedSecret and $iv to your users table
Call the Pair endpoint and embed the returned QR code image in your 2FA setup page. The user will scan this with their Google Authenticator app.
function get_qr_code_html(string $appName, string $userEmail, string $secret): string { $url = 'https://www.authenticatorApi.com/pair.aspx?' . http_build_query([ 'AppName' => $appName, 'AppInfo' => $userEmail, 'SecretCode' => $secret, ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, ]); $html = curl_exec($ch); curl_close($ch); return $html; // Returns an <img> tag with the QR code } // In your setup page template: echo get_qr_code_html('MyApp', $_SESSION['user_email'], $secret);
After the user enters their password, prompt them for the 6-digit code from their app. Validate it against the stored secret using the Validate endpoint.
function validate_totp_pin(string $pin, string $secret): bool { // Basic input sanity check if (!ctype_digit($pin) || strlen($pin) !== 6) { return false; } $url = 'https://www.authenticatorApi.com/Validate.aspx?' . http_build_query([ 'Pin' => $pin, 'SecretCode' => $secret, ]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, ]); $response = trim(curl_exec($ch)); curl_close($ch); return strcasecmp($response, 'true') === 0; } // In your login handler: $pin = $_POST['totp_pin'] ?? ''; $secret = get_user_secret($_SESSION['user_id']); // fetch & decrypt from DB if (validate_totp_pin($pin, $secret)) { $_SESSION['2fa_verified'] = true; header('Location: /dashboard'); } else { $error = 'Invalid code. Please try again.'; }
Add a middleware-style check to all pages that require full authentication. A user must have passed both their password and their TOTP check.
// auth_check.php — include at the top of protected pages session_start(); if (empty($_SESSION['user_id'])) { header('Location: /login'); exit; } if (user_has_2fa_enabled($_SESSION['user_id']) && empty($_SESSION['2fa_verified'])) { header('Location: /login/verify'); exit; }
The following is a minimal but complete single-file PHP example demonstrating the full enrolment and login flow.
<?php /** * minimal_2fa.php * A self-contained demo of 2FA enrolment and validation. * Not for production — no real session/DB persistence. */ session_start(); function http_get(string $url): string { $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>10]); $r = curl_exec($ch); curl_close($ch); return $r; } function random_base32(int $len = 32): string { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; return implode('', array_map(fn() => $chars[random_int(0,31)], range(1,$len))); } // --- Enrol --- if ($_SERVER['REQUEST_METHOD'] === 'GET' && !isset($_SESSION['secret'])) { $_SESSION['secret'] = random_base32(); } $secret = $_SESSION['secret']; // --- Validate --- $message = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $pin = preg_replace('/\D/', '', $_POST['pin'] ?? ''); $resp = trim(http_get("https://www.authenticatorApi.com/Validate.aspx?Pin=$pin&SecretCode=$secret")); $message = strcasecmp($resp, 'true') === 0 ? '<div class="alert alert-success">✓ Code accepted!</div>' : '<div class="alert alert-danger">✗ Invalid code. Try again.</div>'; } $qr = http_get("https://www.authenticatorApi.com/pair.aspx?AppName=Demo&AppInfo=user@example.com&SecretCode=$secret"); ?> <!-- HTML output --> <h2>Scan this QR code with Google Authenticator</h2> <?= $qr ?> <p>Or enter this secret manually: <code><?= htmlspecialchars($secret) ?></code></p> <form method="post"> <input type="text" name="pin" placeholder="6-digit code" maxlength="6"> <button type="submit">Verify</button> </form> <?= $message ?>
In a Laravel application the pattern is the same — generate, store, display, validate — but you use Laravel's HTTP client and session helpers instead of raw curl.
// In your TwoFactorController use Illuminate\Support\Facades\Http; public function enroll(Request $request) { $secret = strtoupper(base_convert(bin2hex(random_bytes(20)), 16, 32)); $secret = substr($secret, 0, 32); $request->user()->update(['totp_secret' => encrypt($secret)]); $qr = Http::get('https://www.authenticatorApi.com/pair.aspx', [ 'AppName' => config('app.name'), 'AppInfo' => $request->user()->email, 'SecretCode' => $secret, ])->body(); return view('auth.2fa-setup', ['qr' => $qr, 'secret' => $secret]); } public function verify(Request $request) { $secret = decrypt($request->user()->totp_secret); $result = Http::get('https://www.authenticatorApi.com/Validate.aspx', [ 'Pin' => $request->input('pin'), 'SecretCode' => $secret, ])->body(); if (strtolower(trim($result)) === 'true') { $request->session()->put('2fa_verified', true); return redirect()->intended('/dashboard'); } return back()->withErrors(['pin' => 'Invalid code, please try again.']); }
openssl_encrypt or Laravel's encrypt())session_regenerate_id(true))random_int() or random_bytes()