145 lines
7.7 KiB
Markdown
145 lines
7.7 KiB
Markdown
---
|
|
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
|
|
|
|
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 found 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`, 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, looks up `User.findOne({ email: googleUser.email })`. If no user, 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.
|
|
|
|
> [!tip] Account linking is implicit by email
|
|
> A user who originally signed up via email + password can sign in with Google as long as the email matches — no extra "link account" step. The backend simply reuses the existing user document. There is **no** separate `googleId` field stored today, so this is a one-way trust on `googleUser.email`.
|
|
|
|
## 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
|
|
BE->>DB: User.findOne({ email })
|
|
alt Sign-up: user 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: user missing
|
|
BE-->>FE: 404 USER_NOT_FOUND
|
|
else Sign-in: ok
|
|
BE->>DB: user.lastLoginAt = now; back-fill avatar if blank
|
|
end
|
|
BE->>BE: generate access + refresh; push refresh
|
|
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`.
|
|
- **User already exists during sign-up** → `409`; frontend prompts to use sign-in instead.
|
|
- **User missing during sign-in** → `404`; frontend redirects to sign-up.
|
|
- **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`
|