Google

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

  1. Go to Google Cloud Console and select your project (top bar)
  2. In the left menu: APIs & Services → Credentials
  3. 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.

Examplelocal API
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.

ExampleAPI callback
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.

Environment.env
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

  1. Client opens GET /api/v1/auth/google?redirect=<allowlisted URL>
  2. User consents on Google; browser hits GET /api/v1/auth/google/callback
  3. API upserts user, stores exchangeCode in Redis, redirects to redirect?code=...
  4. 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).

HTTPGET /api/v1/auth/google
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).

HTTPGET /api/v1/auth/google/callback
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.

HTTP RequestPOST /api/v1/auth/exchange
POST /api/v1/auth/exchange
Content-Type: application/json

{
  "code": "EXCHANGE_CODE"
}
HTTP Response200 OK
{
  "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.

JavaScriptStart Google login
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}`;
JavaScriptCallback page /auth/callback
// /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.

React NativeOpen OAuth in browser
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');
}
React NativeHandle deep link + exchange
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