# 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 ```env ZOHO_CLIENT_ID=your_zoho_client_id ZOHO_CLIENT_SECRET=your_zoho_client_secret ``` ### Refresh Process ```javascript // 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 ```env 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 ```javascript // 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```sql 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 ```javascript if (!refreshToken) { throw new Error('No Zoho refresh token available. Please re-authenticate.'); } ``` **User Action Required**: Re-authenticate via OAuth ### Refresh Token Expired/Revoked ```javascript 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 ```javascript // 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 ```javascript // 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