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
async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 10) {
if (!user || !user.accessToken || !user.refreshToken) {
return user;
}
const dateNow = new Date();
const expiryBuffer = 1000 * 60 * 2; // 2 minutes => 120000ms
try {
const pingResponse = await axios.get(`${user.platformAdditionalInfo.restUrl}/ping`, {
headers: {
'BhRestToken': user.platformAdditionalInfo.bhRestToken,
},
});
// Session expired
if (new Date(pingResponse.data.sessionExpires - expiryBuffer) < new Date()) {
user = await bullhornTokenRefresh(user, dateNow, tokenLockTimeout, oauthApp);
}
// Session not expired
else {
return user;
}
}
catch (e) {
// Session expired
user = await bullhornTokenRefresh(user, dateNow, tokenLockTimeout, oauthApp);
}
await user.save();
return user;
}
async function checkAndRefreshAccessToken(oauthApp, user, tokenLockTimeout = 10) {
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)) {
// case: use dynamoDB to manage token refresh lock
if (process.env.USE_TOKEN_REFRESH_LOCK === 'true') {
let lock = await Lock.get({ userId: user.id });
let newLock;
if (!!lock?.ttl && lock.ttl < dateNow.getTime()) {
await lock.delete();
lock = null;
}
if (lock) {
let processTime = 0;
while (!!lock && processTime < tokenLockTimeout) {
await new Promise(resolve => setTimeout(resolve, 2000)); // wait for 2 seconds
processTime += 2;
lock = await Lock.get({ userId: user.id });
}
// Timeout -> let users try another time
if (processTime >= tokenLockTimeout) {
throw new Error('Token lock timeout');
}
user = await UserModel.findByPk(user.id);
}
else {
newLock = await Lock.create({
userId: user.id
});
}
const token = oauthApp.createToken(user.accessToken, user.refreshToken);
const { accessToken, refreshToken, expires } = await token.refresh();
user.accessToken = accessToken;
user.refreshToken = refreshToken;
user.tokenExpiry = expires;
await user.save();
if (newLock) {
await newLock.delete();
}
}
// case: run withou token refresh lock
else {
const token = oauthApp.createToken(user.accessToken, user.refreshToken);
const { accessToken, refreshToken, expires } = await token.refresh();
user.accessToken = accessToken;
user.refreshToken = refreshToken;
user.tokenExpiry = expires;
await user.save();
}
}
return user;
}