checkAndRefreshAccessToken
For CRMs that don't follow standard OAuth 2.0 or API key authentication patterns, developers need to provide this method in their adapter to check and refresh tokens. This interface is particularly useful for CRMs like Bullhorn that have custom authentication flows or session management requirements.
When to implement
You should implement this interface when your CRM:
- Has custom session management (like Bullhorn's session tokens)
- Requires special token refresh logic beyond standard OAuth 2.0
- Uses non-standard authentication mechanisms
- Has platform-specific token validation requirements
Input parameters
Parameter | Type | Required | Description |
---|---|---|---|
oauthApp |
Object | Yes | The OAuth application instance created by getOAuthApp() |
user |
Object | Yes | The user object containing authentication tokens and platform-specific information |
tokenLockTimeout |
Number | No | Timeout in seconds for token refresh locks (default: 10) |
User object structure
The user
object contains:
{
id: String, // User ID
accessToken: String, // Current access token
refreshToken: String, // Current refresh token
tokenExpiry: Date, // Token expiration timestamp
platform: String, // Platform name (e.g., 'bullhorn')
platformAdditionalInfo: Object // Platform-specific data
}
Return value(s)
This interface should return the updated user
object with refreshed tokens if necessary.
Return type: Promise<Object>
The returned user object should have updated:
- accessToken
- New access token if refreshed
- refreshToken
- New refresh token if refreshed
- tokenExpiry
- New expiration timestamp if refreshed
- platformAdditionalInfo
- Any platform-specific data that was updated
Implementation guidelines
- Early return: If the user object is invalid or missing required tokens, return the user object as-is
- Token validation: Check if tokens are expired or about to expire (consider a buffer time)
- Refresh logic: Implement your CRM's specific token refresh mechanism
- Error handling: Handle authentication errors gracefully
- User persistence: Save the updated user object to the database
- Lock management: Use token refresh locks if
USE_TOKEN_REFRESH_LOCK
is enabled
Default behavior
If this interface is not implemented, the system will use the default OAuth 2.0 token refresh logic in packages/core/lib/oauth.js
, which:
- Checks if tokens are expired (with 2-minute buffer)
- Uses standard OAuth 2.0 refresh token flow
- Supports token refresh locking via DynamoDB
- Handles concurrent refresh requests
Reference
let authData;
try {
const refreshTokenResponse = await axios.post(`${user.platformAdditionalInfo.tokenUrl}?grant_type=refresh_token&refresh_token=${user.refreshToken}&client_id=${process.env.BULLHORN_CLIENT_ID}&client_secret=${process.env.BULLHORN_CLIENT_SECRET}`);
authData = refreshTokenResponse.data;
} catch (e) {
const serverLoggingSettings = await getServerLoggingSettings({ user });
if (serverLoggingSettings.apiUsername && serverLoggingSettings.apiPassword) {
authData = await bullhornPasswordAuthorize(user, oauthApp, serverLoggingSettings);
} else {
throw e;
}
}
const { access_token: accessToken, refresh_token: refreshToken, expires_in: expires } = authData;
user.accessToken = accessToken;
user.refreshToken = refreshToken;
const userLoginResponse = await axios.post(`${user.platformAdditionalInfo.loginUrl}/login?version=2.0&access_token=${user.accessToken}`);
const { BhRestToken, restUrl } = userLoginResponse.data;
let updatedPlatformAdditionalInfo = user.platformAdditionalInfo;
updatedPlatformAdditionalInfo.bhRestToken = BhRestToken;
updatedPlatformAdditionalInfo.restUrl = restUrl;
// Not sure why, assigning platformAdditionalInfo first then give it another value so that it can be saved to db
user.platformAdditionalInfo = {};
user.platformAdditionalInfo = updatedPlatformAdditionalInfo;
const date = new Date();
user.tokenExpiry = date.setSeconds(date.getSeconds() + expires);
console.log('Bullhorn token refreshing finished')
if (newLock) {
const deletionStartTime = new Date().getTime();
async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 20) {
const dateNow = new Date();
const tokenExpiry = new Date(user.tokenExpiry);
const expiryBuffer = 1000 * 60 * 2; // 2 minutes => 120000ms
// Special case: Bullhorn
if (user.platform) {
const platformModule = adapterRegistry.getAdapter(user.platform);
if (platformModule.checkAndRefreshAccessToken) {
return platformModule.checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout);
}
}
// Other CRMs
if (user && user.accessToken && user.refreshToken && tokenExpiry.getTime() < (dateNow.getTime() + expiryBuffer)) {
let newLock;
// case: use dynamoDB to manage token refresh lock
if (process.env.USE_TOKEN_REFRESH_LOCK === 'true') {
const { Lock } = require('../models/dynamo/lockSchema');
// Try to atomically create lock only if it doesn't exist
try {
newLock = await Lock.create(
{
userId: user.id,
ttl: dateNow.getTime() + 1000 * 30
},
{
overwrite: false
}
);
console.log('lock created')
} catch (e) {
// If creation failed due to condition, a lock exists
if (e.name === 'ConditionalCheckFailedException' || e.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
let lock = await Lock.get({ userId: user.id });
if (!!lock?.ttl && lock.ttl < dateNow.getTime()) {
// Try to delete expired lock and create a new one atomically
try {
console.log('lock expired.')
await lock.delete();
newLock = await Lock.create(
{
userId: user.id,
ttl: dateNow.getTime() + 1000 * 30
},
{
overwrite: false
}
);
} catch (e2) {
if (e2.name === 'ConditionalCheckFailedException' || e2.__type === 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException') {
// Another process created a lock between our delete and create
lock = await Lock.get({ userId: user.id });
} else {
throw e2;
}
}
}
if (lock && !newLock) {
let processTime = 0;
let delay = 500; // Start with 500ms
const maxDelay = 8000; // Cap at 8 seconds
while (!!lock && processTime < tokenLockTimeout) {
await new Promise(resolve => setTimeout(resolve, delay));