Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions generator/cybersource-javascript-template/index.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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;<={{ }}=>
}));
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}));
130 changes: 130 additions & 0 deletions src/utilities/capturecontext/CaptureContextParsingUtility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'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, callback);
}

try {
JWTUtility.verifyJwt(jwtValue, publicKey);
return callback(null, parsedJwt.payload);
} catch (verificationError) {
return fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, 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 {Function} callback - Callback function
* @private
*/
function fetchPublicKeyAndVerify(jwtValue, parsedJwt, kid, runEnvironment, 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
};
85 changes: 85 additions & 0 deletions src/utilities/capturecontext/PublicKeyApiController.js
Original file line number Diff line number Diff line change
@@ -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 };