Oh no - an unsupported Authentication Method!

Oh no - an unsupported Authentication Method!

Sometimes an OAuth integration starts with a simple client_id and client_secret. Other times, the provider expects cryptographic signing, JWT assertions, and elliptic curve encryption.

Apple Business Manager is one of those integrations.

The good news is that once the moving parts are broken down, the architecture becomes much easier to understand and reusable for other modern integrations.

This guide walks through a practical ServiceNow design for handling ES256-signed JWT authentication using a MID Server.

Along the way, this article also explains:

What a JWT actually is

What ES256 signing means

Why cryptographic signing matters

What JAR files are and why the MID Server needs them

How to safely obtain external Java libraries

How ServiceNow and MID Servers work together securely


First: What Is a JWT?

JWT stands for JSON Web Token. At its core, a JWT is simply a structured block of JSON data that can be securely transmitted between systems.

A JWT normally contains three parts:

Header — describes the signing algorithm

Payload — contains claims and identity information

Signature — proves the token was created by a trusted source

Important concept:

A JWT is not automatically encrypted. In many OAuth flows, the JWT is signed rather than encrypted.

Signing proves authenticity. It tells Apple:

“This request genuinely came from the owner of the private key.”

That is exactly what Apple Business Manager expects during OAuth authentication.


What Does ES256 Mean?

ES256 is the signing algorithm Apple requires for JWT assertions.

Under the hood:

ES = Elliptic Curve Digital Signature Algorithm (ECDSA)

256 = SHA-256 hashing

Compared to older RSA-based signing approaches, elliptic curve cryptography provides strong security with smaller key sizes and efficient performance.

In practical terms:

A private key signs the JWT

Apple validates the signature using the public key

The token proves the caller is trusted

ServiceNow itself is intentionally restrictive around direct cryptographic access. That is where the MID Server becomes useful.


The Architecture

Rather than pushing the entire integration onto the MID Server, the cleaner pattern is:

Let ServiceNow orchestrate the integration while the MID Server performs only the signing operation.

ComponentResponsibility
ServiceNowConfiguration, token caching, API orchestration, and Apple API calls
MID ServerGenerate ES256-signed JWT assertions

This separation keeps the design clean and secure.


The Authentication Flow

ServiceNow checks for a cached Apple access token

If the token exists and remains valid, ServiceNow reuses it

If the token expired, ServiceNow retrieves the Apple private key securely

ServiceNow sends signing inputs to the MID Server

The MID Server generates an ES256-signed JWT assertion

The assertion returns to ServiceNow

ServiceNow exchanges the assertion for an OAuth bearer token

ServiceNow calls Apple Business Manager APIs directly

Key design idea:

The MID Server is not the integration runtime. It acts as a focused cryptographic signing service.


Understanding the Private Key

Apple provides a private key when creating API credentials.

This private key is the most sensitive part of the integration. Anyone with access to it can generate valid JWT assertions.

The key normally arrives in PEM format:

-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----

Or:

-----BEGIN EC PRIVATE KEY-----...-----END EC PRIVATE KEY-----

These are simply different formatting standards for representing cryptographic key material.

The implementation should support both.


Where Should the Private Key Be Stored?

A common mistake is storing private keys inside plain text system properties.

Instead:

Store the private key inside the ServiceNow Credentials table

Reference the credential using a system property

Restrict credential access carefully

ValueStorage Location
Apple private keyCredential password field
Credential sys_idSystem property
Apple team IDSystem property
Apple key IDSystem property
MID Server nameSystem property

What Are JAR Files?

If this is the first time working with MID Server Java dependencies, the term JAR file can look intimidating.

A JAR file (Java ARchive) is simply a packaged Java library.

Think of it as:

“A reusable bundle of Java code that applications can import and use, not unlike Script Includes.”

In this implementation, the MID Server uses external Java libraries to:

Create JWT objects

Generate signatures

Handle cryptographic operations

Without these libraries, the MID Server would not understand classes like:

Packages.com.nimbusds.jwt.SignedJWT

Required MID Server Libraries

This implementation uses the Nimbus JOSE JWT library ecosystem:

nimbus-jose-jwt

json-smart

accessors-smart

These libraries should be uploaded into the ServiceNow JAR Files table and synced to the MID Server.


How to Safely Obtain JAR Files

This part matters more than many developers realize.

JAR files are executable Java libraries. Downloading random JARs from untrusted sites introduces serious supply chain risk.

Safe sources include:

The vendor’s official GitHub repository

Maven Central

Official release pages

Avoid:

Unknown mirror sites

Forum attachments

Random “free JAR download” websites

Good practice:

Verify version numbers, checksums, and release signatures whenever possible before uploading libraries into production MID Servers.


Generating the JWT Assertion

Once the libraries are available, the MID Server can create and sign the JWT assertion.

The JWT includes claims such as:

ClaimPurpose
issApple Team ID
subApple Client ID
audApple token endpoint
iatIssued timestamp
expExpiration timestamp
jtiUnique token identifier

The assertion should remain short-lived. This implementation uses a 15 minute expiry window.


Example MID Server Signing Flow

var signedJWT = new Packages.com.nimbusds.jwt.SignedJWT(header, claimsSet);var signer = new Packages.com.nimbusds.jose.crypto.ECDSASigner(privateKey);signedJWT.sign(signer);return String(signedJWT.serialize());

This is the moment the unsigned JWT becomes cryptographically trusted.


Token Caching Matters

Without caching, every API call would trigger:

A new JWT assertion

A new OAuth request

Another MID Server signing operation

That increases latency and unnecessary load.

Instead, ServiceNow stores the OAuth bearer token temporarily and refreshes it only when necessary.

var token = new ABMAuth().getAccessToken(false);

Consumers of the integration never need to care whether the token came from cache or from a fresh authentication cycle.


Security Considerations

Never log private keys

Never log bearer tokens

Restrict credential access carefully

Use short-lived JWT assertions

Use trusted MID Servers only

Review third-party JAR sources carefully

The JWT assertion is temporary, but while valid it can still request OAuth tokens. Treat it as sensitive.


The Bigger Pattern

Apple Business Manager is not unique here.

Many modern integrations now rely on signed JWT assertions instead of static shared secrets.

Once this architecture exists, the same approach becomes reusable across:

Financial APIs

Healthcare integrations

Enterprise identity providers

Zero trust authentication systems

The pattern remains the same:

Store sensitive material securely

Use ServiceNow for orchestration

Delegate cryptographic signing to the MID Server

Cache tokens intelligently

Keep the consumer experience simple


Final Thoughts

ES256 JWT authentication looks intimidating the first time it appears in an integration project.

There are acronyms everywhere:

JWT

ECDSA

PEM

OAuth

JAR files

PKCS#8

But underneath the terminology, the architecture is surprisingly logical:

ServiceNow manages the workflow

The MID Server handles cryptographic signing

Apple validates the signature

OAuth tokens unlock the API

Eventually, all of that complexity becomes:

var token = new ABMAuth().getAccessToken(false);

That single line hides a modern, secure, reusable authentication architecture.

💡
ABMMidAppleToken (MID SERVER SCRIPT INCLUDE)

var ABMMidAppleToken = Class.create();ABMMidAppleToken.prototype = { initialize: function() { this.clientId = probe.getParameter('client_id'); this.teamId = probe.getParameter('team_id'); this.keyId = probe.getParameter('key_id'); this.privateKeyPem = probe.getParameter('private_key_pem');
this.appleAudience = 'https://account.apple.com/auth/oauth2/v2/token'; },
getClientAssertion: function() { try { this._validateInputs();
var clientAssertion = this._createClientAssertion();
return JSON.stringify({ success: true, client_assertion: clientAssertion }); } catch (e) { return JSON.stringify({ success: false, error_message: String(e) }); } },
_validateInputs: function() { var missing = [];
if (!this.clientId) { missing.push('client_id'); }
if (!this.teamId) { missing.push('team_id'); }
if (!this.keyId) { missing.push('key_id'); }
if (!this.privateKeyPem) { missing.push('private_key_pem'); }
if (missing.length > 0) { throw 'Missing MID token inputs: ' + missing.join(', '); } },
_createClientAssertion: function() { var privateKey = this._loadPrivateKey(this.privateKeyPem);
var nowMillis = new Packages.java.util.Date().getTime(); var issuedAt = new Packages.java.util.Date(nowMillis); var expiresAt = new Packages.java.util.Date(nowMillis + (15 * 60 * 1000));
var header = new Packages.com.nimbusds.jose.JWSHeader .Builder(Packages.com.nimbusds.jose.JWSAlgorithm.ES256) .keyID(this.keyId) .build();
var claimsSet = new Packages.com.nimbusds.jwt.JWTClaimsSet.Builder() .issuer(this.teamId) .subject(this.clientId) .claim('aud', this.appleAudience) .issueTime(issuedAt) .expirationTime(expiresAt) .jwtID(String(Packages.java.util.UUID.randomUUID().toString())) .build();
var signedJWT = new Packages.com.nimbusds.jwt.SignedJWT(header, claimsSet); var signer = new Packages.com.nimbusds.jose.crypto.ECDSASigner(privateKey);
signedJWT.sign(signer);
return String(signedJWT.serialize()); },
_loadPrivateKey: function(privateKeyPem) { var pem = String(privateKeyPem || '') .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .trim();
if (!pem) { throw 'Private key PEM is empty'; }
if (pem.indexOf('-----BEGIN PRIVATE KEY-----') === 0) { return this._loadPkcs8PrivateKey(pem); }
if (pem.indexOf('-----BEGIN EC PRIVATE KEY-----') === 0) { var keyBytes = this._pemToByteArray( pem, '-----BEGIN EC PRIVATE KEY-----', '-----END EC PRIVATE KEY-----' );
try { return this._loadPkcs8DerBytes(keyBytes); } catch (pkcs8Error) { return this._loadSec1EcPrivateKeyBytes(keyBytes); } }
throw 'Unsupported private key format. Expected BEGIN PRIVATE KEY or BEGIN EC PRIVATE KEY.'; },
_loadPkcs8PrivateKey: function(pem) { var keyBytes = this._pemToByteArray( pem, '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----' );
return this._loadPkcs8DerBytes(keyBytes); },
_loadPkcs8DerBytes: function(bytes) { var keySpec = new Packages.java.security.spec.PKCS8EncodedKeySpec( this._toJavaByteArray(bytes) );
var keyFactory = Packages.java.security.KeyFactory.getInstance('EC');
return keyFactory.generatePrivate(keySpec); },
_loadSec1EcPrivateKeyBytes: function(sec1Bytes) { var privateScalarBytes = this._extractSec1PrivateScalar(sec1Bytes);
var BigInteger = Packages.java.math.BigInteger; var privateScalar = new BigInteger(1, this._toJavaByteArray(privateScalarBytes));
var AlgorithmParameters = Packages.java.security.AlgorithmParameters; var ECGenParameterSpec = Packages.java.security.spec.ECGenParameterSpec; var ECParameterSpec = Packages.java.security.spec.ECParameterSpec; var ECPrivateKeySpec = Packages.java.security.spec.ECPrivateKeySpec; var KeyFactory = Packages.java.security.KeyFactory;
var params = AlgorithmParameters.getInstance('EC'); params.init(new ECGenParameterSpec('secp256r1'));
var ecSpec = params.getParameterSpec(ECParameterSpec); var privateKeySpec = new ECPrivateKeySpec(privateScalar, ecSpec);
return KeyFactory.getInstance('EC').generatePrivate(privateKeySpec); },
_extractSec1PrivateScalar: function(bytes) { var offset = 0;
if (bytes[offset++] !== 0x30) { throw 'Invalid SEC1 EC key. Expected DER SEQUENCE. Found tag: 0x' + this._toHex(bytes[offset - 1]); }
var seqLenInfo = this._readDerLength(bytes, offset); offset = seqLenInfo.nextOffset;
if (bytes[offset++] !== 0x02) { throw 'Invalid SEC1 EC key. Expected INTEGER version. Found tag: 0x' + this._toHex(bytes[offset - 1]); }
var versionLenInfo = this._readDerLength(bytes, offset); offset = versionLenInfo.nextOffset + versionLenInfo.length;
var tag = bytes[offset];
if (tag !== 0x04) { throw 'Invalid SEC1 EC key. Expected OCTET STRING private key. Found tag: 0x' + this._toHex(tag) + ' at offset ' + offset; }
offset++;
var privateLenInfo = this._readDerLength(bytes, offset); offset = privateLenInfo.nextOffset;
var privateBytes = [];
for (var i = 0; i < privateLenInfo.length; i++) { privateBytes.push(bytes[offset + i]); }
if (privateBytes.length === 0) { throw 'Invalid SEC1 EC key. Private scalar is empty.'; }
while (privateBytes.length < 32) { privateBytes.unshift(0); }
if (privateBytes.length > 32) { throw 'Invalid SEC1 EC key. Private scalar is longer than 32 bytes: ' + privateBytes.length; }
return privateBytes; },
_pemToByteArray: function(pem, beginMarker, endMarker) { var cleaned = String(pem || '') .replace(beginMarker, '') .replace(endMarker, '') .replace(/\r/g, '') .replace(/\n/g, '') .replace(/\s/g, '');
if (!cleaned) { throw 'Private key PEM is empty after removing header/footer'; }
var invalidMatch = cleaned.match(/[^A-Za-z0-9+/=]/);
if (invalidMatch) { throw 'Private key contains invalid Base64 character "' + invalidMatch[0] + '" at position ' + invalidMatch.index; }
var decoded = Packages.java.util.Base64.getDecoder().decode(cleaned); var bytes = [];
for (var i = 0; i < decoded.length; i++) { var value = Number(decoded[i]);
if (value < 0) { value += 256; }
bytes.push(value); }
return bytes; },
_readDerLength: function(bytes, offset) { var first = bytes[offset++];
if (first < 0x80) { return { length: first, nextOffset: offset }; }
var byteCount = first & 0x7f; var length = 0;
for (var i = 0; i < byteCount; i++) { length = (length << 8) + bytes[offset++]; }
return { length: length, nextOffset: offset }; },
_toJavaByteArray: function(bytes) { var ByteType = Packages.java.lang.Byte.TYPE; var output = Packages.java.lang.reflect.Array.newInstance(ByteType, bytes.length);
for (var i = 0; i < bytes.length; i++) { var value = Number(bytes[i]);
if (value > 127) { value = value - 256; }
output[i] = value; }
return output; },
_toHex: function(value) { value = Number(value);
if (value < 0) { value += 256; }
var hex = value.toString(16).toUpperCase();
if (hex.length < 2) { hex = '0' + hex; }
return hex; },
type: 'ABMMidAppleToken'};

ABMAuth (Script include)

var ABMAuth = Class.create();ABMAuth.prototype = { initialize: function() { this.credentialSysId = gs.getProperty('abm.credential_sys_id');
/* * For Apple Business Manager, client_id and team_id are the same * for this implementation. */ this.teamId = gs.getProperty('abm.team_id'); this.clientId = gs.getProperty('abm.team_id');
this.keyId = gs.getProperty('abm.key_id'); this.midServerName = gs.getProperty('abm.mid_server_name');
/* * Confirmed scope for Apple Business Manager. */ this.scope = 'business.api';
this.responseTimeoutSeconds = 60;
/* * If the cached token expires within the next 5 minutes, * treat it as expired and refresh it. */ this.tokenSafetyBufferSeconds = 300;
this.cacheTokenProperty = 'abm.cached_access_token'; this.cacheExpiresAtProperty = 'abm.cached_access_token_expires_at'; },
getAccessToken: function(forceRefresh) { this._validateProperties();
if (forceRefresh !== true) { var cachedToken = this._getCachedAccessToken();
if (cachedToken) { return cachedToken; } }
return this._refreshAccessToken(); },
clearCachedAccessToken: function() { this._setProperty(this.cacheTokenProperty, ''); this._setProperty(this.cacheExpiresAtProperty, '');
gs.info('ABM cached access token cleared.'); },
_refreshAccessToken: function() { var credential = this._getCredential(); var clientAssertion = this._getClientAssertionFromMid(credential); var tokenResponse = this._requestAppleAccessTokenResponse(credential.client_id, clientAssertion);
this._storeCachedAccessToken( tokenResponse.access_token, parseInt(tokenResponse.expires_in, 10) || 3600 );
return tokenResponse.access_token; },
_getCachedAccessToken: function() { var token = gs.getProperty(this.cacheTokenProperty, '');
if (!token) { return ''; }
var expiresAtValue = gs.getProperty(this.cacheExpiresAtProperty, '');
if (!expiresAtValue) { return ''; }
var expiresAt;
try { expiresAt = new GlideDateTime(expiresAtValue); } catch (e) { gs.warn('ABM cached token expiry could not be parsed. Refreshing token. Value: ' + expiresAtValue); return ''; }
var usableUntil = new GlideDateTime(); usableUntil.addSeconds(this.tokenSafetyBufferSeconds);
if (!expiresAt.after(usableUntil)) { return ''; }
return token; },
_storeCachedAccessToken: function(accessToken, expiresInSeconds) { if (!accessToken) { throw 'Cannot store empty ABM access token'; }
var safeExpiresIn = parseInt(expiresInSeconds, 10) || 3600;
/* * Store the usable expiry minus the safety buffer. * Example: Apple returns 3600 seconds. * Stored expiry becomes now + 3300 seconds. */ var expiresAt = new GlideDateTime(); expiresAt.addSeconds(safeExpiresIn - this.tokenSafetyBufferSeconds);
this._setProperty(this.cacheTokenProperty, accessToken); this._setProperty(this.cacheExpiresAtProperty, expiresAt.getValue());
gs.info('ABM access token cached in system properties. Expires at: ' + expiresAt.getDisplayValue()); },
_setProperty: function(name, value) { var prop = new GlideRecord('sys_properties'); prop.addQuery('name', name); prop.setLimit(1); prop.query();
if (prop.next()) { prop.setValue('value', value); prop.update(); return; }
prop.initialize(); prop.setValue('name', name); prop.setValue('value', value); prop.insert(); },
_getCredential: function() { var gr = new GlideRecord('discovery_credentials');
if (!gr.get(this.credentialSysId)) { throw 'ABM credential not found: ' + this.credentialSysId; }
if (gr.getValue('active') == 'false') { throw 'ABM credential is inactive: ' + this.credentialSysId; }
var privateKeyPem = '';
try { privateKeyPem = String(gr.password.getDecryptedValue() || ''); } catch (e) { throw 'Unable to decrypt ABM credential password using getDecryptedValue(): ' + e; }
privateKeyPem = privateKeyPem .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .trim();
if (!privateKeyPem) { throw 'ABM credential password/private key is empty after decryption'; }
if ( privateKeyPem.indexOf('-----BEGIN EC PRIVATE KEY-----') !== 0 && privateKeyPem.indexOf('-----BEGIN PRIVATE KEY-----') !== 0 ) { throw 'Unsupported ABM private key format. Expected BEGIN EC PRIVATE KEY or BEGIN PRIVATE KEY.'; }
return { client_id: this.clientId, private_key_pem: privateKeyPem }; },
_getClientAssertionFromMid: function(credential) { var probeName = 'ABM Apple Client Assertion Request ' + gs.generateGUID();
var jsProbe = new JavascriptProbe(this.midServerName); jsProbe.setName(probeName);
jsProbe.addParameter('client_id', credential.client_id); jsProbe.addParameter('team_id', this.teamId); jsProbe.addParameter('key_id', this.keyId); jsProbe.addParameter('private_key_pem', credential.private_key_pem);
jsProbe.setJavascript( "var tokenGenerator = new ABMMidAppleToken(); tokenGenerator.getClientAssertion();" );
var eccSysId = jsProbe.create(); var result = this._waitForProbeResult(eccSysId);
if (!result.success) { throw result.error_message || 'MID client assertion generation failed'; }
if (!result.client_assertion) { throw 'MID response did not include client_assertion'; }
return result.client_assertion; },
_requestAppleAccessTokenResponse: function(clientId, clientAssertion) { var request = new sn_ws.RESTMessageV2(); request.setEndpoint('https://account.apple.com/auth/oauth2/token'); request.setHttpMethod('POST'); request.setRequestHeader('Host', 'account.apple.com'); request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); request.setRequestHeader('Accept', 'application/json');
var body = [ 'grant_type=' + encodeURIComponent('client_credentials'), 'client_id=' + encodeURIComponent(clientId), 'client_assertion_type=' + encodeURIComponent('urn:ietf:params:oauth:client-assertion-type:jwt-bearer'), 'client_assertion=' + encodeURIComponent(clientAssertion), 'scope=' + encodeURIComponent(this.scope) ].join('&');
request.setRequestBody(body);
var response = request.execute(); var status = response.getStatusCode(); var responseBody = response.getBody();
gs.info('ABM Apple token response status: ' + status);
if (status < 200 || status >= 300) { gs.error('ABM Apple token response body: ' + responseBody); throw 'Apple token request failed. HTTP ' + status + ': ' + responseBody; }
var parsed = JSON.parse(responseBody);
if (!parsed.access_token) { throw 'Apple token response did not include access_token: ' + responseBody; }
return parsed; },
_waitForProbeResult: function(outputEccSysId) { var deadline = new GlideDateTime(); deadline.addSeconds(this.responseTimeoutSeconds);
while (new GlideDateTime().before(deadline)) { var input = new GlideRecord('ecc_queue'); input.addQuery('queue', 'input'); input.addQuery('response_to', outputEccSysId); input.orderByDesc('sys_created_on'); input.setLimit(1); input.query();
if (input.next()) { var payload = String(input.getValue('payload') || ''); return this._parseProbePayload(payload); }
gs.sleep(1000); }
throw 'Timed out waiting for MID client assertion response after ' + this.responseTimeoutSeconds + ' seconds. ECC output sys_id: ' + outputEccSysId; },
_parseProbePayload: function(payload) { if (!payload) { return { success: false, error_message: 'MID returned empty ECC payload' }; }
/* * JavascriptProbe payloads can vary slightly by release. * Try direct JSON first, then extract JSON from the payload. */ try { return JSON.parse(payload); } catch (ignoreDirectJson) { // Continue to JSON extraction. }
var match = payload.match(/\{[\s\S]*\}/);
if (!match) { return { success: false, error_message: 'Could not find JSON object in MID ECC payload: ' + payload }; }
try { return JSON.parse(match[0]); } catch (e) { return { success: false, error_message: 'Could not parse MID JSON response: ' + e + '. Payload: ' + payload }; } },
_validateProperties: function() { var missing = [];
if (!this.credentialSysId) { missing.push('abm.credential_sys_id'); }
if (!this.teamId) { missing.push('abm.team_id'); }
if (!this.clientId) { missing.push('abm.team_id used as client_id'); }
if (!this.keyId) { missing.push('abm.key_id'); }
if (!this.midServerName) { missing.push('abm.mid_server_name'); }
if (missing.length > 0) { throw 'Missing ABM configuration properties: ' + missing.join(', '); } },
type: 'ABMAuth'};