Skip to content

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

  1. Early return: If the user object is invalid or missing required tokens, return the user object as-is
  2. Token validation: Check if tokens are expired or about to expire (consider a buffer time)
  3. Refresh logic: Implement your CRM's specific token refresh mechanism
  4. Error handling: Handle authentication errors gracefully
  5. User persistence: Save the updated user object to the database
  6. 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;
}