Third-Party API Integrations

Login with Google (OAuth 2.0) in PHP and Node.js

By Domain India Team · DomainIndia SupportPublished 11 min read
Knowledge base article
Contents (11 sections)

"Sign in with Google" lets visitors create an account on your website with the Google account they already have, so there is no registration form to fill in and no new password to forget. This guide walks through the OAuth 2.0 authorisation-code flow with OpenID Connect: setting up the Google Cloud console, sending the user to Google, verifying the ID token on your server and creating or updating the user, with working code in PHP and Node.js.

Key takeaways

Create a web client in the Google Cloud console and register your exact HTTPS callback URL. Send the user to Google with a random state value, and on the callback check the state, exchange the code for tokens on your server, and verify the ID token's signature before you trust it. Key your users by Google's sub claim, not by email, and never put the client secret in browser code.

1. OAuth 2.0 terms in two minutes

TermWhat it means
Authorisation serverGoogle. It signs the user in and issues codes and tokens
ClientYour web application
Client IDPublic identifier for your app. Safe to appear in the browser
Client secretPrivate. Used only from your server to Google, never in front-end code
ScopeWhat you ask for. openid email profile is enough for sign-in
Authorisation codeA short-lived, single-use code Google sends to your callback URL
ID tokenA signed JWT describing the user. Sign-in relies on this
Access tokenLets you call Google APIs as the user. Not needed for sign-in alone

2. How the flow works

  1. The user clicks "Sign in with Google".
    Your server creates a random state value, stores it in the session and redirects the browser to Google.
  2. The user signs in at Google
    and approves the requested scopes.
  3. Google redirects back
    to your callback URL with ?code=…&state=….
  4. Your server checks the state,
    then sends the code, client ID and client secret to Google's token endpoint and receives an ID token.
  5. Your server verifies the ID token,
    finds or creates the user, starts a session and redirects to your app.

The same flow runs on every sign-in, including returning users.

3. Set up the Google Cloud console

Google moved the OAuth settings into the Google Auth Platform section of the console, split into Branding, Audience, Clients and Data access. Labels change from time to time, so follow the names on screen.

  1. Create or choose a project
    at console.cloud.google.com.
  2. Branding.
    Enter the app name, a support email, your home page, your privacy policy link and your authorised domain, such as yourdomain.com.
  3. Audience.
    Choose External. While the app is in Testing, only the test users you add can sign in, and Google limits it to 100 test users.
  4. Data access.
    Add the openid, email and profile scopes. These are non-sensitive, so sign-in apps don't need Google's full scope review.
  5. Clients.
    Create an OAuth client of type Web application and add each redirect URI exactly, for example http://localhost:3000/auth/google/callback for development and https://yourdomain.com/auth/google/callback for production.
  6. Store the client ID and secret
    as environment variables or in a config file outside your web root, never in your repository.

When you are ready for real users, publish the app to production. Google may ask you to verify your brand, such as proving you own the domain, before your app name and logo appear on the consent screen. For keeping secrets out of code, see environment variables and secrets management.

4. PHP implementation

Install Google's official client library with Composer:

bash
composer require google/apiclient

The library ships with every Google API service. For sign-in you need none of them, so you can keep the upload small by listing only the services you use under extra.google/apiclient-services in composer.json and adding Google's Google\Task\Composer::cleanup script, as described in the library's README.

Step 1: redirect to Google

php
<?php
// /auth/google
require __DIR__ . '/../vendor/autoload.php';

session_set_cookie_params(['secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
session_start();

$client = new Google\Client();
$client->setClientId(getenv('GOOGLE_CLIENT_ID'));
$client->setClientSecret(getenv('GOOGLE_CLIENT_SECRET'));
$client->setRedirectUri(getenv('GOOGLE_REDIRECT_URI'));
$client->setScopes(['openid', 'email', 'profile']);

$state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state;
$client->setState($state);

header('Location: ' . $client->createAuthUrl());
exit;

Use SameSite=Lax, not Strict: with Strict, the browser drops your session cookie on the redirect back from Google and the state check fails.

Step 2: handle the callback

php
<?php
// /auth/google/callback
require __DIR__ . '/../vendor/autoload.php';

session_set_cookie_params(['secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
session_start();

$expected = $_SESSION['oauth_state'] ?? '';
unset($_SESSION['oauth_state']);
if ($expected === '' || !hash_equals($expected, $_GET['state'] ?? '')) {
    http_response_code(400);
    exit('Invalid state');
}
if (empty($_GET['code'])) {
    http_response_code(400);
    exit('Missing code');
}

$client = new Google\Client();
$client->setClientId(getenv('GOOGLE_CLIENT_ID'));
$client->setClientSecret(getenv('GOOGLE_CLIENT_SECRET'));
$client->setRedirectUri(getenv('GOOGLE_REDIRECT_URI'));

try {
    $token = $client->fetchAccessTokenWithAuthCode($_GET['code']);
    if (isset($token['error'])) {
        throw new RuntimeException($token['error_description'] ?? 'Token exchange failed');
    }

    // Checks the signature, issuer, audience and expiry
    $payload = $client->verifyIdToken($token['id_token'] ?? '');
    if (!$payload || empty($payload['email_verified'])) {
        throw new RuntimeException('Invalid ID token or unverified email');
    }

    $user = findOrCreateUserByGoogleSub(
        $payload['sub'],
        $payload['email'],
        $payload['name'] ?? '',
        $payload['picture'] ?? null
    );

    session_regenerate_id(true);   // new session ID after login
    $_SESSION['user_id'] = $user->id;
    header('Location: /dashboard');
    exit;
} catch (Throwable $e) {
    error_log('Google sign-in failed: ' . $e->getMessage());
    header('Location: /login?error=oauth');
    exit;
}

findOrCreateUserByGoogleSub() looks the user up by sub first. If there is no match, it creates a new user, or links an existing account as described in section 7.

5. Node.js implementation with Passport

Passport is the common authentication middleware for Express, and passport-google-oauth20 handles the Google flow. The example uses ES modules ("type": "module" in package.json).

bash
npm install express express-session passport passport-google-oauth20
javascript
// auth.js
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
import { findOrCreateUserByGoogleSub, findUserById } from './users.js';

passport.use(new GoogleStrategy(
  {
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: process.env.GOOGLE_CALLBACK_URL,
    scope: ['openid', 'email', 'profile'],
    state: true,   // random state stored in the session and checked on return
    pkce: true,    // adds a PKCE code challenge (requires state)
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      const email = profile.emails?.[0];
      if (!email?.verified) return done(null, false);
      const user = await findOrCreateUserByGoogleSub(
        profile.id, email.value, profile.displayName, profile.photos?.[0]?.value);
      done(null, user);
    } catch (err) {
      done(err);
    }
  }
));

passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
  try { done(null, await findUserById(id)); } catch (err) { done(err); }
});
javascript
// app.js
import express from 'express';
import session from 'express-session';
import passport from 'passport';
import './auth.js';

const app = express();
app.set('trust proxy', 1);   // needed for secure cookies behind a proxy

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { secure: true, httpOnly: true, sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000 },
}));
app.use(passport.initialize());
app.use(passport.session());

app.get('/auth/google', passport.authenticate('google'));
app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login?error=oauth' }),
  (req, res) => res.redirect('/dashboard'));

app.post('/logout', (req, res, next) => {
  req.logout(err => (err ? next(err) : req.session.destroy(() => res.redirect('/'))));
});

app.listen(process.env.PORT || 3000);

Current Passport versions regenerate the session on login, which protects against session fixation. In production, use a persistent session store rather than the default in-memory store.

6. Security essentials

Verify the ID token
Never just base64-decode it. The libraries check Google's signature, the issuer, the audience (your client ID) and the expiry for you.
Always use state
A random state value tied to the session stops attackers from logging users into the attacker's account. PKCE adds a second layer, and current OAuth security guidance (RFC 9700) recommends it for server-side apps too.
HTTPS only
Google accepts plain http redirect URIs only for localhost. Production callbacks must use HTTPS.
Minimal scopes
Ask only for openid, email and profile. Extra scopes such as Drive access alarm users on the consent screen and may need Google review.
The client secret never goes in the browser

Anyone who can read your JavaScript can copy a secret placed there and impersonate your app. Keep it in server-side configuration outside your web root, and rotate it in the console if it ever leaks.

7. Linking accounts and storing users

  • Key users by sub. It is Google's stable user ID and never changes. Email addresses can change, so store email as a lookup and display field only.
  • Require email_verified. Never link or create an account from an unverified address.
  • Existing account with the same email. The safest option asks the user to sign in with their password once to link Google. Automatic linking is less work for the user, but do it only when email_verified is true.
  • Keep a password or other login as a fallback, so users can still get in if they lose access to their Google account.
  • Discard the access token after sign-in unless you really call Google APIs for the user.
sql
CREATE TABLE users (
    id             BIGSERIAL PRIMARY KEY,
    google_sub     VARCHAR(255) UNIQUE,        -- NULL for password-only users
    email          VARCHAR(255) NOT NULL,
    email_verified BOOLEAN NOT NULL DEFAULT FALSE,
    name           VARCHAR(255),
    picture        TEXT,
    password_hash  VARCHAR(255),               -- NULL for Google-only users
    created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    last_login_at  TIMESTAMPTZ
);
CREATE INDEX idx_users_email ON users (email);

The UNIQUE constraint already creates an index on google_sub, so it needs no separate one. For MySQL, use BIGINT AUTO_INCREMENT and DATETIME.

8. Running this on Domain India

  • Shared hosting and PHP. Composer can't run on Domain India shared hosting, even over jailed SSH, because the process functions it needs are disabled. Run composer require on your own computer or in CI and upload the project with its vendor/ folder. On our cPanel servers the library can reach Google over HTTPS with PHP's curl (measured). On DirectAdmin, curl functions are disabled for most sites, so test the whole sign-in flow on your plan before launch. See PHP disabled functions on shared hosting.
  • HTTPS. Free SSL is included with shared hosting: AutoSSL with Let's Encrypt on cPanel and Let's Encrypt on DirectAdmin. See free SSL with Let's Encrypt.
  • Node.js on cPanel. The Setup Node.js App tool offers Node.js 20, 22 and 24. Choose 22 or 24, because Node.js 20 has reached end of life. See deploying a Node.js app on shared hosting.
  • App Platform. Node.js apps are detected automatically, and you set secrets as environment variables. See getting started with the App Platform.

Frequently asked questions

How do I test Google sign-in on my own computer?

Add http://localhost:3000/auth/google/callback, or your local port, as an authorised redirect URI on your web client in the Google Cloud console. Keep the app in Testing and add your own Google account as a test user. Google allows plain http only for localhost.

Do I need Google's app verification for sign-in?

Apps that request only the openid, email and profile scopes don't need Google's full scope review. When you publish to production, Google may still ask you to verify your brand, such as your domain, home page and privacy policy, before your app name and logo appear on the consent screen.

Should I identify users by email or by the sub claim?

By the sub claim. It is Google's permanent ID for the account, while the email address can change. Store the email for display and lookups, and accept it only when email_verified is true.

Should signing out of my site also sign the user out of Google?

No. Ending the Google session would also sign the user out of Gmail and other Google services. Destroy your own session only; the user manages their Google session separately.

What is Google One Tap?

One Tap is a prompt from Google Identity Services that lets a signed-in Google user sign in to your site without a full redirect. It returns an ID token that your server verifies in the same way as in this guide, so add it once the basic flow works.

Can I use this flow in a mobile app?

Not as written. Mobile apps should use the platform sign-in SDKs or the OAuth flow for installed apps with PKCE, and should never contain a client secret. The web-application client in this guide is for server-side web apps.

Can I run this on Domain India shared hosting?

Yes, with HTTPS from the free SSL included with hosting. Composer doesn't run on shared hosting, so install the PHP library on your own computer and upload the vendor folder. For Node.js, use the cPanel Setup Node.js App tool with Node.js 22 or 24, or the App Platform.

Ready to add Google sign-in? Host a PHP site on cPanel hosting, run a Node.js app on the App Platform, or read the MERN OAuth 2.0 guide for a React front end.

Host your app with free SSL

Shared hosting with free SSL, PHP version selection and a Node.js app tool, managed from one control panel.

See cPanel hosting

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app