Skip to main content

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:

  1. 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:

  1. Encode Redirect URI

    • URL-encode the redirect URI for safety
  2. Build Token Request

    • Prepare form-urlencoded data with grant_type, client credentials, code
  3. Exchange Code for Tokens

    • POST to Zoho token endpoint
    • Include client_id, client_secret, redirect_uri, code
  4. 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:

  1. Build Refresh Request

    • Construct URL with refresh_token, client credentials, grant_type
  2. Request New Access Token

    • POST to Zoho token endpoint
    • Include access_type=offline for persistent access
  3. 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 authentication
  • req.auth.account_id (String) - DashClicks account ID
  • req.auth.uid (String) - DashClicks user ID

Returns: HTTP redirect to OAuth URL or forward_url

Business Logic Flow:

  1. Validate Forward URL

    • Check if forward_url query parameter exists
    • Return 400 if missing
  2. Check Existing Token

    • Query database for existing token by account_id and uid
  3. 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
  4. Create JWT State Token

    • Encode account_id, uid, forward_url in JWT
    • Sign with APP_SECRET, 1-hour expiration
  5. Generate OAuth URL

    • Call authorizeURL() with state token
    • Redirect user to Zoho authorization page

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 Zoho
  • req.query.state (String) - JWT state token with user context

Returns: HTTP redirect to forward_url with status

Business Logic Flow:

  1. Decode JWT State Token

    • Verify and decode state parameter
    • Extract account_id, uid, forward_url
  2. Validate Authorization Code

    • Check if code query parameter exists
    • Redirect with error if missing
  3. Check Existing Token

    • Query database for existing token
    • Prevent duplicate token creation
  4. Exchange Code for Tokens

    • Call accessToken() provider function
    • Receive access_token, refresh_token, api_domain
  5. Save Token to Database

    • Create document with token data, account_id, owner
    • Store in integrations.zoho.key collection
  6. 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 ID
  • req.auth.uid (String) - DashClicks user ID

Returns: JSON response with success/error status

Business Logic Flow:

  1. Find Token Document

    • Query database by account_id and uid
  2. Validate Token Exists

    • Check if document ID exists
    • Return 404 if not found
  3. Delete Token

    • Remove document from integrations.zoho.key collection
  4. 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 ID
  • owner_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:

  1. Query MongoDB

    • Find document matching account_id and owner
  2. Handle Not Found

    • Return { error: 'No token found' } if no document
  3. Transform Document

    • Convert _id to id string field
    • Remove _id from returned object

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 save
    • token (Object) - Token data from Zoho
    • account_id (String) - Account ID
    • owner (String) - User ID

Returns: Promise<Object> - Saved document with ID

Business Logic Flow:

  1. Create MongoDB Document

    • Instantiate new ZohoKey model with data
  2. Save to Database

    • Execute .save() operation
  3. Transform Response

    • Convert _id to id string
    • Remove _id from returned object

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:

  1. Delete Document

    • Execute deleteOne({ _id: id })
  2. Return Success

    • Resolve with true

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: true on 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_domain returned in token response used for API calls
  • Example: https://www.zohoapis.in vs https://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=offline ensures refresh tokens are provided
  • ๐Ÿ’พ Token Storage: Tokens stored per user (account_id + owner combination)

๐Ÿ’ฌ

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM