From 0a6b68e9b7f0b17c08a355092e9c43f2ccab6499 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 28 Oct 2025 16:12:14 +0530 Subject: [PATCH 1/3] utility for parsing capture-context-response --- .../index.mustache | 1 + src/index.js | 1 + .../CaptureContextParsingUtility.js | 131 ++++++++++++++++++ .../capturecontext/PublicKeyApiController.js | 85 ++++++++++++ 4 files changed, 218 insertions(+) create mode 100644 src/utilities/capturecontext/CaptureContextParsingUtility.js create mode 100644 src/utilities/capturecontext/PublicKeyApiController.js diff --git a/generator/cybersource-javascript-template/index.mustache b/generator/cybersource-javascript-template/index.mustache index c6cab1a1..1f6b26fa 100644 --- a/generator/cybersource-javascript-template/index.mustache +++ b/generator/cybersource-javascript-template/index.mustache @@ -101,5 +101,6 @@ exports.ExternalLoggerWrapper = require('./authentication/logging/ExternalLoggerWrapper.js'); exports.JWEUtility = require('./utilities/JWEUtility.js'); exports.SdkTracker = require('./utilities/tracking/SdkTracker.js'); + exports.CaptureContextParsingUtility = require('./utilities/capturecontext/CaptureContextParsingUtility.js'); return exports;<={{ }}=> })); diff --git a/src/index.js b/src/index.js index 6edf8b99..c5580656 100644 --- a/src/index.js +++ b/src/index.js @@ -8105,5 +8105,6 @@ exports.ExternalLoggerWrapper = require('./authentication/logging/ExternalLoggerWrapper.js'); exports.JWEUtility = require('./utilities/JWEUtility.js'); exports.SdkTracker = require('./utilities/tracking/SdkTracker.js'); + exports.CaptureContextParsingUtility = require('./utilities/capturecontext/CaptureContextParsingUtility.js'); return exports; })); diff --git a/src/utilities/capturecontext/CaptureContextParsingUtility.js b/src/utilities/capturecontext/CaptureContextParsingUtility.js new file mode 100644 index 00000000..65151b4e --- /dev/null +++ b/src/utilities/capturecontext/CaptureContextParsingUtility.js @@ -0,0 +1,131 @@ +'use strict'; + +const JWTUtility = require('../../authentication/util/jwt/JWTUtility'); +const JWTExceptions = require('../../authentication/util/jwt/JWTExceptions'); +const Cache = require('../../authentication/util/Cache'); +const PublicKeyApiController = require('./PublicKeyApiController'); + +/** + * Parses a capture context JWT response and optionally verifies its signature + * @param {string} jwtValue - The JWT token to parse + * @param {Object} merchantConfig - The merchant configuration object + * @param {boolean} verifyJwt - Whether to verify the JWT signature + * @param {Function} callback - Callback function (error, result) + */ +function parseCaptureContextResponse(jwtValue, merchantConfig, verifyJwt, callback) { + if (typeof callback !== 'function') { + throw new Error('callback parameter must be a function'); + } + + if (!jwtValue) { + return callback(JWTExceptions.InvalidJwtException('JWT value is null or undefined')); + } + + if (!merchantConfig) { + return callback(new Error('merchantConfig is required')); + } + + let parsedJwt; + try { + parsedJwt = JWTUtility.parse(jwtValue); + } catch (parseError) { + return callback(parseError); + } + + if (!verifyJwt) { + return callback(null, parsedJwt.payload); + } + + const header = parsedJwt.header; + const kid = header.kid; + + if (!kid) { + return callback(JWTExceptions.JwtSignatureValidationException('JWT header missing key ID (kid) field')); + } + + const runEnvironment = merchantConfig.getRunEnvironment(); + if (!runEnvironment) { + return callback(new Error('Run environment not found in merchant config')); + } + + let publicKey; + let isPublicKeyFromCache = false; + + try { + publicKey = Cache.getPublicKeyFromCache(runEnvironment, kid); + isPublicKeyFromCache = true; + } catch (cacheError) { + isPublicKeyFromCache = false; + } + + if (!isPublicKeyFromCache) { + return fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, false, callback); + } + + try { + JWTUtility.verifyJwt(jwtValue, publicKey); + return callback(null, parsedJwt.payload); + } catch (verificationError) { + return fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, true, callback); + } +} + +/** + * Fetches public key from API and performs JWT verification + * @param {string} jwtValue - The JWT token + * @param {Object} parsedJwt - The parsed JWT object + * @param {string} kid - The key ID + * @param {string} runEnvironment - The runtime environment + * @param {boolean} isRetryAfterCacheFailure - Whether this is a retry after cache failure + * @param {Function} callback - Callback function + * @private + */ +function fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, isRetryAfterCacheFailure, callback) { + fetchPublicKeyFromApi(kid, runEnvironment, (fetchError, publicKey) => { + if (fetchError) { + return callback(fetchError); + } + + try { + JWTUtility.verifyJwt(jwtValue, publicKey); + return callback(null, parsedJwt.payload); + } catch (verificationError) { + return callback(JWTExceptions.JwtSignatureValidationException('JWT validation failed')); + } + }); +} + + +/** + * Fetches public key from API and adds it to cache + * @param {string} kid - The key ID + * @param {string} runEnvironment - The runtime environment + * @param {Function} callback - Callback function (error, publicKey) + * @private + */ +function fetchPublicKeyFromApi(kid, runEnvironment, callback) { + PublicKeyApiController.fetchPublicKey(kid, runEnvironment, (error, publicKey) => { + if (error) { + if (error.message && error.message.includes('Invalid Runtime URL')) { + return callback(new Error('Invalid Runtime URL in Merchant Config')); + } else if (error.message && error.message.includes('Network error')) { + return callback(new Error('Error while trying to retrieve public key from server')); + } else if (error.message && error.message.includes('Failed to parse JWK')) { + return callback(JWTExceptions.InvalidJwkException('JWK received from server cannot be parsed correctly', error)); + } else { + return callback(new Error('Error while trying to retrieve public key from server')); + } + } + + try { + Cache.addPublicKeyToCache(runEnvironment, kid, publicKey); + callback(null, publicKey); + } catch (cacheError) { + callback(null, publicKey); + } + }); +} + +module.exports = { + parseCaptureContextResponse +}; diff --git a/src/utilities/capturecontext/PublicKeyApiController.js b/src/utilities/capturecontext/PublicKeyApiController.js new file mode 100644 index 00000000..39dea98d --- /dev/null +++ b/src/utilities/capturecontext/PublicKeyApiController.js @@ -0,0 +1,85 @@ +'use strict'; + +const axios = require('axios'); +const JWTUtility = require('../../authentication/util/jwt/JWTUtility'); + +/** + * Fetches the public key for the given key ID (kid) from the specified run environment. + * + * @param {string} kid - The key ID for which to fetch the public key. + * @param {string} runEnvironment - The environment domain (e.g., 'apitest.cybersource.com'). + * @param {function(Error, string):void} callback - Callback function called with (error, publicKey). + * If successful, error is null and publicKey is a PEM-formatted string. + * If an error occurs, error is an Error object and publicKey is undefined. + */ +function fetchPublicKey(kid, runEnvironment, callback) { + if (!kid) { + return callback(new Error('kid parameter is required')); + } + + if (!runEnvironment) { + return callback(new Error('runEnvironment parameter is required')); + } + + if (typeof callback !== 'function') { + return callback(new Error('callback parameter must be a function')); + } + + const url = `https://${runEnvironment}/flex/v2/public-keys/${kid}`; + + const axiosConfig = { + method: 'GET', + url: url, + headers: { + 'Accept': 'application/json' + } + }; + + axios.request(axiosConfig) + .then(response => { + try { + if (!response.data) { + return callback(new Error('Empty response received from public key endpoint')); + } + + let jwkJsonString; + if (typeof response.data === 'string') { + jwkJsonString = response.data; + } else { + jwkJsonString = JSON.stringify(response.data); + } + + const publicKey = JWTUtility.getRSAPublicKeyFromJwk(jwkJsonString); + if (!publicKey) { + return callback(new Error('Invalid public key received from JWK')); + } + callback(null, publicKey); + + } catch (parseError) { + const error = new Error(`Failed to parse JWK response: ${parseError.message}`); + error.originalError = parseError; + callback(error); + } + }) + .catch(axiosError => { + let error; + + if (axiosError.response) { + const status = axiosError.response.status; + const statusText = axiosError.response.statusText; + error = new Error(`HTTP ${status}: ${statusText} - Failed to fetch public key for kid: ${kid}`); + error.status = status; + error.response = axiosError.response; + } else if (axiosError.request) { + error = new Error(`No response received - Failed to fetch public key for kid: ${kid}`); + error.code = axiosError.code; + } else { + error = new Error(`Request setup error: ${axiosError.message}`); + } + + error.originalError = axiosError; + callback(error); + }); +} + +module.exports = { fetchPublicKey }; From 82cada0d062be9db8f927d9d8ea79816ceb668e3 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Wed, 29 Oct 2025 14:00:16 +0530 Subject: [PATCH 2/3] removed unused parameter --- src/utilities/capturecontext/CaptureContextParsingUtility.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utilities/capturecontext/CaptureContextParsingUtility.js b/src/utilities/capturecontext/CaptureContextParsingUtility.js index 65151b4e..193eef9a 100644 --- a/src/utilities/capturecontext/CaptureContextParsingUtility.js +++ b/src/utilities/capturecontext/CaptureContextParsingUtility.js @@ -76,11 +76,10 @@ function parseCaptureContextResponse(jwtValue, merchantConfig, verifyJwt, callba * @param {Object} parsedJwt - The parsed JWT object * @param {string} kid - The key ID * @param {string} runEnvironment - The runtime environment - * @param {boolean} isRetryAfterCacheFailure - Whether this is a retry after cache failure * @param {Function} callback - Callback function * @private */ -function fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, isRetryAfterCacheFailure, callback) { +function fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, callback) { fetchPublicKeyFromApi(kid, runEnvironment, (fetchError, publicKey) => { if (fetchError) { return callback(fetchError); From a2dacda48dfc171c3d19757b73e5240728ee5fe0 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Wed, 29 Oct 2025 14:11:32 +0530 Subject: [PATCH 3/3] minor fix --- src/utilities/capturecontext/CaptureContextParsingUtility.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/capturecontext/CaptureContextParsingUtility.js b/src/utilities/capturecontext/CaptureContextParsingUtility.js index 193eef9a..259571b6 100644 --- a/src/utilities/capturecontext/CaptureContextParsingUtility.js +++ b/src/utilities/capturecontext/CaptureContextParsingUtility.js @@ -59,14 +59,14 @@ function parseCaptureContextResponse(jwtValue, merchantConfig, verifyJwt, callba } if (!isPublicKeyFromCache) { - return fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, false, callback); + return fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, callback); } try { JWTUtility.verifyJwt(jwtValue, publicKey); return callback(null, parsedJwt.payload); } catch (verificationError) { - return fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, true, callback); + return fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, callback); } }