The CreatorCon Call for Content is officially open! Get started here.

Saura Sambit
Tera Expert

OAuth 2.0 is a widely adopted authentication method across numerous ServiceNow integrations. Whether it’s for a custom scripted integration or an Integration Hub spoke, OAuth token flows are frequently utilised. Two of the most commonly employed flows are the ‘Client Credentials’ and ‘Authorization Code’ token flows.

 

  • Client Credentials Flow: Used for server-to-server integrations, where the application directly obtains an access token using its client ID and secret.
  • Authorization Code Flow: Involves user consent; the application exchanges an authorization code for an access token after user authentication.

 

The Client Credentials flow is the simplest to implement, as it allows fetching a token from the third-party application without extra authentication. However, with the Authorization Code flow, issues can arise when the token expires, requiring explicit user authentication from the third-party application to obtain a new token.

 

Issue

 

Recently, during an integration with SharePoint Online, we observed that access tokens generated through the Authorization Code flow frequently expired, requiring us to repeatedly contact a SharePoint admin to re-authorize and refresh the token.

 

Observation

 

When examining the tokens in the OAuth Credentials [oauth_credential] table, we found that only the ‘refresh token’ was present, with no ‘access token’ available. Surprisingly, even the out-of-the-box spoke actions failed to automatically generate a new access token using the existing refresh token.

 

Wondering

 

This led me to wonder how ServiceNow’s outbound email functionality operates, as it also uses Microsoft Graph to send emails via Microsoft Exchange, relying on tokens generated through the Authorization Code flow. During my research, I found a support article – KB0823190 – which mentions the below as its 9th point.

  • ‘As long there is a valid refresh token is available, the scheduled job named “Refresh email access token” will run every 3 minutes to check and get the new Access token.’

 

Further research

 

Upon investigating the instance further, I discovered the scheduled job in question, which runs a check every 3 minutes and calls a script include to reissue both the access and refresh tokens. This approach ensures that the tokens in ServiceNow never truly expire, keeping the email functionality active year-round, unless the credentials expire on the Microsoft side.

 

refresh_email_token_script.png

 

 

My initial plan was to leverage the out-of-the-box script include, EmailOAuthHelper, in a separate scheduled job to refresh the SharePoint tokens. However, ServiceNow has restricted it to handle token refreshes solely for email accounts. Nonetheless, it served as valuable inspiration for our approach.

 

Implementation

 

Create a modified version of the EmailOAuthHelper script include, removing any email account references and generalizing its functions, allowing it to refresh tokens from any table within ServiceNow.

 

oauth_token_refresher_script_include.png

 

The script is about 100 lines, so the copyable version can be found here.

 

Next steps

 

After creating the aforementioned script include in the instance, scheduled jobs can be set up to utilize it, automatically retrieving both access and refresh tokens from third-party applications. This approach significantly extends the longevity of integrations without requiring manual token refreshes.

 

  • Example of the scheduled job for refreshing the SharePoint tokens.

 

sharepoint_refresh_token_job.png

 

  • The GlideRecord used in the above script.

 

sharepoint_token_record.png

 

Conclusion

 

The scheduled script, combined with the script include, effectively replicate the functionality of the ‘Get OAuth Token’ related link available across various records in ServiceNow.

Comments
Jon G1
Kilo Sage

Thanks for the write up and sample script! This might just be what I'm looking for.

VedanshR
Tera Explorer

Steps:

Create a new script include:

 

VedanshR_1-1730989623822.png

 

 

Next steps

 

After creating the aforementioned script include in the instance, scheduled jobs can be set up to utilize it, automatically retrieving both access and refresh tokens from third-party applications. This approach significantly extends the longevity of integrations without requiring manual token refreshes.

 

  • Example of the scheduled job for refreshing the SharePoint tokens.

 

VedanshR_2-1730989623889.png

 

 

  • The GlideRecord used in the above script.

 

VedanshR_3-1730989623899.png

 

NBeheydt
Tera Expert

We don't have a script include called EmailOAuthHelper in our instance and the link to the copyable version is blocked.  Can you attach the script to this thread?

Saura Sambit
Tera Expert

@NBeheydt Here you go.

 

var OauthRefreshTokenHandler = Class.create();
OauthRefreshTokenHandler.prototype = {
    initialize: function() {},

    isExpired: function(expiresIn, withinSeconds) {
        if (expiresIn > withinSeconds)
            return false;
        return true;
    },

    getToken: function(requestorId, oauthProfileId) {
        if (!requestorId || !oauthProfileId)
            return null;
        var client = new sn_auth.GlideOAuthClient();
        return client.getToken(requestorId, oauthProfileId);
    },

    refreshAccessToken: function(requestorId, oauthProfileId, token) {
        if (!(token && requestorId && oauthProfileId))
            return;

        var tokenRequest = new sn_auth.GlideOAuthClientRequest();
        tokenRequest.setGrantType("refresh_token");
        tokenRequest.setRefreshToken(token.getRefreshToken());
        tokenRequest.setParameter('oauth_requestor_context', 'email');
        tokenRequest.setParameter('oauth_requestor', requestorId);
        tokenRequest.setParameter('oauth_provider_profile', oauthProfileId);

        var oAuthClient = new sn_auth.GlideOAuthClient();
        var tokenResponse = oAuthClient.requestTokenByRequest(null, tokenRequest);
        var error = tokenResponse.getErrorMessage();
        if (error)
            gs.warn("Error:" + tokenResponse.getErrorMessage());
    },

    invokeClientCredentials: function(requestor, oauth_provider_profile) {
        var requestor_context = "email";
        var tokenRequest = new sn_auth.GlideOAuthClientRequest();

        tokenRequest.setParameter('oauth_requestor_context', requestor_context);
        tokenRequest.setParameter('oauth_requestor', requestor);
        tokenRequest.setParameter('oauth_provider_profile', oauth_provider_profile);

        var oAuthClient = new sn_auth.GlideOAuthClient();
        var tokenResponse = oAuthClient.requestTokenByRequest(null, tokenRequest);

        var errorMsg = tokenResponse.getErrorMessage();

        if (errorMsg) {
            gs.log('OAuth authentication failed for ' + requestor);
        }
    },

    checkAndRefreshAccessToken: function(requestorGr, oauth_profile) {

        var accountMsg = requestorGr.getValue("name");
        if (!accountMsg)
            accountMsg = requestorGr.getUniqueValue();

        var token = this.getToken(requestorGr.getUniqueValue(), oauth_profile);
        if (!token) {
            gs.error("Access token is not fetched =" + requestorGr.getUniqueValue());
            return;
        }
        var accessToken = token.getAccessToken();
        if (accessToken) {
            if (!this.isExpired(token.getExpiresIn(), 300))
                return;
        }

        var oauthEntityGr = new GlideRecord("oauth_entity_profile");
        if (oauthEntityGr.get(oauth_profile) && oauthEntityGr.getValue('grant_type') == 'client_credentials') {
            this.invokeClientCredentials(requestorGr.getUniqueValue(), oauth_profile);
            return;
        }

        if (!token.getRefreshToken()) {
            gs.error("No OAuth refresh token. Manual reauthorization required. " + accountMsg);
            return;
        }

        if (this.isExpired(token.getRefreshTokenExpiresIn(), 0)) {
            gs.error("OAuth refresh token is expired. Manual reauthorization required. " + accountMsg);
            return;
        }

        gs.info("Refreshing oauth access token. " + accountMsg);
        this.refreshAccessToken(requestorGr.getUniqueValue(), oauth_profile, token);
    },

    type: 'OauthRefreshTokenHandler'
};

 

zeeshan ahmed
Tera Contributor

hi @Saura Sambit 

 

Getting Access Token from the available Refresh token is very much clear from your Article , Thanks for that.

 

Please provide some insight what needs to be done for Refresh Token as it will expire in 100 days 

 

Just an additional question : If i am using OOB Okta Spoke do i need to still use you article to generate new Access Tokens ??

 

Thanks in advance

Version history
Last update:
‎09-24-2024 08:06 PM
Updated by:
Contributors