Google OAuth2
Authorization Code Exchange for web and mobile — Google setup, API reference, and frontend examples.
Overview
Google login is optional and controlled by GOOGLE_AUTH_ENABLED. When disabled, Google endpoints return 503 Service Unavailable. JWTs are never placed in the redirect URL; the API issues a one-time exchangeCode (Redis, 60 seconds).
- • Feature-flagged via environment
- • Passport Google strategy + GoogleAuthGuard
- • Redirect allowlist prevents open redirects
- • Same token response shape as POST /auth/login
Google Cloud Console
Create (or edit) an OAuth 2.0 Client ID of type Web application. The authorized redirect URI must match GOOGLE_CALLBACK_URL exactly.
1. Open credentials
- Go to Google Cloud Console and select your project (top bar)
- In the left menu: APIs & Services → Credentials
- Edit an existing OAuth client (pencil icon), or Create Credentials → OAuth client ID (Application type: Web application)
2. Configure URLs
On the Web application settings page, fill the link sections as follows (use your API host and port).
Authorized JavaScript origins
Base URL of this Nest API, including the port — no path.
http://localhost:3000
Authorized redirect URIs
Exact callback route on this API (same value as GOOGLE_CALLBACK_URL). Do not use your SPA path here.
http://localhost:3000/api/v1/auth/google/callback
After saving, copy Client ID and Client Secret into your .env. Frontend post-login URLs belong in GOOGLE_REDIRECT_ALLOWLIST, not in Google’s redirect URI list.
Environment variables
Loaded from .env via src/config/google-oauth.config.ts (namespace googleOAuth on ConfigService). See .env.example.
GOOGLE_AUTH_ENABLED=true GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=your-client-secret GOOGLE_CALLBACK_URL=http://localhost:3000/api/v1/auth/google/callback GOOGLE_REDIRECT_ALLOWLIST=myapp://success,http://localhost:5173/auth/callback GOOGLE_OAUTH_DEFAULT_ROLES=
Default roles for new Google users
New Google users receive roles: [] by default (same as password register). Set GOOGLE_OAUTH_DEFAULT_ROLES in .env to a comma-separated list of Role values (e.g. user or user,manager). Existing users linked by email keep their current roles.
Sequence
- Client opens GET /api/v1/auth/google?redirect=<allowlisted URL>
- User consents on Google; browser hits GET /api/v1/auth/google/callback
- API upserts user, stores exchangeCode in Redis, redirects to redirect?code=...
- Client POSTs { code } to /api/v1/auth/exchange and receives JWTs
API reference
1. Start OAuth
Opens Google consent. Query redirect must match a prefix in GOOGLE_REDIRECT_ALLOWLIST (or omit to use the first allowlist entry).
GET /api/v1/auth/google?redirect=http%3A%2F%2Flocalhost%3A5173%2Fauth%2Fcallback
400 invalid redirect · 503 feature disabled
2. Callback
Handled by the API. Redirects to your app with ?code= (or &code= if the URL already has a query).
GET /api/v1/auth/google/callback?code=GOOGLE_AUTH_CODE&state=... → 302 Location: http://localhost:5173/auth/callback?code=EXCHANGE_CODE
3. Exchange code
Public endpoint. Consumes the code (single use) and returns the same shape as login.
POST /api/v1/auth/exchange
Content-Type: application/json
{
"code": "EXCHANGE_CODE"
}{
"user": {
"id": "...",
"email": "[email protected]",
"firstName": "Jane",
"lastName": "Doe",
"roles": [],
"googleId": "1082...",
"isActive": true
},
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}401 invalid/expired/used code · 503 feature disabled
Frontend — Web (SPA)
Typical flow: button starts OAuth in the same window (or popup); a callback route on your frontend origin reads code from the query string and calls exchange.
const API = 'http://localhost:3000/api/v1';
const redirect = encodeURIComponent(
'http://localhost:5173/auth/callback'
);
// Full-page redirect to Nest → Google → back to SPA
window.location.href = `${API}/auth/google?redirect=${redirect}`;// /auth/callback on the SPA
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
if (!code) throw new Error('Missing exchange code');
const res = await fetch('http://localhost:3000/api/v1/auth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
if (!res.ok) throw new Error(await res.text());
const { user, accessToken, refreshToken } = await res.json();
// persist tokens as your app requires
sessionStorage.setItem('accessToken', accessToken);
console.log('signed in', user.email);- • Add your frontend callback URL to GOOGLE_REDIRECT_ALLOWLIST (e.g. http://localhost:5173/auth/callback)
- • GOOGLE_CALLBACK_URL must point at the Nest API, not the SPA
- • Store accessToken / refreshToken securely (memory + httpOnly cookie patterns as you prefer)
Frontend — Mobile (deep link)
Use a custom scheme (or universal link) in the allowlist. After Google, the API redirects to myapp://success?code=.... The app opens, parses the code, and calls exchange.
import * as Linking from 'expo-linking';
import * as WebBrowser from 'expo-web-browser';
const API = 'https://api.example.com/api/v1';
const redirect = encodeURIComponent('myapp://success');
async function signInWithGoogle() {
const url = `${API}/auth/google?redirect=${redirect}`;
await WebBrowser.openAuthSessionAsync(url, 'myapp://success');
}import * as Linking from 'expo-linking';
async function exchangeFromUrl(url) {
const { queryParams } = Linking.parse(url);
const code = queryParams?.code;
if (!code) return;
const res = await fetch(`${API}/auth/exchange`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code }),
});
const data = await res.json();
// data.accessToken, data.refreshToken, data.user
return data;
}
Linking.addEventListener('url', ({ url }) => {
exchangeFromUrl(url);
});- • Register the scheme in the OS / Expo app.json
- • Prefer Linking.addEventListener('url', ...) and getInitialURL()
- • Exchange over HTTPS against your API base URL
Security notes
- • Redirect URLs must match GOOGLE_REDIRECT_ALLOWLIST prefixes
- • Exchange codes are single-use and expire in 60 seconds (Redis)
- • Existing local users are linked by verified Google email (googleId set; roles unchanged)
- • Google-only users have password null — password login / change-password will fail until a password is set
User model
Postgres entity / interface fields used by Google login:
- • googleId — unique nullable Google subject ID
- • password — nullable for OAuth-only accounts
- • isActive — set true for new Google users (email verified by Google)
- • roles — from GOOGLE_OAUTH_DEFAULT_ROLES env (default [])
Related
- Social Authentication hub
- Facebook OAuth2 guide
- X / Twitter OAuth2 guide
- GitHub OAuth2 guide
- Figma OAuth2 guide
- LinkedIn OpenID Connect guide
- Slack OpenID Connect guide
- Atlassian OAuth 2.0 (3LO) guide
- GitLab OAuth2 guide
- Bitbucket OAuth2 guide
- Discord OAuth2 guide
- Twitch OAuth2 guide
- Steam OpenID 2.0 guide
- Reddit OAuth2 guide
- Auth & JWT