Skip to content
Merged
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
154 changes: 154 additions & 0 deletions CyberSource/utilities/flex/CaptureContextParsingUtility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# coding: utf-8
"""
Capture Context Parsing Utility Module.

This module provides functionality to parse and verify JWT capture context responses
from CyberSource. It supports signature verification using public keys fetched from
the Flex V2 API, with caching to improve performance.
"""

from __future__ import absolute_import

from authenticationsdk.util.jwt.JWTUtility import parse, verify_jwt
from authenticationsdk.util.jwt.JWTExceptions import (
InvalidJwtException,
InvalidJwkException,
JwtSignatureValidationException
)
from authenticationsdk.util.Cache import FileCache
from CyberSource.utilities.flex.PublicKeyApiController import fetch_public_key


def parse_capture_context_response(jwt_value, merchant_config, verify_jwt_signature=True):
"""
Parse a capture context JWT response and optionally verify its signature.

This function parses a JWT token and optionally verifies its signature using
the public key fetched from the CyberSource Flex V2 API. Public keys are cached
to improve performance on subsequent verifications.

Args:
jwt_value (str): The JWT token to parse
merchant_config (object): The merchant configuration object containing run_environment
verify_jwt_signature (bool): Whether to verify the JWT signature (default: True)

Returns:
dict: The decoded JWT payload

Raises:
InvalidJwtException: If the JWT token is invalid or cannot be parsed
InvalidJwkException: If the public key (JWK) is invalid
JwtSignatureValidationException: If signature verification fails
ValueError: If merchant_config is missing or invalid
Exception: For other errors during parsing or verification
"""
if not jwt_value:
raise InvalidJwtException('JWT value is null or undefined')

if not merchant_config:
raise ValueError('merchantConfig is required')

# Parse the JWT token
try:
parsed_jwt = parse(jwt_value)
except InvalidJwtException:
raise
except Exception as parse_error:
raise InvalidJwtException('Failed to parse JWT token', parse_error)

# If verification is not requested, return the payload immediately
if not verify_jwt_signature:
return parsed_jwt['payload']

# Extract the key ID from the JWT header
header = parsed_jwt['header']
kid = header.get('kid')

if not kid:
raise JwtSignatureValidationException('JWT header missing key ID (kid) field')

# Get the run environment from merchant config
run_environment = None
if hasattr(merchant_config, 'run_environment'):
run_environment = merchant_config.run_environment
elif hasattr(merchant_config, 'getRunEnvironment'):
run_environment = merchant_config.getRunEnvironment()

if not run_environment:
raise ValueError('Run environment not found in merchant config')

# Try to get the public key from cache
cache = FileCache()
public_key = None
is_public_key_from_cache = False

try:
public_key = cache.get_public_key_from_cache(run_environment, kid)
is_public_key_from_cache = True
except KeyError:
is_public_key_from_cache = False

# If public key is not in cache or verification fails, fetch from API
if not is_public_key_from_cache:
public_key = _fetch_public_key_and_verify(jwt_value, kid, run_environment, cache)
return parsed_jwt['payload']

# Try to verify with cached public key
try:
verify_jwt(jwt_value, public_key)
return parsed_jwt['payload']
except (JwtSignatureValidationException, InvalidJwkException):
# If verification fails with cached key, fetch fresh key from API
public_key = _fetch_public_key_and_verify(jwt_value, kid, run_environment, cache)
return parsed_jwt['payload']


def _fetch_public_key_and_verify(jwt_value, kid, run_environment, cache):
"""
Fetch public key from API and perform JWT verification.

This is a private helper function that fetches the public key from the
Flex V2 API, caches it, and verifies the JWT signature.

Args:
jwt_value (str): The JWT token
kid (str): The key ID
run_environment (str): The runtime environment
cache (FileCache): The cache instance

Returns:
dict: The public key in JWK format

Raises:
InvalidJwkException: If the JWK cannot be parsed
JwtSignatureValidationException: If verification fails
Exception: For other errors during fetch or verification
"""
try:
# Fetch the public key from the Flex V2 API
public_key = fetch_public_key(kid, run_environment)

# Add to cache (cache failure is not critical)
try:
cache.add_public_key_to_cache(run_environment, kid, public_key)
except Exception:
# Cache failure is not critical, continue with the public key
pass

except Exception as fetch_error:
# Re-raise with appropriate error type
if 'Invalid Runtime URL' in str(fetch_error):
raise ValueError('Invalid Runtime URL in Merchant Config')
elif 'No response received' in str(fetch_error) or 'Network error' in str(fetch_error):
raise Exception('Network Error: No response received while trying to retrieve public key from server', fetch_error)
elif 'Failed to parse JWK' in str(fetch_error):
raise InvalidJwkException('JWK received from server cannot be parsed correctly', fetch_error)
else:
raise

# Verify the JWT with the fetched public key
try:
verify_jwt(jwt_value, public_key)
return public_key
except (JwtSignatureValidationException, InvalidJwkException):
raise JwtSignatureValidationException('JWT validation failed')
119 changes: 119 additions & 0 deletions CyberSource/utilities/flex/PublicKeyApiController.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# coding: utf-8
"""
Public Key API Controller Module.

This module provides functionality to fetch RSA public keys in JWK format
from the CyberSource Flex V2 public keys API endpoint.
"""

from __future__ import absolute_import

import json
import ssl
import certifi
import urllib3

from authenticationsdk.util.jwt.JWTUtility import get_rsa_public_key_from_jwk
from authenticationsdk.util.jwt.JWTExceptions import InvalidJwkException
from CyberSource.configuration import Configuration


def fetch_public_key(kid, run_environment):
"""
Fetch the public key for the given key ID (kid) from the specified run environment.

This function makes an HTTP GET request to the Flex V2 public keys endpoint
to retrieve the RSA public key in JWK format.

Args:
kid (str): The key ID for which to fetch the public key
run_environment (str): The environment domain (e.g., 'apitest.cybersource.com')

Returns:
RSAPublicKey: The RSA public key object from cryptography library

Raises:
ValueError: If kid or run_environment is missing
urllib3.exceptions.HTTPError: If the HTTP request fails
InvalidJwkException: If the JWK cannot be parsed correctly
Exception: For other errors during the fetch or parse process
"""
if not kid:
raise ValueError('kid parameter is required')

if not run_environment:
raise ValueError('runEnvironment parameter is required')

url = f"https://{run_environment}/flex/v2/public-keys/{kid}"

headers = {
'Accept': 'application/json'
}

# Use the same SSL configuration pattern as rest.py
configuration = Configuration()
# Use certifi bundle as fallback (same as rest.py)
configuration.cert_reqs = ssl.CERT_REQUIRED
configuration.ca_certs = certifi.where()

# Create urllib3 PoolManager with SSL configuration (matches rest.py pattern)
http = urllib3.PoolManager(
cert_reqs=configuration.cert_reqs,
ca_certs=configuration.ca_certs
)

try:
# Make the HTTP GET request
response = http.request('GET', url, headers=headers)

# Check if the request was successful
if response.status not in range(200, 300):
error = Exception(
f'HTTP {response.status}: {response.reason} - Failed to fetch public key for kid: {kid}'
)
error.status = response.status
raise error

# Get response data as string
response_data = response.data.decode('utf-8')

if not response_data:
raise Exception('Empty response received from public key endpoint')

# Use response data directly as JSON string
jwk_json_string = response_data

# Extract and validate the RSA public key from JWK
try:
public_key = get_rsa_public_key_from_jwk(jwk_json_string)
if not public_key:
raise Exception('Invalid public key received from JWK')
return public_key
except InvalidJwkException as parse_error:
error = Exception(f'Failed to parse JWK response: {str(parse_error)}')
error.original_error = parse_error
raise error

except urllib3.exceptions.HTTPError as http_error:
error = Exception(f'HTTP error - Failed to fetch public key for kid: {kid}')
error.original_error = http_error
raise error

except urllib3.exceptions.MaxRetryError as retry_error:
error = Exception(f'No response received - Failed to fetch public key for kid: {kid}')
error.original_error = retry_error
raise error

except urllib3.exceptions.SSLError as ssl_error:
error = Exception(f'SSL error - Failed to fetch public key for kid: {kid}')
error.original_error = ssl_error
raise error

except Exception as general_error:
# Re-raise if it's already one of our custom errors
if hasattr(general_error, 'original_error'):
raise
# Otherwise wrap it
error = Exception(f'Error fetching public key: {str(general_error)}')
error.original_error = general_error
raise error
38 changes: 37 additions & 1 deletion authenticationsdk/util/Cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def _initialize_cache(self):
self._p12_cache_lock = threading.RLock()
self.mlecache = {}
self._mle_cache_lock = threading.RLock()
self.public_key_cache = {}
self._public_key_cache_lock = threading.RLock()

def fetch_cached_p12_certificate(self, merchant_config, p12_file_path, key_password):
try:
Expand Down Expand Up @@ -166,4 +168,38 @@ def setup_mle_cache(self, merchant_config, cache_key, certificate_file_path):
private_key
)
with self._mle_cache_lock:
self.mlecache[cache_key] = cert_info
self.mlecache[cache_key] = cert_info

def add_public_key_to_cache(self, run_environment, key_id, public_key):
"""
Add a public key to the cache for Flex V2 public keys.

Args:
run_environment (str): The runtime environment (e.g., 'apitest.cybersource.com')
key_id (str): The key ID (kid) from the JWT header
public_key (dict): The RSA public key in JWK format
"""
cache_key = f"{GlobalLabelParameters.PUBLIC_KEY_CACHE_IDENTIFIER}_{run_environment}_{key_id}"
with self._public_key_cache_lock:
self.public_key_cache[cache_key] = public_key

def get_public_key_from_cache(self, run_environment, key_id):
"""
Retrieve a public key from the cache.

Args:
run_environment (str): The runtime environment (e.g., 'apitest.cybersource.com')
key_id (str): The key ID (kid) from the JWT header

Returns:
dict: The RSA public key in JWK format

Raises:
KeyError: If the public key is not found in the cache
"""
cache_key = f"{GlobalLabelParameters.PUBLIC_KEY_CACHE_IDENTIFIER}_{run_environment}_{key_id}"

with self._public_key_cache_lock:
if cache_key not in self.public_key_cache:
raise KeyError(f"Public key not found in cache for [{run_environment}, {key_id}]")
return self.public_key_cache[cache_key]
1 change: 1 addition & 0 deletions authenticationsdk/util/GlobalLabelParameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ class GlobalLabelParameters:
MESSAGE_AFTER_MLE_REQUEST = "Request after MLE: "
MLE_CACHE_IDENTIFIER_FOR_CONFIG_CERT = "mleCertFromMerchantConfig"
MLE_CACHE_IDENTIFIER_FOR_P12_CERT = "mleCertFromP12"
PUBLIC_KEY_CACHE_IDENTIFIER = "FlexV2PublicKeys"
DEFAULT_MAX_IDLE_CONNECTIONS = 100
DEFAULT_MAX_POOL_SIZE = 10
DEFAULT_MAX_KEEP_ALIVE_DELAY = 300 # in seconds
Expand Down
Loading