Centralized_Reporting_Backend/docs/TOKEN_REFRESH.md
2025-10-10 12:10:33 +05:30

9.8 KiB

Token Refresh Mechanism

Overview

The n8n integration includes automatic token refresh for both Zoho and Salesforce to ensure uninterrupted API access.


How It Works

Two-Level Token Refresh Strategy

1. Proactive Refresh (Before Token Expires)

  • Checks token expiration before every API call
  • Refreshes token if it expires within 5 minutes
  • Prevents 401 errors before they happen

2. Reactive Refresh (On 401 Error)

  • If API returns 401 (Unauthorized), automatically refreshes token
  • Retries the API call once with the new token
  • Handles cases where token expired during the request

Zoho Token Refresh

Configuration Required

ZOHO_CLIENT_ID=your_zoho_client_id
ZOHO_CLIENT_SECRET=your_zoho_client_secret

Refresh Process

// 1. Check if token is expired
const isExpired = expiresAt && new Date(expiresAt) <= new Date(Date.now() + 5 * 60 * 1000);

if (isExpired) {
  // 2. Call Zoho token refresh API
  POST https://accounts.zoho.com/oauth/v2/token
  Body:
    refresh_token: xxx
    client_id: xxx
    client_secret: xxx
    grant_type: refresh_token

  // 3. Receive new access token
  Response: {
    access_token: "new_token",
    expires_in: 3600
  }

  // 4. Update database with new token
  // 5. Use new token for API call
}

Token Lifetime

  • Access Token: 1 hour (3600 seconds)
  • Refresh Token: Does not expire (unless revoked)
  • Proactive Refresh: 5 minutes before expiry

Salesforce Token Refresh

Configuration Required

SALESFORCE_CLIENT_ID=your_salesforce_client_id
SALESFORCE_CLIENT_SECRET=your_salesforce_client_secret
SALESFORCE_INSTANCE_URL=https://login.salesforce.com  # or https://test.salesforce.com for sandbox

Refresh Process

// 1. Check if token is expired
const isExpired = expiresAt && new Date(expiresAt) <= new Date(Date.now() + 5 * 60 * 1000);

if (isExpired) {
  // 2. Call Salesforce token refresh API
  POST https://login.salesforce.com/services/oauth2/token
  Body:
    grant_type: refresh_token
    refresh_token: xxx
    client_id: xxx
    client_secret: xxx

  // 3. Receive new access token
  Response: {
    access_token: "new_token",
    instance_url: "https://yourinstance.salesforce.com"
  }

  // 4. Update database with new token and instance URL
  // 5. Use new token for API call
}

Token Lifetime

  • Access Token: 2 hours (estimated, Salesforce doesn't return expires_in on refresh)
  • Refresh Token: Does not expire (unless revoked or password changed)
  • Proactive Refresh: 5 minutes before expiry

Implementation Details

File: src/integrations/n8n/handler.js

1. Get Service Tokens

async getServiceTokens(serviceName) {
  const tokenData = await userAuthTokenRepo.findByUserAndService(this.userId, serviceName);
  
  return {
    accessToken: decrypt(tokenData.accessToken),
    refreshToken: decrypt(tokenData.refreshToken),
    instanceUrl: tokenData.instanceUrl,
    expiresAt: tokenData.expiresAt  // ← Used to check expiration
  };
}

2. Refresh Zoho Token

async refreshZohoToken() {
  const { refreshToken } = await this.getServiceTokens('zoho');
  
  // Call Zoho refresh API
  const response = await axios.post('https://accounts.zoho.com/oauth/v2/token', params);
  const { access_token, expires_in } = response.data;
  
  // Update database
  await userAuthTokenRepo.upsertToken({
    userUuid: this.userId,
    serviceName: 'zoho',
    accessToken: encrypt(access_token),
    refreshToken: encrypt(refreshToken),
    expiresAt: new Date(Date.now() + expires_in * 1000)
  });
  
  return access_token;
}

3. Refresh Salesforce Token

async refreshSalesforceToken() {
  const { refreshToken, instanceUrl } = await this.getServiceTokens('salesforce');
  
  // Call Salesforce refresh API
  const response = await axios.post(`${tokenUrl}/services/oauth2/token`, params);
  const { access_token, instance_url } = response.data;
  
  // Update database
  await userAuthTokenRepo.upsertToken({
    userUuid: this.userId,
    serviceName: 'salesforce',
    accessToken: encrypt(access_token),
    refreshToken: encrypt(refreshToken),
    instanceUrl: instance_url || instanceUrl,
    expiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000)  // 2 hours
  });
  
  return access_token;
}

4. Fetch with Auto-Refresh

async fetchZohoData(service, module, options) {
  try {
    let { accessToken, expiresAt } = await this.getServiceTokens('zoho');
    
    // Proactive refresh (check expiration)
    const isExpired = expiresAt && new Date(expiresAt) <= new Date(Date.now() + 5 * 60 * 1000);
    if (isExpired) {
      logger.info('Token expired, refreshing');
      accessToken = await this.refreshZohoToken();
    }
    
    // Make API call
    const result = await this.client.fetchZohoData(service, module, accessToken, query);
    return this.normalizeResponse(result, 'zoho');
    
  } catch (error) {
    // Reactive refresh (on 401 error)
    if (error.message.includes('401')) {
      logger.info('Received 401, refreshing token');
      const newAccessToken = await this.refreshZohoToken();
      const result = await this.client.fetchZohoData(service, module, newAccessToken, query);
      return this.normalizeResponse(result, 'zoho');
    }
    throw error;
  }
}

Flow Diagram

Successful Request (Token Valid)

User Request
    ↓
Check Token Expiration
    ↓
Token Valid (> 5 min remaining)
    ↓
Fetch Data from n8n
    ↓
Return Data to User

Proactive Refresh (Token Expiring Soon)

User Request
    ↓
Check Token Expiration
    ↓
Token Expiring (< 5 min remaining)
    ↓
Refresh Token
    ↓
Update Database
    ↓
Fetch Data with New Token
    ↓
Return Data to User

Reactive Refresh (401 Error)

User Request
    ↓
Check Token Expiration
    ↓
Token Appears Valid
    ↓
Fetch Data from n8n
    ↓
Receive 401 Error
    ↓
Refresh Token
    ↓
Update Database
    ↓
Retry Fetch Data
    ↓
Return Data to User

Database Schema

user_auth_tokens Table

CREATE TABLE user_auth_tokens (
  id INT PRIMARY KEY,
  user_uuid CHAR(36),
  service_name ENUM('zoho', 'salesforce', ...),
  access_token TEXT,      -- Encrypted
  refresh_token TEXT,     -- Encrypted (used for refresh)
  instance_url VARCHAR(255),  -- Salesforce only
  expires_at DATETIME,    -- Used to check if refresh needed
  created_at DATETIME,
  updated_at DATETIME
);

Logging

Token Refresh Logs

// Before refresh
INFO: Zoho token expired or expiring soon, refreshing { userId: 'xxx' }
INFO: Refreshing Zoho token { userId: 'xxx' }

// After successful refresh
INFO: Zoho token refreshed successfully { userId: 'xxx' }

// On 401 error
INFO: Received 401 error, attempting token refresh { userId: 'xxx' }
INFO: Refreshing Zoho token { userId: 'xxx' }
INFO: Zoho token refreshed successfully { userId: 'xxx' }

// On failure
ERROR: Failed to refresh Zoho token { userId: 'xxx', error: '...' }

Error Handling

No Refresh Token Available

if (!refreshToken) {
  throw new Error('No Zoho refresh token available. Please re-authenticate.');
}

User Action Required: Re-authenticate via OAuth

Refresh Token Expired/Revoked

catch (error) {
  throw new Error(`Zoho token refresh failed: ${error.message}`);
}

Possible Causes:

  • User revoked access
  • Refresh token expired (rare)
  • OAuth app credentials changed
  • Password changed (Salesforce)

User Action Required: Re-authenticate via OAuth


Best Practices

1. 5-Minute Buffer

  • Refreshes 5 minutes before expiry
  • Prevents race conditions
  • Accounts for clock skew

2. Single Retry

  • Only retries once after token refresh
  • Prevents infinite loops
  • Fails fast if refresh doesn't work

3. Encrypted Storage

  • All tokens encrypted in database
  • Refresh tokens never logged
  • Secure token transmission

4. Database Updates

  • Updates expires_at after each refresh
  • Keeps refresh token unchanged
  • Updates instance URL (Salesforce)

Testing Token Refresh

Test Expired Token

// Manually expire token in database
UPDATE user_auth_tokens 
SET expires_at = NOW() 
WHERE user_uuid = 'your-user-id' AND service_name = 'zoho';

// Make API call - should auto-refresh
curl -X GET "http://localhost:3000/api/v1/n8n/zoho/crm/leads" \
  -H "Authorization: Bearer YOUR_JWT"

Expected Logs:

INFO: Zoho token expired or expiring soon, refreshing
INFO: Refreshing Zoho token
INFO: Zoho token refreshed successfully
INFO: Fetching Zoho data via n8n

Test 401 Error Handling

// Use invalid token
UPDATE user_auth_tokens 
SET access_token = 'invalid_encrypted_token' 
WHERE user_uuid = 'your-user-id' AND service_name = 'salesforce';

// Make API call - should catch 401 and refresh
curl -X GET "http://localhost:3000/api/v1/n8n/salesforce/crm/leads" \
  -H "Authorization: Bearer YOUR_JWT"

Expected Logs:

INFO: Fetching Salesforce data via n8n
ERROR: n8n webhook call failed (401)
INFO: Received 401 error, attempting token refresh
INFO: Refreshing Salesforce token
INFO: Salesforce token refreshed successfully
INFO: Fetching Salesforce data via n8n (retry)

Troubleshooting

Issue: "No refresh token available"

Cause: User authenticated without requesting refresh token

Solution:

  1. Check OAuth scope includes refresh token
  2. Re-authenticate user via /oauth/callback

Issue: "Token refresh failed"

Cause: Refresh token invalid or expired

Solution:

  1. User must re-authenticate
  2. Check OAuth app credentials in .env

Issue: Token refreshes but still gets 401

Cause: API issue or wrong credentials

Solution:

  1. Check provider API status
  2. Verify OAuth app has correct permissions
  3. Check instance URL (Salesforce)

Last Updated: October 9, 2025
Version: 1.0.0