Skip to main content
Being Idea Innovations
Facebook Login with PHP and MySQL: A Secure 2026 Implementation
Back to Blog

Facebook Login with PHP and MySQL: A Secure 2026 Implementation

1 August 20186 min readPHP
Share:

"Log in with Facebook" is a useful signup shortcut, and the code that circulates for it — including the earlier version of this article — is a catalogue of things that no longer work and things that were never safe. It uses a deleted SDK, a removed permission, and interpolates a value straight into SQL.

This guide covers Facebook Login as it works in 2026: the server-side Authorization Code flow, done securely.

What the old approach got wrong

The widely copied version has, in one file, all of these:

  • SQL injection. "SELECT ... WHERE fbid=$uid" and "INSERT ... VALUES ($uid, '$fullname', '$email')" drop user-controlled values straight into queries.
  • A removed permission. publish_actions was deleted in 2018. Requesting it now fails app review outright.
  • Deleted SDKs. all.js and the old new Facebook([...]) PHP SDK no longer exist. SDK v2.2 expired years ago.
  • No CSRF protection. No state parameter, so the login callback can be forged.
  • Trusting the client. The browser tells the server "I'm logged in" and the server believes it.
  • latin1 tables that cannot store many real names.

The fix is not to patch these one by one. The whole flow moved server-side, for good reasons.

Facebook Login is OpenID Connect

Modern Facebook Login is an OAuth 2.0 / OpenID Connect flow. The pattern is identical to "Sign in with Google" — so if you have implemented one, the other is the same shape with different endpoints. The social login guide covers the full flow with PKCE; this focuses on the Facebook specifics.

Facebook Login authorization code flow between browser, your server and Facebook Browser Your server Facebook click “Log in with Facebook” redirect to Facebook with state + scope user approves → redirect back with code + state exchange code for token, fetch /me — server-side
The code-for-token exchange happens on your server with your App Secret. The browser never handles a token, and the server never trusts a client claim of "logged in".

Setup

  1. Create a Consumer app at developers.facebook.com/apps and add the Facebook Login product.
  2. Under Facebook Login → Settings, add your exact redirect URI to Valid OAuth Redirect URIs. It must match to the character.
  3. Keep credentials server-side, out of version control:
FB_APP_ID=your_app_id
FB_APP_SECRET=your_app_secret        # never in client code, never committed
FB_REDIRECT_URI=https://example.com/auth/facebook/callback

Step 1: send the user to Facebook

<?php
declare(strict_types=1);
session_start();

// Unguessable, single-use, stored in the session — this is the CSRF defence.
$_SESSION['fb_state'] = bin2hex(random_bytes(32));

$params = http_build_query([
    'client_id'     => $_ENV['FB_APP_ID'],
    'redirect_uri'  => $_ENV['FB_REDIRECT_URI'],
    'state'         => $_SESSION['fb_state'],
    // Request only what you need. email + public_profile need no app review.
    'scope'         => 'email,public_profile',
    'response_type' => 'code',
]);

header('Location: https://www.facebook.com/v21.0/dialog/oauth?' . $params);
exit;

Your login button is now a plain link to this script — no SDK, no JavaScript required:

<a class="btn-facebook" href="/auth/facebook/start">Log in with Facebook</a>

Request the minimum scope. Anything beyond email and public_profile triggers Facebook's App Review, which is a real process with real delays.

Step 2: handle the callback

<?php
declare(strict_types=1);
session_start();

// Reject anything whose state does not match — blocks CSRF and forged callbacks.
if (empty($_GET['state']) || !hash_equals($_SESSION['fb_state'] ?? '', $_GET['state'])) {
    http_response_code(400);
    exit('Invalid state.');
}
unset($_SESSION['fb_state']);   // single use

if (isset($_GET['error'])) {
    // User clicked "Cancel" — not an error, just send them back.
    header('Location: /login');
    exit;
}

$code = $_GET['code'] ?? '';
if ($code === '') {
    http_response_code(400);
    exit('Missing authorization code.');
}

// Exchange the code for an access token — server-side, with the app secret.
$tokenResponse = httpGetJson('https://graph.facebook.com/v21.0/oauth/access_token?' . http_build_query([
    'client_id'     => $_ENV['FB_APP_ID'],
    'client_secret' => $_ENV['FB_APP_SECRET'],
    'redirect_uri'  => $_ENV['FB_REDIRECT_URI'],
    'code'          => $code,
]));

$accessToken = $tokenResponse['access_token'] ?? null;
if ($accessToken === null) {
    http_response_code(502);
    exit('Could not obtain an access token.');
}

Step 3: verify the token before trusting it

An access token obtained legitimately for a different app must not be accepted by yours. Facebook's debug endpoint confirms which app a token belongs to:

$appToken = $_ENV['FB_APP_ID'] . '|' . $_ENV['FB_APP_SECRET'];

$debug = httpGetJson('https://graph.facebook.com/debug_token?' . http_build_query([
    'input_token'  => $accessToken,
    'access_token' => $appToken,
]));

if (($debug['data']['app_id'] ?? '') !== $_ENV['FB_APP_ID']
    || ($debug['data']['is_valid'] ?? false) !== true) {
    http_response_code(401);
    exit('Token failed validation.');
}

Skipping this check is the "confused deputy" vulnerability — your app accepting a token minted for someone else's.

Step 4: fetch the profile

$me = httpGetJson('https://graph.facebook.com/v21.0/me?' . http_build_query([
    'fields'       => 'id,name,email',
    'access_token' => $accessToken,
]));

$facebookId = $me['id']   ?? null;   // stable, app-scoped
$name       = $me['name'] ?? '';
$email      = $me['email'] ?? null;  // may be absent — see below

if ($facebookId === null) {
    http_response_code(502);
    exit('Could not read profile.');
}

Facebook may not return an email. Users who registered with a phone number, or who declined the email permission, have none. Your code must handle a null email rather than assuming one — the original crashed on exactly this.

Step 5: store the user safely

CREATE TABLE users (
  id          BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  facebook_id VARCHAR(64)  NULL,
  name        VARCHAR(150) NOT NULL,
  email       VARCHAR(255) NULL,
  created_at  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uniq_facebook_id (facebook_id),
  UNIQUE KEY uniq_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

// Look up by the Facebook ID — prepared statement, no interpolation.
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE facebook_id = ?');
$stmt->execute([$facebookId]);
$user = $stmt->fetch();

if ($user === false) {
    $stmt = $pdo->prepare(
        'INSERT INTO users (facebook_id, name, email, created_at) VALUES (?, ?, ?, NOW())'
    );
    $stmt->execute([$facebookId, $name, $email]);
    $userId = (int) $pdo->lastInsertId();
} else {
    $userId = (int) $user['id'];
}

// New session ID on login — prevents session fixation.
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;

header('Location: /dashboard');
exit;

Two things the original got dangerously wrong here. Every value goes through a prepared statementfbid=$uid was a textbook injection. And the lookup key is the Facebook ID, not the email.

The account-linking vulnerability

This is the one that turns social login into account takeover, and it is worth stating plainly.

The tempting shortcut is: match the returned email to an existing account and log that user in. But if an attacker can create a Facebook account using victim@example.com without proving they own it, they walk straight into the victim's account on your site.

The rules:

  • Key the identity on the Facebook ID (the (provider, subject) pair), never the email alone.
  • Do not auto-link to a password account by email without a verification step. When the emails match but no Facebook link exists yet, email the account holder a confirmation link before joining them.

A small HTTP helper

function httpGetJson(string $url): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    $body   = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($body === false || $status >= 400) {
        return [];
    }

    return json_decode($body, true, 512, JSON_THROW_ON_ERROR) ?? [];
}

Logging out and disconnection

Standard session teardown logs the user out of your site. Note that Facebook also requires a Data Deletion Callback — an endpoint users can trigger from their Facebook settings to have you erase their data. It is a condition of using Facebook Login, and apps get disabled for not providing one.

Summary

  • Use the server-side Authorization Code flow — no client-side SDK needed for login.
  • Always send and verify a state parameter.
  • Exchange the code for a token on the server, with the App Secret.
  • Verify the token belongs to your app via debug_token.
  • Request minimal scope; publish_actions is gone.
  • Handle a missing email gracefully.
  • Prepared statements everywhere — never interpolate the Facebook ID.
  • Key identity on the Facebook ID, and never auto-link by unverified email.
  • Use utf8mb4, and provide a data deletion callback.
Share:

Written by

Amit Verma

Founder / Senior Software Engineer

Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.

More articles by Amit

Want to talk tech?

We ship software that scales. Let's work together.

No long-term contracts
Senior engineers only
US · AU · NZ timezone coverage
14-day trial on retainers