437 lines
9.8 KiB
Markdown
437 lines
9.8 KiB
Markdown
# 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
|
|
|