Zoho - Authentication
๐ Overviewโ
Zoho integration uses OAuth 2.0 Authorization Code Flow with automatic token refresh to maintain persistent access to Zoho CRM data. The authentication system includes JWT-based state management for secure OAuth callbacks and automatic token invalidation detection.
Source Files:
- Provider:
external/Integrations/Zoho/Providers/auth/index.js - Controller:
external/Integrations/Zoho/Controller/authController/index.js - Model:
external/Integrations/Zoho/Model/authModel/index.js - Routes:
external/Integrations/Zoho/Routes/authRoutes/index.js
External API: Zoho OAuth 2.0 endpoints (domain varies by data center)
๐๏ธ Collections Usedโ
integrations.zoho.keyโ
- Operations: Create, Read, Update, Delete
- Model:
shared/models/zoho-key.js - Usage Context: Store OAuth tokens (access + refresh), API domain, and token invalidation status
Document Structure:
{
"_id": ObjectId,
"token": {
"access_token": "1000.2cf2aef...",
"refresh_token": "1000.50b9048...",
"api_domain": "https://www.zohoapis.in",
"token_type": "Bearer",
"expires_in": 3600
},
"account_id": "12345",
"owner": "user_Lwh9EzeD8",
"token_invalidated": false // Set to true when INVALID_TOKEN error occurs
}
๐ Data Flowโ
OAuth Initialization Flowโ
sequenceDiagram
participant Client as DashClicks Frontend
participant Auth as Auth Controller
participant DB as MongoDB (zoho-key)
participant ZohoAPI as Zoho OAuth API
Client->>Auth: GET /v1/e/zoho/auth?forward_url=...
Auth->>DB: Find existing token
alt Token exists and valid
DB-->>Auth: Return token
Auth-->>Client: Redirect to forward_url with token ID
else Token invalidated
DB-->>Auth: Token marked invalid
Auth->>DB: Delete invalidated token
Auth->>Auth: Create JWT state token
Auth-->>Client: Redirect to Zoho OAuth
else No token exists
Auth->>Auth: Create JWT state token (aid, uid, forward_url)
Auth->>Auth: Generate authorization URL
Auth-->>Client: Redirect to Zoho OAuth
end
OAuth Callback Flowโ
sequenceDiagram
participant ZohoAPI as Zoho OAuth
participant Callback as OAuth Callback Handler
participant AuthProvider as Auth Provider
participant DB as MongoDB (zoho-key)
participant Client as DashClicks Frontend
ZohoAPI->>Callback: GET /callback?code=...&state=JWT
Callback->>Callback: Verify JWT state token
Callback->>AuthProvider: Exchange code for tokens
AuthProvider->>ZohoAPI: POST /oauth/v2/token
ZohoAPI-->>AuthProvider: Access + refresh tokens
Callback->>DB: Check if token exists
alt Token doesn't exist
Callback->>DB: Save new token
DB-->>Callback: Token saved with ID
Callback-->>Client: Redirect with success + token ID
else Token exists
Callback-->>Client: Redirect with existing token ID
end
Token Refresh Flowโ
sequenceDiagram
participant Service as CRM Service
participant AuthProvider as Auth Provider
participant DB as MongoDB (zoho-key)
participant ZohoAPI as Zoho OAuth API
Service->>DB: Fetch stored token
DB-->>Service: Return token with refresh_token
Service->>AuthProvider: getAccessToken(refresh_token)
AuthProvider->>ZohoAPI: POST /oauth/v2/token (grant_type=refresh_token)
ZohoAPI-->>AuthProvider: New access token
AuthProvider-->>Service: Return new access_token
Note over Service: Use new access_token for API calls
๐ง Business Logic & Functionsโ
Authentication Provider Functionsโ
authorizeURL(stateParameter)โ
Purpose: Generate Zoho OAuth authorization URL with state parameter
Source: Providers/auth/index.js
External API Endpoint: N/A (URL construction only)
Parameters:
stateParameter(String) - JWT token containing user context (aid, uid, forward_url)
Returns: Promise<String> - Full authorization URL
Business Logic Flow:
- Build Authorization URL
- Constructs URL with OAuth parameters
- Includes client_id, redirect_uri, scope, response_type
- Attaches encrypted state parameter
Authorization URL Structure:
https://accounts.zoho.in/oauth/v2/auth
?response_type=code
&access_type=offline
&scope=ZohoCRM.modules.ALL
&client_id={ZOHO_CLIENT_ID}
&redirect_uri={ZOHO_REDIRECT_URL}
&state={JWT_STATE_TOKEN}
Example Usage:
const stateToken = jwt.sign(
{ aid: '12345', uid: 'user_123', forward_url: 'https://app.dashclicks.com' },
process.env.APP_SECRET,
{ expiresIn: '1h' },
);
const authUrl = await authProvider.authorizeURL(stateToken);
// Returns: https://accounts.zoho.in/oauth/v2/auth?...
Side Effects:
- โน๏ธ No database or API calls - pure URL construction
accessToken(queryCode)โ
Purpose: Exchange authorization code for OAuth tokens
Source: Providers/auth/index.js
External API Endpoint: POST https://accounts.zoho.in/oauth/v2/token
Parameters:
queryCode(String) - Authorization code from OAuth callback
Returns: Promise<Object> - Axios response with token data
{
data: {
access_token: "1000.2cf2aef...",
refresh_token: "1000.50b9048...",
api_domain: "https://www.zohoapis.in",
token_type: "Bearer",
expires_in: 3600
}
}
Business Logic Flow:
-
Encode Redirect URI
- URL-encode the redirect URI for safety
-
Build Token Request
- Prepare form-urlencoded data with grant_type, client credentials, code
-
Exchange Code for Tokens
- POST to Zoho token endpoint
- Include client_id, client_secret, redirect_uri, code
-
Validate Response
- Check for error in response data
- Reject promise if error exists
API Request Example:
POST https://accounts.zoho.in/oauth/v2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=1000.K74YDUPOSYBG...
&client_secret=e03827dcd291de64...
&redirect_uri=http://localhost:5000/v1/integrations/zoho/auth/callback
&code=1000.xxxx...
&access_type=offline
API Response Example:
{
"access_token": "1000.2cf2aef25f4404a7e3cf72bd78e535ba.fe3fb5058117fd3757a3afe82ba3d75a",
"refresh_token": "1000.50b9048fac1c119c5410bb13307eb7ff.72472e16049b4f42ab43d05983d19dda",
"api_domain": "https://www.zohoapis.in",
"token_type": "Bearer",
"expires_in": 3600
}
Error Handling:
- 400 Bad Request: Invalid authorization code or client credentials
- 401 Unauthorized: Invalid client_id or client_secret
- Network Errors: Axios request failures propagated to caller
Example Usage:
const response = await authProvider.accessToken('1000.authorization_code');
const tokens = response.data;
// Save tokens to database
Side Effects:
- โ ๏ธ External API Call: Consumes authorization code (one-time use)
- โ ๏ธ Token Generation: Creates new access and refresh tokens
getAccessToken(refreshToken)โ
Purpose: Obtain new access token using refresh token
Source: Providers/auth/index.js
External API Endpoint: POST https://accounts.zoho.in/oauth/v2/token
Parameters:
refreshToken(String) - Stored refresh token from database
Returns: Promise<Object> - New token data
{
"access_token": "1000.new_access_token...",
"api_domain": "https://www.zohoapis.in",
"token_type": "Bearer",
"expires_in": 3600
}
Business Logic Flow:
-
Build Refresh Request
- Construct URL with refresh_token, client credentials, grant_type
-
Request New Access Token
- POST to Zoho token endpoint
- Include access_type=offline for persistent access
-
Return New Token
- Extract access_token from response
- Return token data for immediate use
API Request Example:
POST https://accounts.zoho.in/oauth/v2/token
?refresh_token=1000.50b9048fac1c119c...
&access_type=offline
&client_id=1000.K74YDUPOSYBG...
&client_secret=e03827dcd291de64...
&grant_type=refresh_token
API Response Example:
{
"access_token": "1000.new_access_token_here",
"api_domain": "https://www.zohoapis.in",
"token_type": "Bearer",
"expires_in": 3600
}
Error Handling:
- 400 Invalid Grant: Refresh token expired or revoked
- 401 Unauthorized: Invalid client credentials
- Network Errors: Axios failures propagated to caller
Example Usage:
const newTokenData = await authProvider.getAccessToken(storedRefreshToken);
// Use newTokenData.access_token for API calls
Provider API Rate Limits:
- Zoho rate limits: ~100 API calls per minute per organization
- Token refresh calls count toward rate limit
Side Effects:
- โ ๏ธ External API Call: Generates new access token
- โน๏ธ Refresh Token: Remains valid (not regenerated)
Authentication Controller Functionsโ
redirectTokenUrl(req, res, next)โ
Purpose: Initiate OAuth flow or return existing valid token
Source: Controller/authController/index.js
External API Endpoint: N/A (orchestration function)
Parameters:
req.query.forward_url(String) - URL to redirect after authenticationreq.auth.account_id(String) - DashClicks account IDreq.auth.uid(String) - DashClicks user ID
Returns: HTTP redirect to OAuth URL or forward_url
Business Logic Flow:
-
Validate Forward URL
- Check if forward_url query parameter exists
- Return 400 if missing
-
Check Existing Token
- Query database for existing token by account_id and uid
-
Handle Token States
- Token Invalidated: Delete token, redirect to OAuth
- Token Valid: Redirect to forward_url with token ID
- No Token: Create JWT state token, redirect to OAuth
-
Create JWT State Token
- Encode account_id, uid, forward_url in JWT
- Sign with APP_SECRET, 1-hour expiration
-
Generate OAuth URL
- Call
authorizeURL()with state token - Redirect user to Zoho authorization page
- Call
JWT State Token Structure:
{
"aid": "12345",
"uid": "user_Lwh9EzeD8",
"forward_url": "https://app.dashclicks.com/integrations",
"iat": 1633036800,
"exp": 1633040400
}
Success Redirect Example:
https://app.dashclicks.com/integrations
?status=success
&integration=zoho
&token=6123abc... (MongoDB _id of token document)
Error Redirect Example:
https://app.dashclicks.com/integrations
?status=error
&integration=zoho
&reason=Something went wrong
Error Handling:
- Missing forward_url: Return 400 JSON response
- Database Errors: Redirect to forward_url with error status
- OAuth Errors: Redirect with error reason parameter
Example Usage:
GET /v1/e/zoho/auth?forward_url=https://app.dashclicks.com/integrations
Authorization: Bearer {jwt_token}
Side Effects:
- โ ๏ธ Database Query: Checks for existing token
- โ ๏ธ Token Deletion: Removes invalidated tokens
- โ ๏ธ HTTP Redirect: Redirects user to OAuth or forward_url
getQueryCode(req, res, next)โ
Purpose: OAuth callback handler - exchange code for tokens and save to database
Source: Controller/authController/index.js
External API Endpoint: Calls accessToken() provider function
Parameters:
req.query.code(String) - Authorization code from Zohoreq.query.state(String) - JWT state token with user context
Returns: HTTP redirect to forward_url with status
Business Logic Flow:
-
Decode JWT State Token
- Verify and decode state parameter
- Extract account_id, uid, forward_url
-
Validate Authorization Code
- Check if code query parameter exists
- Redirect with error if missing
-
Check Existing Token
- Query database for existing token
- Prevent duplicate token creation
-
Exchange Code for Tokens
- Call
accessToken()provider function - Receive access_token, refresh_token, api_domain
- Call
-
Save Token to Database
- Create document with token data, account_id, owner
- Store in
integrations.zoho.keycollection
-
Redirect with Success
- Redirect to forward_url with token ID
- Include status=success and integration=zoho
Token Document Structure (Saved to DB):
{
"token": {
"access_token": "1000.2cf2aef...",
"refresh_token": "1000.50b9048...",
"api_domain": "https://www.zohoapis.in",
"token_type": "Bearer",
"expires_in": 3600
},
"account_id": "12345",
"owner": "user_Lwh9EzeD8"
}
Success Redirect Example:
https://app.dashclicks.com/integrations
?status=success
&integration=zoho
&token=6123abc... (MongoDB document _id)
Error Redirect Example:
https://app.dashclicks.com/integrations
?status=error
&integration=zoho
&reason=Invalid authorization code
Error Handling:
- Missing Code: Redirect with error "Something went wrong!"
- JWT Verification Failed: Redirect with JWT error message
- Token Exchange Failed: Redirect with Zoho API error
- Database Save Failed: Redirect with error reason
- Existing Token: Redirect with existing token ID (success)
Example Usage:
# Zoho redirects user here after authorization
GET /v1/e/zoho/auth/callback
?code=1000.authorization_code...
&state=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Side Effects:
- โ ๏ธ External API Call: Exchanges code for tokens
- โ ๏ธ Database Write: Saves token document
- โ ๏ธ Authorization Code Consumed: One-time use, cannot be reused
- โ ๏ธ HTTP Redirect: Redirects user to forward_url
deleteAccessToken(req, res, next)โ
Purpose: Delete stored OAuth token from database
Source: Controller/authController/index.js
External API Endpoint: N/A (database operation only)
Parameters:
req.auth.account_id(String) - DashClicks account IDreq.auth.uid(String) - DashClicks user ID
Returns: JSON response with success/error status
Business Logic Flow:
-
Find Token Document
- Query database by account_id and uid
-
Validate Token Exists
- Check if document ID exists
- Return 404 if not found
-
Delete Token
- Remove document from
integrations.zoho.keycollection
- Remove document from
-
Return Success Response
- Confirm deletion with success message
Success Response:
{
"success": true,
"errno": 400,
"message": "Token has been successfully deleted"
}
Error Response (Not Found):
{
"success": false,
"errno": 400,
"message": "No token found. Make sure user authenticated via oauth"
}
Error Handling:
- 404 Not Found: Token doesn't exist in database
- Database Errors: Passed to error middleware via
next(error)
Example Usage:
DELETE /v1/e/zoho/auth
Authorization: Bearer {jwt_token}
Response:
{
"success": true,
"message": "Token has been successfully deleted"
}
Side Effects:
- โ ๏ธ Database Deletion: Permanently removes token document
- โน๏ธ No Zoho API Call: Does not revoke token on Zoho's end
- โ ๏ธ User Must Re-authenticate: Next API call will require new OAuth flow
Database Model Functionsโ
findTokenQuery(account_id, owner_id)โ
Purpose: Find stored OAuth token by account and user
Source: Model/authModel/index.js
Parameters:
account_id(String) - DashClicks account IDowner_id(String) - DashClicks user ID
Returns: Promise<Object> - Token document or error object
{
"id": "6123abc...",
"token": { ... },
"account_id": "12345",
"owner": "user_123",
"token_invalidated": false
}
Business Logic Flow:
-
Query MongoDB
- Find document matching account_id and owner
-
Handle Not Found
- Return
{ error: 'No token found' }if no document
- Return
-
Transform Document
- Convert
_idtoidstring field - Remove
_idfrom returned object
- Convert
Example Usage:
const token = await authModel.findTokenQuery('12345', 'user_123');
if (token.error) {
// No token found
} else {
// Use token.token.access_token
}
Side Effects:
- โน๏ธ Database Read: Queries MongoDB
saveRefreshToken(data)โ
Purpose: Save new OAuth token to database
Source: Model/authModel/index.js
Parameters:
data(Object) - Token document to savetoken(Object) - Token data from Zohoaccount_id(String) - Account IDowner(String) - User ID
Returns: Promise<Object> - Saved document with ID
Business Logic Flow:
-
Create MongoDB Document
- Instantiate new ZohoKey model with data
-
Save to Database
- Execute
.save()operation
- Execute
-
Transform Response
- Convert
_idtoidstring - Remove
_idfrom returned object
- Convert
Example Usage:
const savedToken = await authModel.saveRefreshToken({
token: { access_token: '...', refresh_token: '...' },
account_id: '12345',
owner: 'user_123',
});
// Returns: { id: '6123abc...', token: {...}, ... }
Side Effects:
- โ ๏ธ Database Write: Creates new document in
integrations.zoho.key
deleteToken(id)โ
Purpose: Delete token document by ID
Source: Model/authModel/index.js
Parameters:
id(String) - MongoDB document ID
Returns: Promise<Boolean> - true on success
Business Logic Flow:
-
Delete Document
- Execute
deleteOne({ _id: id })
- Execute
-
Return Success
- Resolve with
true
- Resolve with
Example Usage:
await authModel.deleteToken('6123abc...');
// Token deleted from database
Side Effects:
- โ ๏ธ Database Deletion: Permanently removes document
๐ Integration Pointsโ
Internal Servicesโ
Used By:
- CRM data export endpoints (
/v1/e/zoho/export/:type) - All Zoho API calls require valid OAuth token
Token Refresh Pattern:
Every API call to Zoho CRM automatically refreshes the access token:
// 1. Fetch stored token from database
const storedToken = await authModel.findTokenQuery(account_id, owner);
// 2. Use refresh_token to get new access_token
const newTokenData = await authProvider.getAccessToken(storedToken.token.refresh_token);
// 3. Use new access_token for API call
const response = await axios.get(zohoApiUrl, {
headers: { Authorization: `Bearer ${newTokenData.access_token}` },
});
External API Dependenciesโ
Provider: Zoho Corporation
Endpoints:
- Authorization:
https://accounts.zoho.in/oauth/v2/auth - Token Exchange:
https://accounts.zoho.in/oauth/v2/token
Authentication: OAuth 2.0 Authorization Code Flow
Rate Limits: ~100 API calls per minute per organization
OAuth Scopes:
ZohoCRM.modules.ALL- Full access to all CRM modules
๐งช Edge Cases & Special Handlingโ
Token Invalidation Detectionโ
Issue: Zoho tokens can be invalidated (user revokes access, password change, etc.)
Detection:
- Error response contains
code: 'INVALID_TOKEN' - Caught in main error handler in
index.js
Handling:
if (error.response.data.code === 'INVALID_TOKEN') {
// Mark all tokens for account as invalidated
await zohoKeys.updateMany({ account_id: accountId }, { $set: { token_invalidated: true } });
error.message = 'TOKEN_INVALIDATED';
}
- Sets
token_invalidated: trueon all account tokens - Next OAuth flow will delete invalidated tokens
- User must re-authenticate
JWT State Token Expirationโ
Issue: State token in OAuth flow has 1-hour expiration
Handling:
- JWT verification will fail if expired
- User redirected to forward_url with error
- User must restart OAuth flow
Multiple Zoho Data Centersโ
Issue: Zoho has different data centers (.com, .in, .eu, etc.)
Handling:
- Environment variables allow configuration per deployment
api_domainreturned in token response used for API calls- Example:
https://www.zohoapis.invshttps://www.zohoapis.com
Duplicate Token Preventionโ
Issue: User might complete OAuth flow multiple times
Handling:
- Callback handler checks for existing token before saving
- If token exists, returns existing token ID
- Prevents duplicate token documents
โ ๏ธ Important Notesโ
- ๐ Access Token Expiry: Access tokens expire after 1 hour (3600 seconds)
- ๐ Automatic Refresh: Every API call refreshes access token using refresh_token
- ๐ Data Center Aware: API domain varies by Zoho data center (stored in token document)
- ๐ JWT State Security: State parameter signed with APP_SECRET, 1-hour expiration
- ๐จ Token Invalidation: System detects and marks invalidated tokens automatically
- ๐ One-Time Code: Authorization codes can only be exchanged once
- ๐ Offline Access:
access_type=offlineensures refresh tokens are provided - ๐พ Token Storage: Tokens stored per user (account_id + owner combination)
๐ Related Documentationโ
- Integration Overview: Zoho Integration
- Zoho OAuth Docs: Zoho OAuth 2.0 Documentation
- CRM Data Export: ./crm-data.md
- Database Collections: MongoDB Collections