Customizing OAuth2 Parameters in ServiceNow for NetSuite Integration
When integrating ServiceNow with external systems using OAuth2, you might encounter scenarios where the default OAuth2 implementation doesn't quite meet the requirements of your authorization server. I recently faced this challenge while integrating ServiceNow with NetSuite, where the standard OAuth2 flow needed parameter modifications to work correctly.
In this post, I'll show you how to extend ServiceNow's OAuthUtil script include and customize the interceptRequestParameters function to modify OAuth2 token request parameters. This technique is particularly useful for NetSuite integrations but applies to any OAuth2 integration requiring custom parameter handling.
Understanding the Challenge
ServiceNow's OAuth2 implementation works well for most standard OAuth2 flows. However, some systems like NetSuite have specific requirements that don't align perfectly with ServiceNow's defaults. According to Oracle's documentation, NetSuite's OAuth2 implementation requires:
- code_verifier with SHA256 hashing: NetSuite requires the code_verifier parameter to be hashed using SHA256
- code_verifier length validation: The code_verifier must be between 43 and 128 characters
- code_challenge_method must be SHA256: When using code_challenge, the code_challenge_method parameter must be set to 'S256'
- PKCE parameter relationship: If code_challenge is configured, code_challenge_method is mandatory
- Specific parameter formats for grant types
- Custom scope definitions
- Additional custom parameters for token exchange
The default ServiceNow OAuth2 flow doesn't provide a straightforward way to modify these parameters through configuration alone. This is where extending the OAuthUtil script include becomes essential.
The Solution: Extending OAuthUtil
ServiceNow provides the OAuthUtil script include as part of its OAuth2 implementation. By creating a custom script include that extends OAuthUtil, we can override the interceptRequestParameters function to modify request parameters before they're sent to the authorization server.
Here's how to implement this solution:
Step 1: Create a Custom Script Include
First, create a new Script Include that extends OAuthUtil:
var CustomOAuthUtil = Class.create();
CustomOAuthUtil.prototype = Object.extendsObject(OAuthUtil, {
initialize: function() {
OAuthUtil.prototype.initialize.call(this);
},
interceptRequestParameters: function(requestParameterMap, flow, providerName) {
// Log the incoming parameters for debugging
gs.info('CustomOAuthUtil: Intercepting parameters for provider: ' + providerName);
gs.info('CustomOAuthUtil: Flow type: ' + flow);
gs.info('CustomOAuthUtil: Original parameters: ' + JSON.stringify(requestParameterMap));
// Call the parent method first
OAuthUtil.prototype.interceptRequestParameters.call(this, requestParameterMap, flow, providerName);
// Customize parameters based on provider name
if (providerName == 'NetSuite OAuth Provider') {
this._customizeNetSuiteParameters(requestParameterMap, flow);
}
return requestParameterMap;
},
_customizeNetSuiteParameters: function(requestParameterMap, flow) {
// Handle different OAuth flows
if (flow == 'authorization_code') {
// NetSuite specific modifications for authorization code flow
// Modify grant_type format if needed
if (requestParameterMap.containsKey('grant_type')) {
var grantType = requestParameterMap.get('grant_type');
gs.info('CustomOAuthUtil: Original grant_type: ' + grantType);
requestParameterMap.put('grant_type', 'authorization_code');
}
// NetSuite requires code_verifier to be SHA256 hashed with specific length
if (requestParameterMap.containsKey('code_verifier')) {
var codeVerifier = requestParameterMap.get('code_verifier');
gs.info('CustomOAuthUtil: Processing code_verifier for NetSuite');
// Validate code_verifier length (43-128 characters)
if (codeVerifier.length < 43 || codeVerifier.length > 128) {
gs.error('CustomOAuthUtil: code_verifier length must be between 43 and 128 characters. Current length: ' + codeVerifier.length);
// Generate a new compliant code_verifier if needed
codeVerifier = this._generateCompliantCodeVerifier();
}
// Apply SHA256 hash to code_verifier
var hashedVerifier = this._hashCodeVerifier(codeVerifier);
requestParameterMap.put('code_verifier', hashedVerifier);
gs.info('CustomOAuthUtil: Applied SHA256 hash to code_verifier');
}
// NetSuite requires code_challenge_method to be SHA256 when code_challenge is present
if (requestParameterMap.containsKey('code_challenge')) {
// Ensure code_challenge_method is set to S256
requestParameterMap.put('code_challenge_method', 'S256');
gs.info('CustomOAuthUtil: Set code_challenge_method to S256 for NetSuite');
// If code_challenge exists but code_challenge_method doesn't, add it
if (!requestParameterMap.containsKey('code_challenge_method')) {
gs.warn('CustomOAuthUtil: code_challenge present without code_challenge_method - adding S256');
requestParameterMap.put('code_challenge_method', 'S256');
}
}
// Modify scope format
if (requestParameterMap.containsKey('scope')) {
var scope = requestParameterMap.get('scope');
// NetSuite expects space-separated scopes
scope = scope.replace(/,/g, ' ');
requestParameterMap.put('scope', scope);
gs.info('CustomOAuthUtil: Modified scope format for NetSuite');
}
} else if (flow == 'refresh_token') {
// Handle refresh token flow modifications
gs.info('CustomOAuthUtil: Processing refresh token flow');
// Add any refresh-specific parameter modifications here
}
// Log final parameters for debugging
gs.info('CustomOAuthUtil: Modified parameters: ' + JSON.stringify(requestParameterMap));
},
_hashCodeVerifier: function(codeVerifier) {
// Apply SHA256 hash to the code_verifier
var crypto = new GlideCertificateEncryption();
var hashedBytes = crypto.generateMac(codeVerifier, 'SHA-256', '');
// Convert to base64url encoding (URL-safe base64)
var base64 = GlideStringUtil.base64Encode(hashedBytes);
// Make it URL-safe by replacing characters
var base64url = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
return base64url;
},
_generateCompliantCodeVerifier: function() {
// Generate a code_verifier that meets NetSuite's requirements (43-128 chars)
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
var length = 64; // A safe middle ground
var verifier = '';
for (var i = 0; i < length; i++) {
verifier += chars.charAt(Math.floor(Math.random() * chars.length));
}
gs.info('CustomOAuthUtil: Generated compliant code_verifier with length: ' + verifier.length);
return verifier;
},
type: 'CustomOAuthUtil'
});
Step 2: Configure Your OAuth Provider
After creating the custom script include, you need to configure your OAuth provider to use it:
- Navigate to System OAuth > Application Registry
- Open your NetSuite OAuth provider record
- In the OAuth Entity Profile or OAuth Entity Scope, update the Script Include field to reference your custom script:
CustomOAuthUtil
Step 3: Advanced Parameter Modifications
For more complex scenarios, you can implement sophisticated parameter handling:
_customizeNetSuiteParameters: function(requestParameterMap, flow) {
// Dynamic parameter modification based on instance settings
var netsuiteConfig = this._getNetSuiteConfiguration();
if (flow == 'authorization_code') {
// Handle dynamic client assertions if needed
if (netsuiteConfig.requiresClientAssertion) {
var assertion = this._generateClientAssertion(netsuiteConfig);
requestParameterMap.put('client_assertion', assertion);
requestParameterMap.put('client_assertion_type',
'urn:ietf:params:oauth:client-assertion-type:jwt-bearer');
// Remove client_secret if using assertion
if (requestParameterMap.containsKey('client_secret')) {
requestParameterMap.remove('client_secret');
}
}
// Handle custom redirect URI encoding
if (requestParameterMap.containsKey('redirect_uri')) {
var redirectUri = requestParameterMap.get('redirect_uri');
// NetSuite might require specific encoding
var encodedUri = this._customEncodeUri(redirectUri);
requestParameterMap.put('redirect_uri', encodedUri);
}
// Add NetSuite-specific headers as parameters if needed
if (netsuiteConfig.customHeaders) {
for (var header in netsuiteConfig.customHeaders) {
requestParameterMap.put(header, netsuiteConfig.customHeaders[header]);
}
}
}
},
_getNetSuiteConfiguration: function() {
// Retrieve configuration from system properties or custom table
return {
requiresClientAssertion: gs.getProperty('netsuite.oauth.use_assertion', 'false') == 'true',
customHeaders: {
'X-NetSuite-Application-Id': gs.getProperty('netsuite.application.id', '')
}
};
},
_generateClientAssertion: function(config) {
// Implement JWT generation for client assertion
// This is a simplified example - implement proper JWT signing
var header = {
"alg": "RS256",
"typ": "JWT"
};
var claims = {
"iss": gs.getProperty('oauth.client.id'),
"sub": gs.getProperty('oauth.client.id'),
"aud": gs.getProperty('netsuite.token.endpoint'),
"exp": Math.floor(Date.now() / 1000) + 300,
"iat": Math.floor(Date.now() / 1000)
};
// In production, implement proper JWT signing
return "generated.jwt.token";
},
_customEncodeUri: function(uri) {
// Implement custom URI encoding if NetSuite requires it
// Some systems have specific encoding requirements
return encodeURI(uri).replace(/%20/g, '+');
}
Step 4: Debugging and Troubleshooting
To effectively debug OAuth2 parameter issues:
- Enable OAuth Debug Logging:
// Add comprehensive logging in your custom script
interceptRequestParameters: function(requestParameterMap, flow, providerName) {
var debugEnabled = gs.getProperty('oauth.debug.enabled', 'false') == 'true';
if (debugEnabled) {
gs.info('=== OAuth Parameter Modification Debug ===');
gs.info('Provider: ' + providerName);
gs.info('Flow: ' + flow);
gs.info('Parameters Before: ' + this._mapToString(requestParameterMap));
}
// Perform modifications
this._customizeNetSuiteParameters(requestParameterMap, flow);
if (debugEnabled) {
gs.info('Parameters After: ' + this._mapToString(requestParameterMap));
gs.info('=== End OAuth Debug ===');
}
return requestParameterMap;
},
_mapToString: function(map) {
var result = {};
var keys = map.keySet().toArray();
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = map.get(key);
// Mask sensitive values
if (key.toLowerCase().indexOf('secret') > -1 ||
key.toLowerCase().indexOf('password') > -1) {
result[key] = '***MASKED***';
} else {
result[key] = value;
}
}
return JSON.stringify(result);
}
- Test Different Scenarios:
// Create a test script include to validate parameter modifications
var OAuthParameterTest = Class.create();
OAuthParameterTest.prototype = {
testNetSuiteParameters: function() {
var customOAuth = new CustomOAuthUtil();
var testMap = new Packages.java.util.HashMap();
// Simulate authorization code flow
testMap.put('grant_type', 'authorization_code');
testMap.put('code', 'test_auth_code');
testMap.put('redirect_uri', 'https://instance.service-now.com/oauth_redirect.do');
testMap.put('code_verifier', 'test_verifier');
customOAuth.interceptRequestParameters(testMap, 'authorization_code', 'NetSuite OAuth Provider');
// Verify modifications
gs.info('Test Results:');
gs.info('code_verifier removed: ' + !testMap.containsKey('code_verifier'));
gs.info('grant_type value: ' + testMap.get('grant_type'));
},
type: 'OAuthParameterTest'
};
Best Practices and Considerations
When implementing OAuth2 parameter customization:
- Security First: Never log sensitive information like client secrets or tokens
- Provider-Specific Logic: Use provider names to ensure modifications only apply to specific integrations
- Maintain Compatibility: Always call the parent method to preserve default ServiceNow functionality
- Error Handling: Implement try-catch blocks to prevent integration failures
- Documentation: Document all parameter modifications for future maintenance
NetSuite-Specific PKCE Requirements
NetSuite has unique requirements for PKCE (Proof Key for Code Exchange) parameters that differ from standard OAuth2 implementations:
code_verifier Requirements
- SHA256 Hashing: The code_verifier must be hashed using SHA256
- Length Validation: The code_verifier must be between 43 and 128 characters
- Base64URL Encoding: The hashed value must use URL-safe base64 encoding
code_challenge and code_challenge_method Relationship
According to the PKCE specification (RFC 7636), NetSuite enforces these rules:
- code_challenge_method is mandatory: If code_challenge is present, code_challenge_method MUST also be included
- SHA256 only: The code_challenge_method parameter must be set to 'S256' (SHA256)
- Parameter association: Both parameters are associated with the authorization code for verification
Here's a complete implementation that handles these requirements:
_customizeNetSuiteParameters: function(requestParameterMap, flow) {
if (flow == 'authorization_code') {
// 1. Handle code_verifier with SHA256 hashing
if (requestParameterMap.containsKey('code_verifier')) {
var codeVerifier = requestParameterMap.get('code_verifier');
// Validate length
if (codeVerifier.length < 43 || codeVerifier.length > 128) {
gs.warn('Code verifier length (' + codeVerifier.length + ') outside required range');
// You may want to throw an error or regenerate
}
// Apply SHA256 hash
var hashedVerifier = this._hashCodeVerifier(codeVerifier);
requestParameterMap.put('code_verifier', hashedVerifier);
}
// 2. Handle code_challenge and code_challenge_method relationship
if (requestParameterMap.containsKey('code_challenge')) {
// NetSuite requires code_challenge_method when code_challenge is present
if (!requestParameterMap.containsKey('code_challenge_method')) {
gs.warn('code_challenge present without code_challenge_method - adding S256');
}
// Always set to S256 for NetSuite
requestParameterMap.put('code_challenge_method', 'S256');
}
// 3. Ensure correct grant_type format
requestParameterMap.put('grant_type', 'authorization_code');
// 4. Add NetSuite-specific parameters
if (!requestParameterMap.containsKey('access_type')) {
requestParameterMap.put('access_type', 'offline');
}
// 5. Format scope correctly for NetSuite
if (requestParameterMap.containsKey('scope')) {
var scope = requestParameterMap.get('scope');
// NetSuite expects space-separated scopes
scope = scope.replace(/,/g, ' ');
requestParameterMap.put('scope', scope);
}
}
},
_hashCodeVerifier: function(codeVerifier) {
// Use ServiceNow's crypto utilities for SHA256
var crypto = new GlideCertificateEncryption();
var hashedBytes = crypto.generateMac(codeVerifier, 'SHA-256', '');
// Convert to base64url (URL-safe base64)
var base64 = GlideStringUtil.base64Encode(hashedBytes);
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
Conclusion
Customizing OAuth2 parameters in ServiceNow by extending the OAuthUtil script include provides the flexibility needed to integrate with systems like NetSuite that have specific authentication requirements. This approach maintains the integrity of ServiceNow's OAuth2 framework while allowing for the customizations necessary for successful integration.
Remember to thoroughly test your modifications in a development environment and monitor the OAuth2 flow closely when first implementing these changes in production. The ability to intercept and modify request parameters gives you complete control over the OAuth2 token exchange process, ensuring successful integration with even the most demanding external systems.