---
title: Google OAuth Flow
tags: [flow, auth, oauth, google]
related_models: ["[[User]]"]
related_apis: ["POST /api/auth/google/signup", "POST /api/auth/google/signin"]
---

# Google OAuth Flow

> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))

Google sign-in/up integration using **Google Identity Services** (`accounts.google.com/gsi/client`). The flow short-circuits email verification because Google accounts are pre-verified.

## Actors

- **User** with a Google account.
- **Google Identity Services (GSI)** — JS SDK loaded on demand by the frontend service.
- **Frontend** — `frontend/src/auth/services/google-oauth.ts`, consumed by `frontend/src/auth/view/jwt/jwt-sign-in-view.tsx` and `jwt-sign-up-view.tsx`.
- **Backend** — `AuthController.googleSignUp` and `googleSignIn` in `backend/src/services/auth/authController.ts`, backed by `googleOAuthService.verifyGoogleToken()` in `backend/src/services/auth/googleOAuthService.ts`.
- **MongoDB** — `User` collection (account linking by email).

## Preconditions

- `NEXT_PUBLIC_GOOGLE_CLIENT_ID` is set on the frontend (`frontend/src/auth/services/google-oauth.ts` line 2 — there is a hard-coded fallback for the dev project ID).
- Same client ID is whitelisted on the backend `googleOAuthService`.
- The current origin is registered under "Authorized JavaScript origins" in Google Cloud Console (see `frontend/GOOGLE_OAUTH_SETUP.md`).

## Step-by-step narrative

### Sign-up

1. User clicks the Google icon on `/auth/jwt/sign-up`. The form is configured with the chosen role (buyer/seller) and an optional referral code.
2. The frontend lazy-loads `https://accounts.google.com/gsi/client` if it is not yet on `window.google`.
3. `google.accounts.oauth2.initTokenClient({ client_id, scope: 'openid email profile' })` is initialised, and `.requestAccessToken()` opens the Google popup.
4. On success the popup returns an **ID token** (a Google-signed JWT containing `email`, `email_verified`, `name`, `given_name`, `family_name`, `picture`, `sub`).
5. Frontend calls `signUpWithGoogle({ googleToken, role, referralCode })` (`frontend/src/auth/context/jwt/action.ts:281-304`), which POSTs `POST /api/auth/google/signup`.
6. Backend `authController.googleSignUp` (`:781-872`) calls `googleOAuthService.verifyGoogleToken(googleToken)`. The verifier uses `google-auth-library` to validate the JWT signature, expiry, audience (`client_id`), and issuer.
7. **Duplicate check**: `User.findOne({ email: googleUser.email })` — if the email already exists, returns **`409 USER_EXISTS`** so the user can use *sign-in* instead.
8. **New user creation** with `password` omitted, `isEmailVerified: true`, `status: "active"`, `profile.avatar = googleUser.picture`, and the chosen `role` from the request.
9. **Referral attribution** (`authController.ts:817-838`): same logic as the email path — increment `referrer.referralStats.totalReferrals`, emit `referral-signup` on `user-${referrer._id}`.
10. Generate access + refresh tokens, push refresh into `user.refreshTokens[]`, respond with `{ user, tokens }`.
11. Frontend stores tokens in `localStorage` and redirects to the dashboard.

### Sign-in

1. User clicks the Google icon on `/auth/jwt/sign-in`.
2. Same GSI flow as sign-up — Google returns an ID token.
3. Frontend calls `signInWithGoogle(googleToken)` → `POST /api/auth/google/signin`.
4. Backend verifies the token, then looks up `User.findOne({ email: googleUser.email, status: "active" })` (`authController.ts:1194`). Note the **`status: "active"` filter**: the query only matches active accounts. If no active user matches, returns **`404 USER_NOT_FOUND`** ("please sign up first"). The frontend surfaces a localized prompt.
5. On hit: `existingUser.lastLoginAt = now`; if `profile.avatar` is empty and Google has a picture, it is back-filled (`authController.ts:905-907`).
6. Tokens issued and returned identically to email login.

> [!warning] No account merge
> There is **no** account-merge step between a Telegram-only / email account and a Google account. The Google sign-in path simply looks up an **active** user by email and reuses that document if one exists; it does not reconcile, link, or merge distinct identities. There is **no** separate `googleId` field stored today, so matching is a one-way trust on `googleUser.email`.

> [!warning] Soft-deleted accounts get a generic 404 on Google sign-in
> Because the sign-in lookup filters by `status: "active"`, a user who registered via Google and was later **soft-deleted** (`status: "deleted"`) is invisible to the query. They receive the **same generic `404 USER_NOT_FOUND`** as a never-registered user — there is **no** distinct "account deleted" / "account disabled" error.

## Sequence diagram

```mermaid
sequenceDiagram
    autonumber
    actor U as User
    participant FE as Frontend
    participant G as Google GSI
    participant BE as Backend
    participant GA as google-auth-library
    participant DB as MongoDB

    U->>FE: Click Google icon
    FE->>G: load gsi/client, initTokenClient(client_id)
    FE->>G: requestAccessToken()
    G-->>U: Popup → consent
    U-->>G: Approve
    G-->>FE: ID token (signed JWT)
    alt Sign-up
        FE->>BE: POST /api/auth/google/signup { googleToken, role, referralCode }
    else Sign-in
        FE->>BE: POST /api/auth/google/signin { googleToken }
    end
    BE->>GA: verifyGoogleToken(googleToken)
    GA-->>BE: { email, name, picture, ... } or null
    alt Sign-up
        BE->>DB: User.findOne({ email })
    else Sign-in
        BE->>DB: User.findOne({ email, status: "active" })
    end
    alt Sign-up: email exists
        BE-->>FE: 409 USER_EXISTS
    else Sign-up: new
        BE->>DB: User.create({ email, role, isEmailVerified:true, profile.avatar })
        opt referral
            BE->>DB: increment referrer.referralStats
        end
    else Sign-in: no active user (missing or soft-deleted)
        BE-->>FE: 404 USER_NOT_FOUND
    else Sign-in: ok
        BE->>DB: set user.lastLoginAt = now
        BE->>DB: back-fill avatar if blank
    end
    BE->>BE: generate access and refresh tokens
    BE->>BE: push refresh token
    BE-->>FE: 200 { user, tokens }
    FE->>FE: localStorage.setItem(accessToken/refreshToken)
    FE-->>U: Redirect /dashboard/{role}
```

## API calls

| Method | Endpoint | Source |
|---|---|---|
| `POST` | `/api/auth/google/signup` | `authRoutes.ts:30` → `authController.googleSignUp` |
| `POST` | `/api/auth/google/signin` | `authRoutes.ts:31` → `authController.googleSignIn` |

## Database writes

- **`users` collection**: on sign-up, full insert (no password). On sign-in, only `lastLoginAt`, possibly `profile.avatar`, and a new refresh token appended.

## Socket events emitted

- **`referral-signup`** → `user-${referrerId}` when sign-up includes a valid `referralCode`.

## Side effects

- **No email** is sent (Google handles trust). No `TempVerification` is created.
- The avatar URL is stored from Google's CDN; consider proxying or rehosting if Google's privacy rules change for `googleusercontent.com`.

## Error / edge cases

- **Invalid Google token** (bad signature, wrong audience, expired) → `googleOAuthService` returns `null` → `401 INVALID_GOOGLE_TOKEN`.
- **Email already exists during sign-up** → `409 USER_EXISTS`; frontend prompts to use sign-in instead.
- **User does not exist during sign-in** → `404 USER_NOT_FOUND`; frontend redirects to sign-up.
- **Soft-deleted user signs in via Google** → `404 USER_NOT_FOUND` (generic, indistinguishable from "never registered") because the lookup filters by `status: "active"`.
- **Popup blocker** → GSI throws a client-side error caught in the view and surfaced as a toast.
- **Network failure to `accounts.google.com`** → GSI rejects; frontend retries on next click.
- **`email_verified === false` on the Google token** → currently not enforced; the backend trusts any successful Google response. For an extra-strict mode, gate on `googleUser.email_verified === true` in `googleOAuthService`.

> [!warning] Single backup file
> `frontend/src/auth/services/google-oauth.ts.backup` is checked in. Delete or convert to a documentation note — it leaks a hard-coded client ID that should only live in `.env.*`.

## Linked flows

- [[Authentication Flow]] — token issuance and storage are identical from step 9 onward.
- [[Registration Flow]] — alternative path that requires email verification.
- [[Referral Flow]] — works identically for Google-signup referrals.

## Source files

- Frontend: `frontend/src/auth/services/google-oauth.ts`
- Frontend: `frontend/src/auth/context/jwt/action.ts:281-331`
- Frontend: `frontend/src/auth/view/jwt/jwt-sign-up-view.tsx`, `jwt-sign-in-view.tsx`
- Frontend: `frontend/GOOGLE_OAUTH_SETUP.md`
- Backend: `backend/src/services/auth/authController.ts:781-941`
- Backend: `backend/src/services/auth/googleOAuthService.ts`
- Backend: `backend/src/services/auth/authRoutes.ts:30-31`
