Google Ads - Authentication
๐ Overviewโ
The Google Ads authentication module handles OAuth 2.0 authorization, token management, and automatic token refresh for accessing the Google Ads API. It manages both manager (MCC) and client account credentials with support for long-lived refresh tokens.
Source Files:
- Controller:
external/Integrations/GoogleAds/Controllers/Auth/AuthController.js - Model:
external/Integrations/GoogleAds/Models/token.js - Routes:
external/Integrations/GoogleAds/Routes/auth.js
External API: Google OAuth 2.0 + Google Ads API
๐๏ธ Collections Usedโ
google-ads-tokenโ
- Operations: Create, Read, Update
- Model:
external/Integrations/GoogleAds/Models/token.js - Usage Context: Store OAuth tokens, manager/client IDs, token expiration timestamps
accountโ
- Operations: Read
- Model:
external/models/account.js - Usage: Validate sub-account relationships for multi-tenant access
userโ
- Operations: Read
- Model:
external/models/user.js - Usage: Fetch user details for notifications
user-configโ
- Operations: Read
- Model:
external/models/user-config.js - Usage: Get notification preferences
๐ Data Flowโ
OAuth Authentication Flowโ
sequenceDiagram
participant User
participant DashClicks
participant Google
participant DB
User->>DashClicks: GET /auth/login?forward_url
DashClicks->>DashClicks: Create JWT state token
DashClicks->>DashClicks: Check existing token
alt Token Exists
DashClicks-->>User: Redirect to forward_url?status=success
else No Token
DashClicks->>Google: Redirect to OAuth consent
Note over Google: Scope: adwords
User->>Google: Grant permissions
Google->>DashClicks: GET /auth/callback?code&state
DashClicks->>DashClicks: Verify JWT state
DashClicks->>Google: Exchange code for tokens
Google-->>DashClicks: access_token, refresh_token
DashClicks->>DB: Save tokens
DashClicks->>User: Send notification (email/FCM)
DashClicks-->>User: Redirect to forward_url?status=success&token={id}
end
Token Refresh Flowโ
sequenceDiagram
participant Controller
participant TokenModel
participant Google
Controller->>TokenModel: Fetch token
TokenModel-->>Controller: Token data
alt Token Expired
Controller->>Google: POST /token (grant_type=refresh_token)
Google-->>Controller: New access_token
Controller->>TokenModel: Update access_token & generated_at
TokenModel-->>Controller: Updated token
end
Controller->>Google: API Request with access_token
๐ง Business Logic & Functionsโ
login()โ
Purpose: Initiates OAuth 2.0 flow or returns existing token
Source: Controllers/Auth/AuthController.js
External API Endpoint: GET https://accounts.google.com/o/oauth2/v2/auth
Parameters:
forward_url(String, Required) - URL to redirect after authenticationsubaccountid(String, Optional) - Sub-account ID for child account connection
Returns: 302 Redirect
Business Logic Flow:
-
Validate Required Parameters
if (!this.req.query.forward_url) {
return this.res.status(400).json({
success: false,
errno: 400,
message: 'forward_url is required',
});
} -
Handle Sub-Account
- Validate sub-account belongs to parent account
- Switch
accountIdcontext to sub-account
-
Check Existing Token
const tokenData = await TokenModel.findTokenDoc(accountId);
if (tokenData) {
// Token exists, redirect to success
return this.res.redirect(
`${successRedirectUrl}?status=success&integration=googleads&token=${tokenId}`,
);
} -
Create JWT State Token
const stateData = {
account_id: accountId,
forward_url: successRedirectUrl,
};
const state = jwt.sign({ data: stateData }, process.env.APP_SECRET); -
Build OAuth URL
const authUrl = this.oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['https://www.googleapis.com/auth/adwords'],
state: state,
prompt: 'consent', // Force consent to get refresh token
}); -
Redirect to Google OAuth
- User grants permissions
- Google redirects to callback with
codeandstate
OAuth Authorization URL:
GET https://accounts.google.com/o/oauth2/v2/auth
Query Parameters:
- client_id: GOOGLE_CLIENT_ID
- redirect_uri: GOOGLE_ADS_REDIRECT_URL
- response_type: code
- scope: https://www.googleapis.com/auth/adwords
- access_type: offline
- state: {JWT_TOKEN}
- prompt: consent
Error Handling:
- 400 Missing forward_url: Returns error JSON
- 400 Invalid sub-account: Sub-account doesn't belong to parent
- 500 Token Check Error: Logs error, proceeds with OAuth
- Redirect on Error: Redirects to
forward_url?status=error&reason={message}
Example Usage:
// Connect Google Ads for account
GET /v1/e/google/ads/auth/login
?forward_url=https://app.dashclicks.com/integrations
// Connect for sub-account
GET /v1/e/google/ads/auth/login
?forward_url=https://app.dashclicks.com/integrations
&subaccountid=507f1f77bcf86cd799439011
Side Effects:
- โ ๏ธ Creates JWT state token
- โ ๏ธ May query existing token from database
- โ ๏ธ Redirects user to Google OAuth consent page
callback()โ
Purpose: OAuth callback handler - exchanges authorization code for tokens
Source: Controllers/Auth/AuthController.js
External API Endpoint: POST https://oauth2.googleapis.com/token
Parameters:
code(String) - Authorization code from Googlestate(String) - JWT token containing original request parameters
Returns: 302 Redirect to forward_url
Business Logic Flow:
-
Verify State Token
let payload;
try {
payload = jwt.verify(state, process.env.APP_SECRET);
} catch (error) {
return this.res.status(401).json({
success: false,
errno: 401,
message: 'Invalid State',
});
}
const { account_id, forward_url } = payload.data; -
Check Existing Token (prevent duplicate connections)
const existingToken = await TokenModel.findTokenDoc(account_id);
if (existingToken) {
// Already connected, redirect to success
return this.redirectOnAuthentication(payload.data, 'success');
} -
Exchange Authorization Code
const { tokens } = await this.oauth2Client.getToken(code);
/*
tokens = {
access_token: "ya29...",
refresh_token: "1//...",
scope: "https://www.googleapis.com/auth/adwords",
token_type: "Bearer",
expiry_date: 1234567890000
}
*/ -
Prepare Token Data
const tokenData = {
account_id: account_id,
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
generated_at: moment().toISOString(),
expires_in: 3600, // 1 hour
scope: tokens.scope,
token_type: tokens.token_type,
}; -
Save to Database
const savedToken = await TokenModel.save(tokenData); -
Send Notifications
// Email notification
await sendMailNotification({
account_id: account_id,
integration: 'googleads',
status: 'connected',
});
// FCM push notification
await sendFCMNotification({
account_id: account_id,
title: 'Google Ads Connected',
body: 'Your Google Ads account has been successfully connected',
}); -
Redirect to Success
this.res.redirect(
`${forward_url}?status=success&integration=googleads&token=${savedToken.id}&accounts=${account_id}`,
);
API Request Example:
// Token exchange (handled by googleapis SDK)
POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded
code={authorization_code}
&client_id={GOOGLE_CLIENT_ID}
&client_secret={GOOGLE_CLIENT_SECRET}
&redirect_uri={GOOGLE_ADS_REDIRECT_URL}
&grant_type=authorization_code
API Response Example:
{
access_token: "ya29.a0AfH6SMBx...",
refresh_token: "1//0gKZ1x2y3z...",
scope: "https://www.googleapis.com/auth/adwords",
token_type: "Bearer",
expiry_date: 1697654400000
}
Error Handling:
- 401 Invalid State: JWT verification failed
- 400 Missing Code: Authorization code not provided
- 500 Token Exchange Error: Google API error
- Database Save Error: Logged and redirected to error URL
Example Usage:
// OAuth callback from Google
GET /v1/e/google/ads/auth/callback
?code=4/0AZEOvh...
&state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Side Effects:
- โ ๏ธ Exchanges OAuth code for access and refresh tokens
- โ ๏ธ Saves tokens to database
- โ ๏ธ Sends email notification to account owner
- โ ๏ธ Sends FCM push notification
- โ ๏ธ External API call to Google (quota counted)
Token Refresh (Automatic)โ
Purpose: Automatically refresh expired access tokens using refresh token
Triggered By: Any API request when token is expired
Logic:
// Check if token is expired
const generatedAt = moment(tokenData.generated_at);
const expiresIn = tokenData.expires_in || 3600; // Default 1 hour
const now = moment();
const secondsSinceGenerated = now.diff(generatedAt, 'seconds');
if (secondsSinceGenerated >= expiresIn) {
// Token expired, refresh it
this.oauth2Client.setCredentials({
refresh_token: tokenData.refresh_token,
});
const { credentials } = await this.oauth2Client.refreshAccessToken();
// Update in database
await TokenModel.update(tokenData._id, {
access_token: credentials.access_token,
generated_at: moment().toISOString(),
});
}
Token Lifetime:
- Access Token: 1 hour (3600 seconds)
- Refresh Token: Long-lived (doesn't expire unless revoked)
Refresh Frequency:
- Automatic refresh when access token expires
- No manual refresh needed
- Refresh token used to obtain new access token
๐ Integration Pointsโ
Internal Servicesโ
- Campaign Management: Uses tokens to fetch/manage campaigns
- Ad Group Operations: Uses tokens for ad group API calls
- Keyword Management: Uses tokens for keyword operations
- MCC Operations: Uses tokens for manager account access
External API Dependenciesโ
- Provider: Google OAuth 2.0 + Google Ads API
- Endpoints:
- OAuth:
https://accounts.google.com/o/oauth2/v2/auth - Token:
https://oauth2.googleapis.com/token - API:
https://googleads.googleapis.com/
- OAuth:
- Authentication: Bearer tokens with automatic refresh
- Rate Limits: Varies by API version and developer token status
Notificationsโ
Email Notification:
- Sent on successful connection
- Template: Integration connected notification
- Includes integration name and account details
FCM Push Notification:
- Sent to mobile devices
- Title: "Google Ads Connected"
- Body: Success message with account info
๐งช Edge Cases & Special Handlingโ
Token Expirationโ
Issue: Access tokens expire after 1 hour
Handling:
- Automatic refresh using refresh token
- Refresh happens transparently before API calls
- No user intervention required
Refresh Token Revocationโ
Issue: User may revoke access in Google account settings
Handling:
- API calls return 401 Unauthorized
- User must re-authenticate
- Requires new OAuth flow
Duplicate Connection Attemptsโ
Issue: User may try to connect already-connected account
Handling:
- Check for existing token before OAuth flow
- Redirect to success immediately if token exists
- Prevents duplicate token records
Sub-Account Validationโ
Issue: Users may provide invalid sub-account IDs
Handling:
- Validate sub-account belongs to parent
- Return 400 error for invalid relationships
- Prevents unauthorized access
State Token Tamperingโ
Issue: Malicious users may modify state parameter
Handling:
- JWT signature verification
- Return 401 for invalid signatures
- Prevents CSRF attacks
โ ๏ธ Important Notesโ
- ๐ OAuth Scopes: Requires
adwordsscope for full API access - ๐ฐ Developer Token: Requires approved Google Ads developer token
- โฑ๏ธ Token Lifetime: Access tokens expire after 1 hour
- ๐ Automatic Refresh: Tokens refreshed automatically on expiration
- ๐ Logging: All auth operations logged with initiator
- ๐จ Error Handling: OAuth errors redirect to forward_url with error reason
- ๐ Notifications: Email and FCM notifications sent on successful connection
- ๐ฏ Account Isolation: Tokens stored per DashClicks account
๐ Related Documentationโ
- Integration Overview: Google Ads Integration
- Google OAuth Docs: OAuth 2.0 for Web Server Applications
- Google Ads API: Authentication Guide
- MCC Operations: Manager Accounts
๐ฏ Authentication Checklistโ
Before Connecting:
- Set
GOOGLE_CLIENT_IDenvironment variable - Set
GOOGLE_CLIENT_SECRETenvironment variable - Configure
GOOGLE_ADS_REDIRECT_URL - Obtain approved developer token
- Enable Google Ads API in Google Cloud Console
During Connection:
- Provide
forward_urlparameter - Grant
adwordsscope permission - Verify callback receives code and state
After Connection:
- Verify token saved in database
- Check access_token and refresh_token present
- Test API call with token
- Confirm email and FCM notifications received