From c9d2df65d3f3a24a72ffc3fe5c2e31ad62f33e5a Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 28 Oct 2025 16:02:58 +0530 Subject: [PATCH 1/5] Added utility for handling JWTs --- src/authentication/util/Cache.js | 15 ++ src/authentication/util/Constants.js | 3 +- src/authentication/util/jwt/JWTExceptions.js | 55 +++++ src/authentication/util/jwt/JWTUtility.js | 236 +++++++++++++++++++ 4 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 src/authentication/util/jwt/JWTExceptions.js create mode 100644 src/authentication/util/jwt/JWTUtility.js diff --git a/src/authentication/util/Cache.js b/src/authentication/util/Cache.js index f18c4c42..dda333a7 100644 --- a/src/authentication/util/Cache.js +++ b/src/authentication/util/Cache.js @@ -242,3 +242,18 @@ function validateCertificateExpiry(certificate, keyAlias, cacheKey, merchantConf } } }; + +exports.addPublicKeyToCache = function(runEnvironment, keyId, publicKey) { + const cacheKey = Constants.PUBLIC_KEY_CACHE_IDENTIFIER + "_" + runEnvironment + "_" + keyId; + cache.put(cacheKey, publicKey); +}; + +exports.getPublicKeyFromCache = function(runEnvironment, keyId) { + const cacheKey = Constants.PUBLIC_KEY_CACHE_IDENTIFIER + "_" + runEnvironment + "_" + keyId; + + if (cache.size() === 0 || !cache.get(cacheKey)) { + throw new Error("Public key not found in cache for [" + runEnvironment + ", " + keyId + "]"); + } + + return cache.get(cacheKey); +}; diff --git a/src/authentication/util/Constants.js b/src/authentication/util/Constants.js index 50ccb1cc..f3368072 100644 --- a/src/authentication/util/Constants.js +++ b/src/authentication/util/Constants.js @@ -108,5 +108,6 @@ module.exports = { STATUS500 : "Internal Server Error", STATUS502 : "Bad Gateway", STATUS503 : "Service Unavailable", - STATUS504 : "Gateway Timeout" + STATUS504 : "Gateway Timeout", + PUBLIC_KEY_CACHE_IDENTIFIER : "FlexV2PublicKeys" }; diff --git a/src/authentication/util/jwt/JWTExceptions.js b/src/authentication/util/jwt/JWTExceptions.js new file mode 100644 index 00000000..e78062e6 --- /dev/null +++ b/src/authentication/util/jwt/JWTExceptions.js @@ -0,0 +1,55 @@ +'use strict'; + +/** + * InvalidJwkException - Error class for invalid JWK (JSON Web Key) + * @param {string} message - Error message describing the invalid JWK + * @param {Error} [cause] - Optional underlying cause of the error + * @constructor + */ +exports.InvalidJwkException = function (message, cause) { + Error.captureStackTrace(this, this.constructor); + this.name = 'InvalidJwkException'; + this.message = message; + if (cause) { + this.cause = cause; + this.stack = this.stack + '\nCaused by: ' + cause.stack; + } +}; +exports.InvalidJwkException.prototype = Object.create(Error.prototype); +exports.InvalidJwkException.prototype.constructor = exports.InvalidJwkException; + +/** + * InvalidJwtException - Error class for invalid JWT token + * @param {string} message - Error message describing the invalid JWT token + * @param {Error} [cause] - Optional underlying cause of the error + * @constructor + */ +exports.InvalidJwtException = function (message, cause) { + Error.captureStackTrace(this, this.constructor); + this.name = 'InvalidJwtException'; + this.message = message; + if (cause) { + this.cause = cause; + this.stack = this.stack + '\nCaused by: ' + cause.stack; + } +}; +exports.InvalidJwtException.prototype = Object.create(Error.prototype); +exports.InvalidJwtException.prototype.constructor = exports.InvalidJwtException; + +/** + * JwtSignatureValidationException - Error class for JWT signature validation failures + * @param {string} message - Error message describing the signature validation failure + * @param {Error} [cause] - Optional underlying cause of the error + * @constructor + */ +exports.JwtSignatureValidationException = function (message, cause) { + Error.captureStackTrace(this, this.constructor); + this.name = 'JwtSignatureValidationException'; + this.message = message; + if (cause) { + this.cause = cause; + this.stack = this.stack + '\nCaused by: ' + cause.stack; + } +}; +exports.JwtSignatureValidationException.prototype = Object.create(Error.prototype); +exports.JwtSignatureValidationException.prototype.constructor = exports.JwtSignatureValidationException; diff --git a/src/authentication/util/jwt/JWTUtility.js b/src/authentication/util/jwt/JWTUtility.js new file mode 100644 index 00000000..9a55bd63 --- /dev/null +++ b/src/authentication/util/jwt/JWTUtility.js @@ -0,0 +1,236 @@ +'use strict' + +const forge = require('node-forge'); +const crypto = require('crypto'); +const JWTExceptions = require('./JWTExceptions.js'); + +// Supported JWT algorithms and their corresponding hash algorithms +const SUPPORTED_ALGORITHMS = { + 'RS256': 'sha256', + 'RS384': 'sha384', + 'RS512': 'sha512' +}; + +// Error messages constants +const ERROR_MESSAGES = { + UNSUPPORTED_ALGORITHM: (algorithm) => + `Unsupported JWT algorithm: ${algorithm}. Supported algorithms: ${Object.keys(SUPPORTED_ALGORITHMS).join(', ')}`, + MISSING_ALGORITHM: 'JWT header missing algorithm (alg) field', + NO_PUBLIC_KEY: 'No public key found', + INVALID_PUBLIC_KEY_FORMAT: 'Invalid public key format. Expected JWK object or JSON string.', + INVALID_RSA_KEY: 'Public key must be an RSA key (kty: RSA)', + MISSING_RSA_PARAMS: 'Invalid RSA JWK: missing required parameters (n, e)' +}; + +/** + * Decodes a base64url encoded string to a JSON object + * @param {string} base64urlString - The base64url encoded string + * @param {string} partName - Name of the JWT part for error reporting (e.g., 'header', 'payload') + * @returns {Object} - The decoded JSON object + * @throws {InvalidJwtException} - If decoding or parsing fails + * @private + */ +function decodeJwtPart(base64urlString, partName) { + try { + const jsonString = Buffer.from(base64urlString, 'base64url').toString('utf8'); + return JSON.parse(jsonString); + } catch (decodeErr) { + if (decodeErr.name === 'SyntaxError') { + throw new JWTExceptions.InvalidJwtException(`Invalid JSON in JWT ${partName}`, decodeErr); + } + throw new JWTExceptions.InvalidJwtException(`Failed to decode JWT ${partName} from base64url`, decodeErr); + } +} + +/** + * Validates and parses a JWK public key + * @param {Object|string} publicKey - The RSA public key (JWK object or JSON string) + * @returns {Object} - The validated JWK object + * @throws {InvalidJwkException} - If the public key is invalid + * @private + */ +function validateAndParseJwk(publicKey) { + let jwkKey; + + if (typeof publicKey === 'string') { + try { + jwkKey = JSON.parse(publicKey); + } catch (parseErr) { + throw new JWTExceptions.InvalidJwkException('Invalid public key JSON format', parseErr); + } + } else if (typeof publicKey === 'object' && publicKey !== null && publicKey.kty) { + jwkKey = publicKey; + } else { + throw new JWTExceptions.InvalidJwkException(ERROR_MESSAGES.INVALID_PUBLIC_KEY_FORMAT); + } + + if (jwkKey.kty !== 'RSA') { + throw new JWTExceptions.InvalidJwkException(ERROR_MESSAGES.INVALID_RSA_KEY); + } + + if (!jwkKey.n || !jwkKey.e) { + throw new JWTExceptions.InvalidJwkException(ERROR_MESSAGES.MISSING_RSA_PARAMS); + } + + return jwkKey; +} + +/** + * Converts JWK RSA parameters to PEM format public key + * @param {Object} jwkKey - The JWK object with RSA parameters + * @returns {string} - The PEM formatted public key + * @throws {InvalidJwkException} - If key conversion fails + * @private + */ +function convertJwkToPem(jwkKey) { + let n, e; + try { + n = Buffer.from(jwkKey.n, 'base64url'); + e = Buffer.from(jwkKey.e, 'base64url'); + } catch (decodeErr) { + + throw new JWTExceptions.InvalidJwkException('Invalid base64url encoding in JWK parameters', decodeErr); + } + + let publicKeyForge; + try { + publicKeyForge = forge.pki.rsa.setPublicKey( + forge.util.createBuffer(n).toHex(), + forge.util.createBuffer(e).toHex() + ); + } catch (keyErr) { + throw new JWTExceptions.InvalidJwkException('Failed to create RSA public key from JWK', keyErr); + } + + try { + return forge.pki.publicKeyToPem(publicKeyForge); + } catch (pemErr) { + throw new JWTExceptions.InvalidJwkException('Failed to convert public key to PEM format', pemErr); + } +} + +/** + * Parses a JWT token and extracts its header, payload, and signature components + * @param {string} jwtToken - The JWT token to parse + * @returns {Object} - Object containing header, payload, signature, and raw parts + * @throws {InvalidJwtException} - If the JWT token is invalid or malformed + */ +exports.parse = function (jwtToken) { + if (!jwtToken) { + throw new JWTExceptions.InvalidJwtException('JWT token is null or undefined'); + } + + if (typeof jwtToken !== 'string') { + throw new JWTExceptions.InvalidJwtException('JWT token must be a string'); + } + + const tokenParts = jwtToken.split('.'); + if (tokenParts.length !== 3) { + throw new JWTExceptions.InvalidJwtException('Invalid JWT token format: expected 3 parts separated by dots'); + } + + // Validate that all parts are non-empty + if (!tokenParts[0] || !tokenParts[1] || !tokenParts[2]) { + throw new JWTExceptions.InvalidJwtException('Invalid JWT token: one or more parts are empty'); + } + + try { + // Use helper function for consistent base64url decoding + const header = decodeJwtPart(tokenParts[0], 'header'); + const payload = decodeJwtPart(tokenParts[1], 'payload'); + const signature = tokenParts[2]; + + return { + header, + payload, + signature, + // Include raw base64url parts for signature verification + rawHeader: tokenParts[0], + rawPayload: tokenParts[1] + }; + } catch (err) { + // Re-throw our custom exceptions + if (err.name === 'InvalidJwtException') { + throw err; + } + throw new JWTExceptions.InvalidJwtException('Malformed JWT cannot be parsed', err); + } +} + +/** + * Verifies a JWT token using an RSA public key + * @param {string} jwtToken - The JWT token to verify + * @param {Object|string} publicKey - The RSA public key (JWK object or JSON string) + * @throws {InvalidJwtException} - If JWT parsing fails + * @throws {JwtSignatureValidationException} - If signature verification fails + */ +exports.verifyJwt = function (jwtToken, publicKey) { + if (!publicKey) { + throw new JWTExceptions.JwtSignatureValidationException('No public key found'); + } + + if (!jwtToken) { + throw new JWTExceptions.JwtSignatureValidationException('JWT token is null or undefined'); + } + + const parsedJwt = exports.parse(jwtToken); + const { header, _, signature, rawHeader, rawPayload } = parsedJwt; + + const algorithm = header.alg; + if (!algorithm) { + throw new JWTExceptions.JwtSignatureValidationException(ERROR_MESSAGES.MISSING_ALGORITHM); + } + + const hashAlgorithm = SUPPORTED_ALGORITHMS[algorithm]; + if (!hashAlgorithm) { + throw new JWTExceptions.JwtSignatureValidationException(ERROR_MESSAGES.UNSUPPORTED_ALGORITHM(algorithm)); + } + + // Validate and parse the JWK public key - let InvalidJwkException bubble up + const jwkKey = validateAndParseJwk(publicKey); + + // Convert JWK to PEM format for verification - let InvalidJwkException bubble up + const publicKeyPem = convertJwkToPem(jwkKey); + const signingInput = rawHeader + '.' + rawPayload; + + let signatureBuffer; + try { + signatureBuffer = Buffer.from(signature, 'base64url'); + } catch (sigDecodeErr) { + throw new JWTExceptions.JwtSignatureValidationException('Invalid base64url encoding in JWT signature', sigDecodeErr); + } + + let isValid; + try { + const verifier = crypto.createVerify(hashAlgorithm.toUpperCase()); + verifier.update(signingInput); + isValid = verifier.verify(publicKeyPem, signatureBuffer); + } catch (verifyErr) { + throw new JWTExceptions.JwtSignatureValidationException('Signature verification failed', verifyErr); + } + + if (!isValid) { + throw new JWTExceptions.JwtSignatureValidationException('JWT signature verification failed'); + } +} + +/** + * Extracts an RSA public key from a JWK JSON string + * @param {string} jwkJsonString - The JWK JSON string containing the RSA key + * @returns {Object} - The RSA public key object + * @throws {InvalidJwkException} - If the JWK is invalid or not an RSA key + */ +exports.getRSAPublicKeyFromJwk = function (jwkJsonString) { + try { + const jwkData = JSON.parse(jwkJsonString); + if (jwkData.kty !== 'RSA') { + throw new JWTExceptions.InvalidJwkException('JWK Algorithm mismatch. Expected algorithm : RSA'); + } + return jwkData; + } catch (err) { + if (err.name === 'InvalidJwkException') { + throw err; + } + throw new JWTExceptions.InvalidJwkException('Failed to parse JWK or extract RSA public key', err); + } +} From e2c4b7932d90fad046b39cf676c1348a1cff38b4 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Wed, 29 Oct 2025 13:55:45 +0530 Subject: [PATCH 2/5] fixed custom error creation --- src/authentication/util/jwt/JWTExceptions.js | 75 +++++++++++--------- src/authentication/util/jwt/JWTUtility.js | 5 +- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/authentication/util/jwt/JWTExceptions.js b/src/authentication/util/jwt/JWTExceptions.js index e78062e6..84e08e72 100644 --- a/src/authentication/util/jwt/JWTExceptions.js +++ b/src/authentication/util/jwt/JWTExceptions.js @@ -1,22 +1,51 @@ 'use strict'; +function createCustomError(name) { + function CustomError(message, cause) { + const instance = Reflect.construct(Error, [message], this.constructor); + + Reflect.setPrototypeOf(instance, Reflect.getPrototypeOf(this)); + + instance.name = name; + + Error.captureStackTrace(instance, this.constructor); + + if (cause) { + instance.cause = cause; + if (cause.stack) { + instance.stack = instance.stack + '\nCaused by: ' + cause.stack; + } + } + + return instance; + } + + CustomError.prototype = Object.create(Error.prototype, { + constructor: { + value: CustomError, + enumerable: false, + writable: true, + configurable: true + }, + name: { + value: name, + enumerable: false, + writable: true, + configurable: true + } + }); + + Reflect.setPrototypeOf(CustomError, Error); + return CustomError; +} + /** * InvalidJwkException - Error class for invalid JWK (JSON Web Key) * @param {string} message - Error message describing the invalid JWK * @param {Error} [cause] - Optional underlying cause of the error * @constructor */ -exports.InvalidJwkException = function (message, cause) { - Error.captureStackTrace(this, this.constructor); - this.name = 'InvalidJwkException'; - this.message = message; - if (cause) { - this.cause = cause; - this.stack = this.stack + '\nCaused by: ' + cause.stack; - } -}; -exports.InvalidJwkException.prototype = Object.create(Error.prototype); -exports.InvalidJwkException.prototype.constructor = exports.InvalidJwkException; +exports.InvalidJwkException = createCustomError('InvalidJwkException'); /** * InvalidJwtException - Error class for invalid JWT token @@ -24,17 +53,7 @@ exports.InvalidJwkException.prototype.constructor = exports.InvalidJwkException; * @param {Error} [cause] - Optional underlying cause of the error * @constructor */ -exports.InvalidJwtException = function (message, cause) { - Error.captureStackTrace(this, this.constructor); - this.name = 'InvalidJwtException'; - this.message = message; - if (cause) { - this.cause = cause; - this.stack = this.stack + '\nCaused by: ' + cause.stack; - } -}; -exports.InvalidJwtException.prototype = Object.create(Error.prototype); -exports.InvalidJwtException.prototype.constructor = exports.InvalidJwtException; +exports.InvalidJwtException = createCustomError('InvalidJwtException'); /** * JwtSignatureValidationException - Error class for JWT signature validation failures @@ -42,14 +61,4 @@ exports.InvalidJwtException.prototype.constructor = exports.InvalidJwtException; * @param {Error} [cause] - Optional underlying cause of the error * @constructor */ -exports.JwtSignatureValidationException = function (message, cause) { - Error.captureStackTrace(this, this.constructor); - this.name = 'JwtSignatureValidationException'; - this.message = message; - if (cause) { - this.cause = cause; - this.stack = this.stack + '\nCaused by: ' + cause.stack; - } -}; -exports.JwtSignatureValidationException.prototype = Object.create(Error.prototype); -exports.JwtSignatureValidationException.prototype.constructor = exports.JwtSignatureValidationException; +exports.JwtSignatureValidationException = createCustomError('JwtSignatureValidationException'); diff --git a/src/authentication/util/jwt/JWTUtility.js b/src/authentication/util/jwt/JWTUtility.js index 9a55bd63..e3580583 100644 --- a/src/authentication/util/jwt/JWTUtility.js +++ b/src/authentication/util/jwt/JWTUtility.js @@ -172,9 +172,8 @@ exports.verifyJwt = function (jwtToken, publicKey) { if (!jwtToken) { throw new JWTExceptions.JwtSignatureValidationException('JWT token is null or undefined'); } - - const parsedJwt = exports.parse(jwtToken); - const { header, _, signature, rawHeader, rawPayload } = parsedJwt; + + const { header, _, signature, rawHeader, rawPayload } = exports.parse(jwtToken); const algorithm = header.alg; if (!algorithm) { From 0a6b68e9b7f0b17c08a355092e9c43f2ccab6499 Mon Sep 17 00:00:00 2001 From: "Kumar,Monu" Date: Tue, 28 Oct 2025 16:12:14 +0530 Subject: [PATCH 3/5] 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 4/5] 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 5/5] 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); } }