"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.
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
| Term | What it means |
|---|---|
| Authorisation server | Google. It signs the user in and issues codes and tokens |
| Client | Your web application |
| Client ID | Public identifier for your app. Safe to appear in the browser |
| Client secret | Private. Used only from your server to Google, never in front-end code |
| Scope | What you ask for. openid email profile is enough for sign-in |
| Authorisation code | A short-lived, single-use code Google sends to your callback URL |
| ID token | A signed JWT describing the user. Sign-in relies on this |
| Access token | Lets you call Google APIs as the user. Not needed for sign-in alone |
2. How the flow works
- The user clicks "Sign in with Google".Your server creates a random
statevalue, stores it in the session and redirects the browser to Google. - The user signs in at Googleand approves the requested scopes.
- Google redirects backto your callback URL with
?code=…&state=…. - Your server checks the state,then sends the code, client ID and client secret to Google's token endpoint and receives an ID token.
- 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.
- Create or choose a projectat console.cloud.google.com.
- Branding.Enter the app name, a support email, your home page, your privacy policy link and your authorised domain, such as
yourdomain.com. - 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.
- Data access.Add the
openid,emailandprofilescopes. These are non-sensitive, so sign-in apps don't need Google's full scope review. - Clients.Create an OAuth client of type Web application and add each redirect URI exactly, for example
http://localhost:3000/auth/google/callbackfor development andhttps://yourdomain.com/auth/google/callbackfor production. - Store the client ID and secretas 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:
composer require google/apiclientThe 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
// /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
// /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).
npm install express express-session passport passport-google-oauth20// 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); }
});// 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
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_verifiedis 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.
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 requireon your own computer or in CI and upload the project with itsvendor/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.
Shared hosting with free SSL, PHP version selection and a Node.js app tool, managed from one control panel.
See cPanel hosting