What you're doing and why it's painful
Google OAuth 2.0 lets users sign in to your app with their Google account. Your app gets a verified email address and basic profile — no password to store, no email verification to build.
The pain: Google's Cloud Console has changed its UI many times and the steps are buried under multiple menus. The OAuth consent screen must be configured before credentials work. Redirect URIs must match exactly — a trailing slash difference causes a cryptic error. And test mode limits which Google accounts can sign in until you publish the app, which requires a review for certain scopes.
Prerequisites
- A Google account
- Your app's redirect URI (the URL Google sends the user back to after sign-in) — e.g.
https://yourdomain.com/api/auth/callback/google
Step 1 — Create a Google Cloud project
- Go to Google Cloud Console
- Click the project selector at the top → New Project
- Name it after your app — e.g.
My App - Click Create
- Make sure the new project is selected in the dropdown
Step 2 — Enable the OAuth APIs
- Go to APIs & Services → Library
- Search for
Google+ APIorGoogle Identity— for basic OAuth (email + profile), you don't need to enable anything extra. The OAuth API is on by default.
Step 3 — Configure the OAuth consent screen
This is the screen users see when they sign in. It must be configured before you can create credentials.
- Go to APIs & Services → OAuth consent screen
- Choose External (for apps that can be used by any Google account)
- Click Create
- Fill in the required fields:
- App name: what users see on the consent screen — e.g.
My App - User support email: your email
- Developer contact email: your email
- App name: what users see on the consent screen — e.g.
- Click Save and Continue
Scopes: On the next screen, click Add or remove scopes and add:
openidemailprofile
These three are the minimum for a standard "Sign in with Google" flow.
- Click Save and Continue through the remaining screens
- Click Back to Dashboard
Step 4 — Create OAuth credentials
- Go to APIs & Services → Credentials
- Click + Create Credentials → OAuth client ID
- Application type: Web application
- Name: e.g.
My App Web Client - Under Authorized redirect URIs, click Add URI and enter your callback URL exactly:
https://yourdomain.com/api/auth/callback/google - Click Create
Google shows you a Client ID and Client Secret. Copy both — the secret is only shown once.
Step 5 — Add credentials to your environment
GOOGLE_CLIENT_ID=1234567890-abc...apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-...
Step 6 — Implement the OAuth flow in your server
The standard OAuth 2.0 flow: redirect user to Google → user approves → Google redirects back with a code → exchange code for tokens → get user info.
import express from 'express';
const router = express.Router();
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo';
// Step 1: Redirect to Google
router.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
redirect_uri: 'https://yourdomain.com/api/auth/callback/google',
response_type: 'code',
scope: 'openid email profile',
access_type: 'offline',
prompt: 'consent',
});
res.redirect(`${GOOGLE_AUTH_URL}?${params}`);
});
// Step 2: Handle callback
router.get('/auth/callback/google', async (req, res) => {
const { code } = req.query;
// Exchange code for tokens
const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code: code as string,
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
redirect_uri: 'https://yourdomain.com/api/auth/callback/google',
grant_type: 'authorization_code',
}),
});
const tokens = await tokenRes.json();
// Get user info
const userRes = await fetch(GOOGLE_USERINFO_URL, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
const user = await userRes.json();
// user.email, user.name, user.picture, user.sub (Google user ID)
// Create or find the user in your database, then set session
res.redirect('/dashboard');
});
Test mode vs published
While your app is in Testing mode (the default), only Google accounts you've added as Test users can sign in. Anyone else sees "This app is blocked."
To add test users:
- Go to OAuth consent screen
- Scroll to Test users
- Click Add users and add the Google email addresses that should have access
To allow any Google account to sign in, publish your app:
- Click Publish App on the OAuth consent screen
- For basic scopes (email, profile, openid), Google approves this automatically
- For sensitive scopes (like Google Drive access), you'll need to complete a verification review
Common errors and gotchas
redirect_uri_mismatch
The redirect URI in your code doesn't exactly match what you registered in Google Cloud Console. Check for:
httpvshttps- Trailing slash (
/callbackvs/callback/) - Port number differences
- Localhost vs production domain
The match must be exact, character for character.
This app is blocked for users
Your app is in Testing mode and the user isn't added as a test user. Either add them or publish the app.
Access blocked: Authorization Error
Your OAuth consent screen isn't configured. Complete Step 3 first.
Getting invalid_client error
Your Client ID or Secret is wrong. Double-check by re-copying from the Credentials page. Common issue: accidentally copying the Client ID instead of the Secret.
Consent screen asking for too many permissions You're requesting scopes you didn't declare on the consent screen configuration. Match the scopes in your redirect URL to the scopes you added in Step 3.