Acheteur Google OAuth Sign-in

Summary

An acheteur can sign in (or register) using their Google account. The client obtains a Google ID token via Google Identity Services and POSTs it to the API. The system verifies the token, upserts the acheteur row (creating it if new, or linking the Google ID to an existing password-based account), issues session tokens, and returns immediately — no email verification step is required because Google already guarantees the email is valid. If an existing verified account is linked to Google for the first time, a notification email is sent.

State Diagram

stateDiagram-v2
    state "Token Google reçu" as TokenReceived
    state "Token invalide" as TokenInvalid
    state "Email manquant" as EmailMissing
    state "Nouveau compte" as NewAccount
    state "Compte existant non lié" as ExistingUnlinked
    state "Compte existant lié" as ExistingLinked
    state "Session créée" as SessionIssued
    [*] --> TokenReceived: acheteur submits Google credential
    TokenReceived --> TokenInvalid: Google verification fails
    TokenInvalid --> [*]: 401 INVALID_CREDENTIAL
    TokenReceived --> EmailMissing: payload has no email
    EmailMissing --> [*]: 400 EMAIL_REQUIRED
    TokenReceived --> NewAccount: no existing acheteur for email
    TokenReceived --> ExistingUnlinked: account exists, no googleId yet
    TokenReceived --> ExistingLinked: account exists with googleId
    NewAccount --> SessionIssued: upsert creates row, emailVerified=true
    ExistingUnlinked --> SessionIssued: googleId set, notification email sent
    ExistingLinked --> SessionIssued: googleId refreshed
    SessionIssued --> [*]: session issued, 200 returned

Steps

1. Submit Google Credential (Actor: acheteur)

Client POSTs { credential: "<Google ID token>" } to POST /acheteur/auth/google.

Rate limit: 10 requests per minute per IP.

Outcome: Request reaches the handler.

2. Verify Google ID Token (Actor: system)

The singleton OAuth2Client (preserves Google’s cert cache across requests) calls verifyIdToken() against GOOGLE_CLIENT_ID. On failure, returns 401 INVALID_CREDENTIAL. If the verified payload contains no email, returns 400 EMAIL_REQUIRED.

Outcome: Validated payload with { email, given_name, family_name, sub }.

3. Upsert Acheteur (Actor: system)

Existing account lookup is performed before the upsert to detect the “first-time Google link” case.

The upsert:

  • Create path — sets emailVerified: true, passwordHash: null, googleId from the token sub.
  • Update path — sets googleId, clears emailVerifyDeadline and verifyReminderSentAt, marks emailVerified: true.

If an existing verified account had no googleId before this request, a acheteur-google-linked notification email is fired asynchronously (fire-and-forget; Slack alert on failure).

Outcome: Acheteur row exists and is fully verified.

4. Prune Sessions (Actor: system)

Oldest refresh tokens beyond the 4-session cap are deleted (pruneAcheteurSessions).

Outcome: Session count capped at 4.

5. Issue Tokens (Actor: system)

A 7-day opaque refresh token is generated, hashed, stored in acheteur_refresh_tokens, and set as an httpOnly cookie (acheteurRefreshToken, maxAge 7 days). A 15-minute access token is signed with ACHETEUR_JWT_SECRET.

Outcome: 200 response with { accessToken, acheteur }, refresh token cookie set.

Error States

  • Invalid or expired Google credential → 401 INVALID_CREDENTIAL
  • Google payload missing email → 400 EMAIL_REQUIRED
  • Notification email send failure → logged + Slack sentry-notif (non-blocking, session still issued)