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

        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));