TurfAITurfAI Developers
Api_syncedReference

Google OAuth Integration - API Documentation

Synced from the source repositories. Do not edit by hand.

Version: 1.0 Status: Implementation Ready Created: 2025-11-08 Feature: Google Sign Up/Login


Table of Contents

  1. Overview
  2. Architecture
  3. Google Cloud Setup
  4. Database Schema
  5. API Endpoints
  6. Authentication Flow
  7. Security Considerations
  8. Environment Variables
  9. Error Handling
  10. Testing Guide
  11. UI Integration Guide

Overview

Goals

Enable users to sign up and log in to TurfAI using their Google accounts through OAuth 2.0, providing:

  • Seamless Authentication: One-click Google sign-in
  • Auto-registration: Automatically create user accounts from Google profiles
  • Account Linking: Link existing email accounts to Google OAuth
  • Profile Data: Import Google profile picture and verified email
  • Security: Leverage Google's authentication security

Benefits

  • User Experience: Faster signup/login without password management
  • Security: OAuth 2.0 standard with Google's security infrastructure
  • Email Verification: Google accounts are pre-verified
  • Profile Data: Automatic profile picture and display name
  • Trust: Users trust Google authentication

Architecture

Component Diagram

┌─────────────┐         ┌──────────────┐         ┌─────────────┐
│             │         │              │         │             │
│  Frontend   │────────▶│     DMS      │────────▶│   Google    │
│  (UI/Web)   │         │   (Strapi)   │         │   OAuth     │
│             │         │              │         │             │
└─────────────┘         └──────────────┘         └─────────────┘
      │                        │                        │
      │                        │                        │
      │     1. Click "Sign in with Google"              │
      │────────────────────────────────────────────────▶│
      │                        │                        │
      │     2. Redirect to /api/connect/google          │
      │◀────────────────────────                        │
      │                        │                        │
      │     3. Google OAuth consent screen              │
      │────────────────────────────────────────────────▶│
      │                        │                        │
      │     4. User authorizes (email, profile)         │
      │◀────────────────────────────────────────────────│
      │                        │                        │
      │     5. Redirect to callback with auth code      │
      │────────────────────────▶                        │
      │                        │                        │
      │                 6. Exchange code for tokens     │
      │                        │────────────────────────▶│
      │                        │                        │
      │                 7. Get user profile from Google │
      │                        │◀────────────────────────│
      │                        │                        │
      │                 8. Create/update user in DB     │
      │                        │                        │
      │     9. Return JWT + redirect to dashboard       │
      │◀────────────────────────                        │
      │                        │                        │
      │     10. Store JWT, access app                   │
      │                        │                        │

Authentication Flow Types

  1. New User Signup: Google account → Auto-create TurfAI user → Login
  2. Existing User Login: Google account → Find TurfAI user → Login
  3. Account Linking: Email user → Connect Google → Linked account
  4. Repeat Login: Google account → JWT refresh → Login

Google Cloud Setup

Prerequisites

  • Google Cloud Project (existing or new)
  • Admin access to Google Cloud Console
  • Domain ownership (for production)

Step 1: Create OAuth 2.0 Credentials

  1. Go to Google Cloud Console

  2. Navigate to APIs & ServicesCredentials

  3. Click + CREATE CREDENTIALSOAuth 2.0 Client ID

  4. Configure OAuth consent screen (if not already done):

    • User Type: External (for public access)
    • App name: TurfAI
    • User support email: Your email
    • Developer contact: Your email
    • Scopes: Add email and profile scopes
    • Test users: Add your test email addresses
  5. Create OAuth Client ID:

    • Application type: Web application
    • Name: TurfAI DMS OAuth Client
    • Authorized JavaScript origins: (if needed for frontend)
      • https://turfai.io
      • https://sandbox.turfai.io
      • http://localhost:3000 (development)
    • Authorized redirect URIs:
      • Production: https://turfai-dms-eatorcypia-uc.a.run.app/api/connect/google/callback
      • Sandbox: https://sandbox-dms.turfai.io/api/connect/google/callback
      • Development: http://localhost:1337/api/connect/google/callback
  6. Save the Client ID and Client Secret

Step 2: Enable Required APIs

  1. Navigate to APIs & ServicesLibrary
  2. Enable:
    • Google+ API (for profile data)
    • People API (for enhanced profile)

App Information:

Scopes:

  • email - Read user email address
  • profile - Read user basic profile info
  • openid - Authenticate user identity

Publishing Status:

  • Development: Testing mode (max 100 test users)
  • Production: Submit for verification (required for public access)

Database Schema

Users Table Extensions

Extend the existing Strapi users table with Google OAuth fields:

-- Add Google OAuth fields to users table
ALTER TABLE users ADD COLUMN IF NOT EXISTS google_id VARCHAR(255) UNIQUE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS google_email VARCHAR(255);
ALTER TABLE users ADD COLUMN IF NOT EXISTS google_profile_picture TEXT;
ALTER TABLE users ADD COLUMN IF NOT EXISTS oauth_provider VARCHAR(50) DEFAULT 'email';
ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT FALSE;
ALTER TABLE users ADD COLUMN IF NOT EXISTS google_connected_at TIMESTAMP;
ALTER TABLE users ADD COLUMN IF NOT EXISTS google_raw_profile JSONB;

-- Create index for faster Google ID lookups
CREATE INDEX IF NOT EXISTS idx_users_google_id ON users(google_id);
CREATE INDEX IF NOT EXISTS idx_users_oauth_provider ON users(oauth_provider);

-- Add comments for documentation
COMMENT ON COLUMN users.google_id IS 'Unique Google user identifier (sub claim from JWT)';
COMMENT ON COLUMN users.google_email IS 'Email from Google account (may differ from primary email)';
COMMENT ON COLUMN users.google_profile_picture IS 'Google profile picture URL';
COMMENT ON COLUMN users.oauth_provider IS 'Authentication provider: email, google, etc.';
COMMENT ON COLUMN users.email_verified IS 'Email verification status (auto-true for Google)';
COMMENT ON COLUMN users.google_connected_at IS 'Timestamp when Google was first connected';
COMMENT ON COLUMN users.google_raw_profile IS 'Raw Google profile data for debugging';

New Table: OAuth Connections Audit Log

Track OAuth connection events for security and debugging:

CREATE TABLE oauth_connections (
  id SERIAL PRIMARY KEY,
  user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
  provider VARCHAR(50) NOT NULL,
  provider_user_id VARCHAR(255),
  connection_type VARCHAR(50), -- 'signup', 'login', 'link', 'reauth'
  ip_address INET,
  user_agent TEXT,
  success BOOLEAN DEFAULT TRUE,
  error_message TEXT,
  metadata JSONB,
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_oauth_connections_user ON oauth_connections(user_id);
CREATE INDEX idx_oauth_connections_provider ON oauth_connections(provider);
CREATE INDEX idx_oauth_connections_created ON oauth_connections(created_at);

User Schema Example

{
  "id": 123,
  "username": "john_doe_google",
  "email": "john.doe@gmail.com",
  "display_name": "John Doe",
  "password": null, // Google users may not have password
  "google_id": "1234567890abcdef",
  "google_email": "john.doe@gmail.com",
  "google_profile_picture": "https://lh3.googleusercontent.com/a/...",
  "oauth_provider": "google",
  "email_verified": true,
  "google_connected_at": "2025-11-08T10:30:00Z",
  "google_raw_profile": {
    "sub": "1234567890abcdef",
    "name": "John Doe",
    "given_name": "John",
    "family_name": "Doe",
    "picture": "https://...",
    "email": "john.doe@gmail.com",
    "email_verified": true,
    "locale": "en"
  },
  "role": {
    "id": 1,
    "name": "Authenticated",
    "type": "authenticated"
  },
  "created_at": "2025-11-08T10:30:00Z",
  "updated_at": "2025-11-08T10:30:00Z"
}

API Endpoints

1. Initiate Google OAuth Flow

Endpoint: GET /api/connect/google

Description: Redirects user to Google OAuth consent screen

Authentication: Not required

Query Parameters:

  • redirect_url (optional): URL to redirect after successful auth

Request:

GET /api/connect/google?redirect_url=https://turfai.io/dashboard
Host: turfai-dms-eatorcypia-uc.a.run.app

Response:

  • HTTP 302 Redirect to Google OAuth consent screen
  • User will be prompted to authorize TurfAI

Implementation Notes:

  • Strapi built-in endpoint
  • Automatically constructs OAuth URL with client ID and scopes
  • Includes CSRF state parameter for security

2. OAuth Callback Handler

Endpoint: GET /api/connect/google/callback

Description: Handles Google OAuth callback, creates/updates user, returns JWT

Authentication: Not required (handled by OAuth code)

Query Parameters:

  • code: Authorization code from Google
  • state: CSRF state parameter

Request (from Google redirect):

GET /api/connect/google/callback?code=4/0AY0e-g7...&state=abc123
Host: turfai-dms-eatorcypia-uc.a.run.app

Response - New User Signup:

HTTP/1.1 302 Found
Location: https://turfai.io/dashboard?jwt=eyJhbGciOiJIUzI1NiIs...

Set-Cookie: jwt=eyJhbGciOiJIUzI1NiIs...; HttpOnly; Secure; SameSite=Lax

Response - Existing User Login:

HTTP/1.1 302 Found
Location: https://turfai.io/dashboard?jwt=eyJhbGciOiJIUzI1NiIs...

Error Response - OAuth Denied:

HTTP/1.1 302 Found
Location: https://turfai.io/login?error=access_denied&error_description=User+denied+access

Error Response - Invalid Code:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid authorization code",
  "data": {
    "provider": "google"
  }
}

Implementation Notes:

  • Strapi built-in endpoint with custom controller override
  • Exchanges auth code for access token
  • Fetches user profile from Google
  • Creates new user if doesn't exist (auto-registration)
  • Updates existing user if found by email or google_id
  • Returns JWT token in URL and cookie

3. Get Google OAuth Status

Endpoint: GET /api/auth/google/status

Description: Check if current user has Google account connected

Authentication: Required (JWT)

Request:

GET /api/auth/google/status
Host: turfai-dms-eatorcypia-uc.a.run.app
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response - Connected:

{
  "google_connected": true,
  "google_email": "john.doe@gmail.com",
  "google_profile_picture": "https://lh3.googleusercontent.com/a/...",
  "connected_at": "2025-11-08T10:30:00Z",
  "can_disconnect": true,
  "has_password": false
}

Response - Not Connected:

{
  "google_connected": false,
  "can_connect": true,
  "current_provider": "email"
}

Error - Unauthorized:

{
  "statusCode": 401,
  "error": "Unauthorized",
  "message": "Invalid token"
}

Endpoint: POST /api/auth/google/link

Description: Link Google account to existing email-based account

Authentication: Required (JWT)

Request:

POST /api/auth/google/link
Host: turfai-dms-eatorcypia-uc.a.run.app
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json

{
  "google_auth_code": "4/0AY0e-g7..."
}

Response - Success:

{
  "success": true,
  "message": "Google account linked successfully",
  "google_email": "john.doe@gmail.com",
  "google_profile_picture": "https://...",
  "linked_at": "2025-11-08T10:30:00Z"
}

Error - Already Linked:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Google account already linked to this user"
}

Error - Email Mismatch:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Google account email does not match your TurfAI email",
  "data": {
    "turfai_email": "john@example.com",
    "google_email": "different@gmail.com",
    "suggestion": "Please use a Google account with matching email or contact support"
  }
}

Error - Google Account In Use:

{
  "statusCode": 409,
  "error": "Conflict",
  "message": "This Google account is already linked to another TurfAI account",
  "data": {
    "google_email": "john.doe@gmail.com"
  }
}

5. Disconnect Google Account

Endpoint: POST /api/auth/google/disconnect

Description: Disconnect Google OAuth from user account

Authentication: Required (JWT)

Request:

POST /api/auth/google/disconnect
Host: turfai-dms-eatorcypia-uc.a.run.app
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response - Success:

{
  "success": true,
  "message": "Google account disconnected successfully",
  "can_still_login": true,
  "login_methods": ["email"]
}

Error - Cannot Disconnect:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Cannot disconnect Google account - no alternative login method available",
  "data": {
    "reason": "no_password",
    "suggestion": "Please set a password before disconnecting Google account",
    "set_password_url": "/api/auth/set-password"
  }
}

Error - Not Connected:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Google account is not connected to this user"
}

6. Get User Profile with OAuth Info

Endpoint: GET /api/users/me

Description: Get current user profile including OAuth connection status

Authentication: Required (JWT)

Request:

GET /api/users/me
Host: turfai-dms-eatorcypia-uc.a.run.app
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "id": 123,
  "username": "john_doe",
  "email": "john.doe@gmail.com",
  "display_name": "John Doe",
  "profile_picture": "https://lh3.googleusercontent.com/a/...",
  "email_verified": true,
  "oauth_provider": "google",
  "google_connected": true,
  "google_email": "john.doe@gmail.com",
  "role": {
    "id": 1,
    "name": "Authenticated",
    "type": "authenticated"
  },
  "created_at": "2025-11-08T10:30:00Z",
  "updated_at": "2025-11-08T10:30:00Z"
}

Authentication Flow

Flow 1: New User Signup via Google

1. User visits TurfAI signup page
2. User clicks "Sign up with Google"
3. Frontend redirects to: GET /api/connect/google
4. DMS redirects to Google OAuth consent screen
5. User authorizes TurfAI (email + profile scopes)
6. Google redirects to: GET /api/connect/google/callback?code=...
7. DMS exchanges code for access token
8. DMS fetches user profile from Google API
9. DMS checks if user exists (by google_id or email):
   - NOT EXISTS: Create new user
     * username = email prefix + random suffix
     * email = google email
     * google_id = Google sub
     * google_email = Google email
     * google_profile_picture = Google picture URL
     * email_verified = true
     * oauth_provider = 'google'
     * role = 'Authenticated'
10. DMS generates JWT token
11. DMS redirects to frontend: https://turfai.io/dashboard?jwt=...
12. Frontend stores JWT in localStorage/cookie
13. User is logged in and redirected to dashboard

Flow 2: Existing User Login via Google

1. User visits TurfAI login page
2. User clicks "Sign in with Google"
3. Frontend redirects to: GET /api/connect/google
4. DMS redirects to Google OAuth consent screen
5. User authorizes (or skips if already authorized)
6. Google redirects to: GET /api/connect/google/callback?code=...
7. DMS exchanges code for access token
8. DMS fetches user profile from Google API
9. DMS finds existing user by google_id
10. DMS updates user profile (picture, last login)
11. DMS generates JWT token
12. DMS redirects to frontend with JWT
13. User is logged in
1. User logged in with email/password
2. User goes to Profile/Settings
3. User clicks "Connect Google Account"
4. Frontend calls: POST /api/auth/google/link-init
5. DMS returns Google OAuth URL
6. Frontend opens OAuth URL in popup/redirect
7. User authorizes on Google
8. Google redirects to callback
9. DMS links Google account to existing user:
   - Validates email match (optional)
   - Sets google_id, google_email, google_profile_picture
   - oauth_provider remains 'email' (primary method)
10. DMS closes popup/redirects back
11. User can now login with either email or Google

Flow 4: Disconnect Google Account

1. User logged in (has Google connected)
2. User goes to Profile/Settings → Connected Accounts
3. User clicks "Disconnect Google"
4. Frontend confirms: "Are you sure?"
5. Frontend calls: POST /api/auth/google/disconnect
6. DMS validates:
   - User has alternative login (email + password)
   - If no password: Error "Set password first"
7. DMS disconnects:
   - Sets google_id = null
   - Sets google_email = null
   - Sets google_profile_picture = null
   - oauth_provider = 'email'
8. User can no longer login with Google
9. User must use email/password

Security Considerations

1. State Parameter (CSRF Protection)

Problem: OAuth redirect could be hijacked

Solution: Use state parameter to validate redirect

// Generate state on init
const state = crypto.randomBytes(16).toString('hex');
await redis.set(`oauth:state:${state}`, userId, 'EX', 600); // 10 min expiry

// Validate on callback
const validState = await redis.get(`oauth:state:${state}`);
if (!validState) {
  throw new Error('Invalid state parameter - possible CSRF attack');
}
await redis.del(`oauth:state:${state}`); // One-time use

2. Token Security

Access Tokens: Never stored in database (used immediately and discarded)

Refresh Tokens: Not used (Google sign-in is stateless, re-auth required)

JWT Tokens: Strapi-generated, short-lived (default 30 days, configurable)

3. Email Verification

Google Users: Email automatically verified (Google validates emails)

Email Conflicts: If email exists, link accounts or reject based on policy

4. Account Takeover Prevention

Scenario: User A has email X, User B tries to signup with Google using email X

Solution:

// On Google OAuth callback
const existingUser = await strapi.query('user').findOne({ email: googleEmail });

if (existingUser && !existingUser.google_id) {
  // Email exists but not linked to Google
  // OPTION 1: Auto-link (if email verified on both sides)
  if (existingUser.email_verified && googleProfile.email_verified) {
    await linkGoogleAccount(existingUser.id, googleProfile);
    return generateJWT(existingUser);
  }

  // OPTION 2: Reject and ask to login first
  throw new Error('Email already registered. Please login and connect Google from settings.');
}

5. Profile Picture Security

Validation: Ensure Google picture URL is from googleusercontent.com

Storage: Store URL only (do not download/re-upload for security)

Fallback: Use initials avatar if Google picture unavailable

6. Scope Limitation

Only Request Necessary Scopes:

  • email - Get user email
  • profile - Get basic profile (name, picture)
  • openid - OpenID Connect
  • ❌ NOT drive - Not needed for sign-in
  • ❌ NOT calendar - Not needed for sign-in

7. Rate Limiting

Implement rate limiting on OAuth endpoints:

// Max 10 OAuth attempts per IP per hour
app.use('/api/connect/google', rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10,
  message: 'Too many OAuth attempts, please try again later'
}));

Environment Variables

DMS Environment Variables

# Google OAuth Configuration
GOOGLE_OAUTH_CLIENT_ID=1234567890-abcdefghijklmnopqrstuvwxyz.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=GOCSPX-abc123def456ghi789jkl012mno
GOOGLE_OAUTH_REDIRECT_URI=https://turfai-dms-eatorcypia-uc.a.run.app/api/connect/google/callback

# OAuth Settings
OAUTH_AUTO_REGISTER=true                    # Auto-create user on first Google login
OAUTH_DEFAULT_ROLE=authenticated            # Default role for OAuth users
OAUTH_EMAIL_VERIFICATION_REQUIRED=false     # Skip email verification for Google users
OAUTH_ALLOW_ACCOUNT_LINKING=true            # Allow linking Google to existing email accounts
OAUTH_REQUIRE_EMAIL_MATCH=false             # Require email match when linking (true = strict)

# Frontend Configuration
FRONTEND_URL=https://turfai.io
OAUTH_SUCCESS_REDIRECT=/dashboard
OAUTH_ERROR_REDIRECT=/login

# Security
OAUTH_STATE_TTL=600                         # State parameter TTL in seconds (10 min)
OAUTH_SESSION_TTL=2592000                   # JWT session TTL in seconds (30 days)

Development vs Production

Development (dms/.env.development):

GOOGLE_OAUTH_CLIENT_ID=dev-client-id.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=dev-secret
GOOGLE_OAUTH_REDIRECT_URI=http://localhost:1337/api/connect/google/callback
FRONTEND_URL=http://localhost:3000

Production (dms/.env.production):

GOOGLE_OAUTH_CLIENT_ID=prod-client-id.apps.googleusercontent.com
GOOGLE_OAUTH_CLIENT_SECRET=prod-secret
GOOGLE_OAUTH_REDIRECT_URI=https://turfai-dms-eatorcypia-uc.a.run.app/api/connect/google/callback
FRONTEND_URL=https://turfai.io

Error Handling

Common Errors

1. Invalid Redirect URI

Error from Google:

Error 400: redirect_uri_mismatch
The redirect URI in the request does not match the ones authorized for the OAuth client.

Cause: Redirect URI not added to Google Console

Fix: Add exact redirect URI to Google OAuth Client authorized redirect URIs


2. Access Denied

Error:

{
  "error": "access_denied",
  "error_description": "User denied access"
}

Cause: User clicked "Cancel" on Google consent screen

Handling: Redirect to login page with friendly message


3. Invalid Authorization Code

Error:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "Invalid authorization code"
}

Causes:

  • Code already used (one-time use)
  • Code expired (valid for ~10 minutes)
  • Code from wrong OAuth client

Fix: User must retry OAuth flow


4. Email Already Registered

Error:

{
  "statusCode": 409,
  "error": "Conflict",
  "message": "Email already registered with different provider",
  "data": {
    "email": "john@example.com",
    "registered_provider": "email",
    "suggestion": "Please login with email and link Google from settings"
  }
}

Handling: Show message with link to login page


5. Google Account Already Linked

Error:

{
  "statusCode": 409,
  "error": "Conflict",
  "message": "This Google account is already linked to another user",
  "data": {
    "google_email": "john@gmail.com"
  }
}

Handling: User must use different Google account or contact support


Testing Guide

Unit Tests

Test 1: OAuth URL Generation

describe('Google OAuth', () => {
  it('should generate valid OAuth URL', () => {
    const url = generateGoogleOAuthUrl();
    expect(url).toContain('accounts.google.com/o/oauth2/v2/auth');
    expect(url).toContain('client_id=');
    expect(url).toContain('redirect_uri=');
    expect(url).toContain('scope=email%20profile%20openid');
    expect(url).toContain('state=');
  });
});

Test 2: New User Creation

it('should create new user from Google profile', async () => {
  const googleProfile = {
    sub: 'google_123',
    email: 'test@gmail.com',
    name: 'Test User',
    picture: 'https://lh3.googleusercontent.com/a/test'
  };

  const user = await createUserFromGoogleProfile(googleProfile);

  expect(user.google_id).toBe('google_123');
  expect(user.email).toBe('test@gmail.com');
  expect(user.email_verified).toBe(true);
  expect(user.oauth_provider).toBe('google');
});

Test 3: Account Linking

it('should link Google to existing email user', async () => {
  // Create email user
  const emailUser = await strapi.entityService.create('plugin::users-permissions.user', {
    data: { email: 'test@example.com', username: 'testuser', password: 'hashed' }
  });

  // Link Google
  const googleProfile = {
    sub: 'google_456',
    email: 'test@gmail.com'
  };

  await linkGoogleAccount(emailUser.id, googleProfile);

  const updatedUser = await strapi.entityService.findOne('plugin::users-permissions.user', emailUser.id);
  expect(updatedUser.google_id).toBe('google_456');
  expect(updatedUser.oauth_provider).toBe('email'); // Primary provider unchanged
});

Integration Tests

Test 4: Complete OAuth Flow

it('should complete full OAuth flow', async () => {
  // 1. Initiate OAuth
  const response1 = await request(app).get('/api/connect/google');
  expect(response1.status).toBe(302);
  expect(response1.header.location).toContain('accounts.google.com');

  // 2. Mock Google callback with auth code
  const mockCode = 'mock_auth_code_123';
  const response2 = await request(app)
    .get('/api/connect/google/callback')
    .query({ code: mockCode, state: 'valid_state' });

  expect(response2.status).toBe(302);
  expect(response2.header.location).toContain('dashboard');
  expect(response2.header.location).toContain('jwt=');
});

Manual Testing

Test Scenario 1: New User Signup

  1. Clear database of test user
  2. Open browser to http://localhost:3000/signup
  3. Click "Sign up with Google"
  4. Authorize with test Google account
  5. Verify redirect to dashboard
  6. Verify user created in database with google_id
  7. Verify profile picture displayed

Test Scenario 2: Existing User Login

  1. Use existing Google-authenticated user
  2. Logout from TurfAI
  3. Click "Sign in with Google"
  4. Should login immediately (no consent screen)
  5. Verify redirect to dashboard

Test Scenario 3: Account Linking

  1. Create email user: test@example.com
  2. Login with email/password
  3. Go to Settings → Connected Accounts
  4. Click "Connect Google"
  5. Authorize with Google
  6. Verify Google account linked
  7. Logout and login with Google
  8. Verify same user account

Test Scenario 4: Error Handling

  1. Initiate OAuth flow
  2. Click "Cancel" on Google consent
  3. Verify redirect to login with error message
  4. Re-initiate and complete successfully

UI Integration Guide

Frontend Implementation

Step 1: Add Google Sign In Button

Login Page (/login):

import { useState } from 'react';
import { Button } from '@/components/ui/button';

export default function LoginPage() {
  const handleGoogleLogin = () => {
    // Redirect to DMS OAuth endpoint
    const dmsUrl = process.env.NEXT_PUBLIC_DMS_URL || 'http://localhost:1337';
    const redirectUrl = `${window.location.origin}/auth/callback`;

    window.location.href = `${dmsUrl}/api/connect/google?redirect_url=${encodeURIComponent(redirectUrl)}`;
  };

  return (
    <div className="login-container">
      <h1>Login to TurfAI</h1>

      {/* Email/Password Form */}
      <form onSubmit={handleEmailLogin}>
        <input type="email" placeholder="Email" />
        <input type="password" placeholder="Password" />
        <button type="submit">Login</button>
      </form>

      <div className="divider">OR</div>

      {/* Google Sign In Button */}
      <Button
        onClick={handleGoogleLogin}
        variant="outline"
        className="google-signin-btn"
      >
        <img src="/google-icon.svg" alt="Google" />
        Sign in with Google
      </Button>
    </div>
  );
}

Step 2: Handle OAuth Callback

Callback Page (/auth/callback):

'use client';

import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';

export default function AuthCallbackPage() {
  const router = useRouter();
  const searchParams = useSearchParams();

  useEffect(() => {
    // Get JWT from URL (returned by DMS)
    const jwt = searchParams.get('jwt');
    const error = searchParams.get('error');

    if (error) {
      // Handle OAuth error
      console.error('OAuth error:', error);
      router.push(`/login?error=${error}`);
      return;
    }

    if (jwt) {
      // Store JWT in localStorage
      localStorage.setItem('jwt', jwt);

      // Update auth context/state
      // ...

      // Redirect to dashboard
      router.push('/dashboard');
    } else {
      // No JWT, redirect to login
      router.push('/login?error=no_token');
    }
  }, [searchParams, router]);

  return (
    <div className="auth-callback">
      <p>Completing authentication...</p>
    </div>
  );
}

Step 3: Profile Settings - Connected Accounts

Settings Page (/settings/accounts):

import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Avatar } from '@/components/ui/avatar';

export default function ConnectedAccountsSettings() {
  const [googleStatus, setGoogleStatus] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchGoogleStatus();
  }, []);

  const fetchGoogleStatus = async () => {
    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_DMS_URL}/api/auth/google/status`, {
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('jwt')}`
        }
      });
      const data = await response.json();
      setGoogleStatus(data);
    } catch (error) {
      console.error('Failed to fetch Google status:', error);
    } finally {
      setLoading(false);
    }
  };

  const handleConnectGoogle = () => {
    const dmsUrl = process.env.NEXT_PUBLIC_DMS_URL;
    window.location.href = `${dmsUrl}/api/connect/google`;
  };

  const handleDisconnectGoogle = async () => {
    if (!confirm('Are you sure you want to disconnect your Google account?')) {
      return;
    }

    try {
      const response = await fetch(`${process.env.NEXT_PUBLIC_DMS_URL}/api/auth/google/disconnect`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('jwt')}`
        }
      });

      if (response.ok) {
        alert('Google account disconnected successfully');
        fetchGoogleStatus();
      } else {
        const error = await response.json();
        alert(error.message);
      }
    } catch (error) {
      console.error('Failed to disconnect Google:', error);
      alert('Failed to disconnect Google account');
    }
  };

  if (loading) return <div>Loading...</div>;

  return (
    <div className="connected-accounts">
      <h2>Connected Accounts</h2>

      <div className="account-card google">
        <div className="account-header">
          <img src="/google-icon.svg" alt="Google" />
          <h3>Google</h3>
        </div>

        {googleStatus?.google_connected ? (
          <div className="account-connected">
            <Avatar src={googleStatus.google_profile_picture} />
            <div className="account-info">
              <p className="email">{googleStatus.google_email}</p>
              <p className="connected-date">
                Connected on {new Date(googleStatus.connected_at).toLocaleDateString()}
              </p>
            </div>
            <Button
              onClick={handleDisconnectGoogle}
              variant="destructive"
              disabled={!googleStatus.can_disconnect}
            >
              Disconnect
            </Button>
            {!googleStatus.has_password && (
              <p className="warning">
                ⚠️ Set a password before disconnecting Google
              </p>
            )}
          </div>
        ) : (
          <div className="account-not-connected">
            <p>Connect your Google account for easy sign-in</p>
            <Button onClick={handleConnectGoogle}>
              Connect Google
            </Button>
          </div>
        )}
      </div>
    </div>
  );
}

Step 4: API Service Helper

Services (/services/authService.ts):

const DMS_URL = process.env.NEXT_PUBLIC_DMS_URL || 'http://localhost:1337';

export const authService = {
  // Initiate Google OAuth
  googleLogin: (redirectUrl?: string) => {
    const url = new URL(`${DMS_URL}/api/connect/google`);
    if (redirectUrl) {
      url.searchParams.set('redirect_url', redirectUrl);
    }
    window.location.href = url.toString();
  },

  // Get Google connection status
  getGoogleStatus: async () => {
    const response = await fetch(`${DMS_URL}/api/auth/google/status`, {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('jwt')}`
      }
    });

    if (!response.ok) {
      throw new Error('Failed to fetch Google status');
    }

    return response.json();
  },

  // Disconnect Google account
  disconnectGoogle: async () => {
    const response = await fetch(`${DMS_URL}/api/auth/google/disconnect`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('jwt')}`
      }
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message);
    }

    return response.json();
  }
};

Implementation Checklist

Phase 1: Google Cloud Setup ✅

  • Create Google Cloud Project
  • Enable Google+ API and People API
  • Configure OAuth consent screen
  • Create OAuth 2.0 Client ID
  • Add authorized redirect URIs
  • Save Client ID and Client Secret

Phase 2: Database Schema ✅

  • Run database migration to add Google OAuth fields
  • Create oauth_connections audit table
  • Add indexes for performance
  • Test schema with sample data

Phase 3: Strapi Configuration ✅

  • Install required npm packages (@strapi/plugin-users-permissions)
  • Configure dms/config/plugins.js with Google provider
  • Add environment variables to .env
  • Override default OAuth callbacks (if needed)
  • Test OAuth endpoints

Phase 4: Custom Endpoints ✅

  • Implement GET /api/auth/google/status
  • Implement POST /api/auth/google/link
  • Implement POST /api/auth/google/disconnect
  • Add validation and error handling
  • Test all endpoints

Phase 5: Security & Testing ✅

  • Implement state parameter validation
  • Add rate limiting to OAuth endpoints
  • Test account linking scenarios
  • Test error handling (denied access, invalid code)
  • Security audit

Phase 6: Documentation ✅

  • Complete API documentation (this document)
  • Create UI integration guide
  • Document error codes and messages
  • Create testing guide
  • Update deployment docs with OAuth env vars

Phase 7: UI Integration (UI Team)

  • Add Google Sign In button to login/signup
  • Implement OAuth callback handling
  • Create Connected Accounts settings page
  • Add Google profile picture to navbar
  • Test complete flow end-to-end

Next Steps

  1. Review this document with team
  2. Create Google Cloud OAuth credentials
  3. Run database migrations
  4. Configure Strapi Google OAuth plugin
  5. Test OAuth flow in development
  6. Deploy to staging and test
  7. Provide documentation to UI team
  8. Deploy to production

Document Version: 1.0 Last Updated: 2025-11-08 Next Review: After Phase 5 completion

On this page

Table of ContentsOverviewGoalsBenefitsArchitectureComponent DiagramAuthentication Flow TypesGoogle Cloud SetupPrerequisitesStep 1: Create OAuth 2.0 CredentialsStep 2: Enable Required APIsStep 3: OAuth Consent Screen ConfigurationDatabase SchemaUsers Table ExtensionsNew Table: OAuth Connections Audit LogUser Schema ExampleAPI Endpoints1. Initiate Google OAuth Flow2. OAuth Callback Handler3. Get Google OAuth Status4. Link Google Account to Existing User5. Disconnect Google Account6. Get User Profile with OAuth InfoAuthentication FlowFlow 1: New User Signup via GoogleFlow 2: Existing User Login via GoogleFlow 3: Email User Links Google AccountFlow 4: Disconnect Google AccountSecurity Considerations1. State Parameter (CSRF Protection)2. Token Security3. Email Verification4. Account Takeover Prevention5. Profile Picture Security6. Scope Limitation7. Rate LimitingEnvironment VariablesDMS Environment VariablesDevelopment vs ProductionError HandlingCommon Errors1. Invalid Redirect URI2. Access Denied3. Invalid Authorization Code4. Email Already Registered5. Google Account Already LinkedTesting GuideUnit TestsTest 1: OAuth URL GenerationTest 2: New User CreationTest 3: Account LinkingIntegration TestsTest 4: Complete OAuth FlowManual TestingTest Scenario 1: New User SignupTest Scenario 2: Existing User LoginTest Scenario 3: Account LinkingTest Scenario 4: Error HandlingUI Integration GuideFrontend ImplementationStep 1: Add Google Sign In ButtonStep 2: Handle OAuth CallbackStep 3: Profile Settings - Connected AccountsStep 4: API Service HelperImplementation ChecklistPhase 1: Google Cloud Setup ✅Phase 2: Database Schema ✅Phase 3: Strapi Configuration ✅Phase 4: Custom Endpoints ✅Phase 5: Security & Testing ✅Phase 6: Documentation ✅Phase 7: UI Integration (UI Team)Next Steps