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
15 changes: 15 additions & 0 deletions src/authentication/util/Cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
3 changes: 2 additions & 1 deletion src/authentication/util/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};
64 changes: 64 additions & 0 deletions src/authentication/util/jwt/JWTExceptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'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 = createCustomError('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 = createCustomError('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 = createCustomError('JwtSignatureValidationException');
235 changes: 235 additions & 0 deletions src/authentication/util/jwt/JWTUtility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
'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 { header, _, signature, rawHeader, rawPayload } = exports.parse(jwtToken);

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);
}
}