9.8 KiB
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_atafter 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:
- Check OAuth scope includes refresh token
- Re-authenticate user via
/oauth/callback
Issue: "Token refresh failed"
Cause: Refresh token invalid or expired
Solution:
- User must re-authenticate
- Check OAuth app credentials in
.env
Issue: Token refreshes but still gets 401
Cause: API issue or wrong credentials
Solution:
- Check provider API status
- Verify OAuth app has correct permissions
- Check instance URL (Salesforce)
Last Updated: October 9, 2025
Version: 1.0.0