Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MLE implementation #120

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f916928
add MLE implementation
monu-kumar-visa Feb 5, 2025
516933d
created a method for request body
monu-kumar-visa Feb 5, 2025
64bef5b
mustache file changes
monu-kumar-visa Feb 6, 2025
245322e
corrected mustache changes
monu-kumar-visa Feb 6, 2025
750c3a8
Merge remote-tracking branch 'origin/master' into feature/mle-impleme…
monu-kumar-visa Feb 6, 2025
f5306bd
minor fix
monu-kumar-visa Feb 7, 2025
6c00f39
resolved PR comments
monu-kumar-visa Feb 10, 2025
df69aa5
checking keys type in mapToControlMLE hash
monu-kumar-visa Feb 10, 2025
c606460
added cache
monu-kumar-visa Feb 10, 2025
a5685d6
corrected log level and message
monu-kumar-visa Feb 11, 2025
727836f
minor fix
monu-kumar-visa Feb 11, 2025
4914d21
fix for appending bactrace in exception message
monu-kumar-visa Feb 11, 2025
6925d42
corrected map check and mustache file
monu-kumar-visa Feb 11, 2025
3330e4e
added empty json check
monu-kumar-visa Feb 11, 2025
608f19d
resolving pr comment and removed unused function
monu-kumar-visa Feb 13, 2025
16f9322
corrected error throw in case of incorrect auth type for MLE
monu-kumar-visa Feb 13, 2025
1e3a469
add MLE.md
monu-kumar-visa Feb 18, 2025
1db105f
added MLE section in readme and add sample codes link
monu-kumar-visa Feb 27, 2025
200b8fb
Deprecated method for JWE decryption
gnongsie Feb 28, 2025
18c16d0
Changes from API updates - Feb 2025
gnongsie Feb 28, 2025
c52ab56
Enforced sensitive masking of logs
gnongsie Mar 21, 2025
159eadc
Enforced sensitive masking of logs
gnongsie Mar 21, 2025
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
6 changes: 5 additions & 1 deletion generator/cybersource-ruby-template/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
=end

require 'uri'

require 'AuthenticationSDK/util/MLEUtility'
module {{moduleName}}
{{#operations}}
class {{classname}}
Expand Down Expand Up @@ -165,6 +165,10 @@ module {{moduleName}}
sdk_tracker = SdkTracker.new
post_body = sdk_tracker.insert_developer_id_tracker(post_body, '{{dataType}}', @api_client.config.host, @api_client.merchantconfig.defaultDeveloperId)
{{/bodyParam}}
is_mle_supported_by_cybs_for_api = {{#vendorExtensions.x-devcenter-metaData.isMLEsupported}}true{{/vendorExtensions.x-devcenter-metaData.isMLEsupported}}{{^vendorExtensions.x-devcenter-metaData.isMLEsupported}}false{{/vendorExtensions.x-devcenter-metaData.isMLEsupported}}
if MLEUtility.check_is_mle_for_API(@api_client.merchantconfig, is_mle_supported_by_cybs_for_api, "{{operationId}},{{operationId}}_with_http_info")
post_body = MLEUtility.new.encrypt_request_payload(@api_client.merchantconfig, post_body)
end
auth_names = [{{#authMethods}}'{{name}}'{{#hasMore}}, {{/hasMore}}{{/authMethods}}]
data, status_code, headers = @api_client.call_api(:{{httpMethod}}, local_var_path,
:header_params => header_params,
Expand Down
50 changes: 48 additions & 2 deletions lib/AuthenticationSDK/core/MerchantConfig.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ def initialize(cybsPropertyObj)
@defaultCustomHeaders = cybsPropertyObj['defaultCustomHeaders']
# Path to client JWE pem file directory
@pemFileDirectory = cybsPropertyObj['pemFileDirectory']
validateMerchantDetails()
@mleKeyAlias = cybsPropertyObj['mleKeyAlias']
@useMLEGlobally = cybsPropertyObj['useMLEGlobally']
@mapToControlMLEonAPI = cybsPropertyObj['mapToControlMLEonAPI']
validateMerchantDetails
logAllProperties(cybsPropertyObj)
validateMLEConfiguration
end

#fall back logic
def validateMerchantDetails()
logmessage=''
Expand Down Expand Up @@ -225,6 +230,44 @@ def validateMerchantDetails()
end
end

def validateMLEConfiguration
unless [true, false].include?(@useMLEGlobally)
err = StandardError.new(Constants::ERROR_PREFIX + "useMLEGlobally must be a boolean")
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err)
raise err
end
if [email protected]? && [email protected]_a?(Hash)
err = StandardError.new(Constants::ERROR_PREFIX + "mapToControlMLEonAPI must be a map")
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err)
raise err
end

[email protected]? && unless @mleKeyAlias.instance_of? String
(err = StandardError.new(Constants::ERROR_PREFIX + "mleKeyAlias must be a string"))
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err)
raise err
end
if @mleKeyAlias.to_s.empty?
@mleKeyAlias = Constants::DEFAULT_ALIAS_FOR_MLE_CERT
end

mle_configured = @useMLEGlobally
if [email protected]? && [email protected]?
@mapToControlMLEonAPI.each do |_, value|
unless [true, false].include?(value) && value
mle_configured = true
break
end
end
end

if mle_configured && !Constants::AUTH_TYPE_JWT.eql?(@authenticationType)
err = StandardError.new(Constants::ERROR_PREFIX + "MLE can only be used with JWT authentication")
@log_obj.logger.error(ExceptionHandler.new.new_api_exception err)
raise
end
end

def logAllProperties(propertyObj)
merchantConfig = ''
hiddenProperties = (Constants::HIDDEN_MERCHANT_PROPERTIES).split(',')
Expand Down Expand Up @@ -278,4 +321,7 @@ def logAllProperties(propertyObj)
attr_accessor :solutionId
attr_accessor :defaultCustomHeaders
attr_accessor :pemFileDirectory
end
attr_accessor :useMLEGlobally
attr_accessor :mapToControlMLEonAPI
attr_accessor :mleKeyAlias
end
6 changes: 5 additions & 1 deletion lib/AuthenticationSDK/util/Constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,8 @@ class Constants
REFRESH_TOKEN_EMPTY = 'RefreshToken is Empty/Null' unless const_defined?(:REFRESH_TOKEN_REQ)

DEPRECATED_ENVIRONMENT = 'The value provided for this field `RunEnvironment` has been deprecated and will not be used anymore.\n\nPlease refer to the README file [ https://github.com/CyberSource/cybersource-rest-samples-node/blob/master/README.md ] for information about the new values that are accepted.'
end

DEFAULT_ALIAS_FOR_MLE_CERT = 'CyberSource_SJC_US' unless const_defined?(:DEFAULT_ALIAS_FOR_MLE_CERT)

CERTIFICATE_EXPIRY_DATE_WARNING_DAYS = 90 unless const_defined?(:CERTIFICATE_EXPIRY_DATE_WARNING_DAYS)
end
115 changes: 115 additions & 0 deletions lib/AuthenticationSDK/util/MLEUtility.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
require_relative '../logging/log_factory.rb'
require 'jose'
public
class MLEUtility
@log_obj
def self.check_is_mle_for_API(merchant_config, is_mle_supported_by_cybs_for_api, operation_ids)
is_mle_for_api = false
if is_mle_supported_by_cybs_for_api && merchant_config.useMLEGlobally
is_mle_for_api = true
end
if merchant_config.mapToControlMLEonAPI.nil? && merchant_config.mapToControlMLEonAPI.nil?
operation_ids.each do |operation_id|
if merchant_config.mapToControlMLEonAPI.key?(operation_id)
is_mle_for_api = merchant_config.mapToControlMLEonAPI[operation_id]
break
end
end
end
is_mle_for_api
end

def encrypt_request_payload(merchant_config, request_payload)
if request_payload.nil?
return nil
end
@log_obj = Log.new(merchant_config.log_config, 'MLEUtility')
@log_obj.logger.info('Encrypting request payload')
@log_obj.logger.debug('LOG_REQUEST_BEFORE_MLE: ' + request_payload)


begin
certificate = get_certificate(merchant_config, @log_obj)
validate_certificate(certificate, merchant_config, @log_obj)
serial_number = extract_serial_number_from_certificate(certificate)
if serial_number.nil?
@log_obj.logger.error('Serial number not found in certificate for MLE')
raise StandardError.new('Serial number not found in MLE certificate')
end

jwk = JOSE::JWK.from_key(certificate.public_key)
if jwk.nil?
@log_obj.logger.error('Failed to create JWK object from public key')
raise StandardError.new('Failed to create JWK object from public key')
end
headers = {
'alg' => 'RSA-OAEP-256',
'enc' => 'A256GCM',
'typ' => 'JWT',
'kid' => serial_number,
'iat' => Time.now.to_i
}
jwe = JOSE::JWE.block_encrypt(jwk, request_payload, headers)

compact_jwe = jwe.compact
@log_obj.logger.debug('LOG_REQUEST_AFTER_MLE: ' + compact_jwe)
return create_request_payload compact_jwe
rescue StandardError => e
@log_obj.logger.error("An error occurred during encryption: #{e.message}")
raise e
end
end


def get_certificate(merchant_config, log_obj)
begin
p12_file_path = File.join(merchant_config.keysDirectory, merchant_config.keyFilename + '.p12')
file = File.binread(p12_file_path)
p12_file = OpenSSL::PKCS12.new(file, merchant_config.keyPass)
x5_cert_pem = OpenSSL::X509::Certificate.new(p12_file.certificate)
x5_cert_pem.subject.to_a.each do |attribute|
return x5_cert_pem if attribute[1].include?(merchant_config.mleKeyAlias)
end
p12_file.ca_certs.each do |cert|
cert.subject.to_a.each do |attribute|
return cert if attribute[1].include?(merchant_config.mleKeyAlias)
end
end
rescue OpenSSL::PKCS12::PKCS12Error => e
log_obj.logger.error("Failed to load PKCS12 file: #{e.message}")
raise e
rescue OpenSSL::X509::CertificateError => e
log_obj.logger.error("Failed to create X509 certificate: #{e.message}")
raise e
rescue StandardError => e
log_obj.logger.error("An error occurred while getting the certificate: #{e.message}")
raise e
end
end

def validate_certificate(certificate, merchant_config, log_obj)
if certificate.not_after.nil?
log_obj.logger.warn("Certificate for MLE don't have expiry date.")
end
if certificate.not_after < Time.now
log_obj.logger.error('Certificate with MLE alias ' + merchant_config.mleKeyAlias + ' is expired as of ' + certificate.not_after.to_s + ". Please update p12 file.")
else
time_to_expire = certificate.not_after - Time.now
if time_to_expire < Constants::CERTIFICATE_EXPIRY_DATE_WARNING_DAYS * 24 * 60 * 60
log_obj.logger.warn('Certificate with MLE alias ' + merchant_config.mleKeyAlias + ' is going to expired on ' + certificate.not_after.to_s + ". Please update p12 file before that.")
end
end
end

def extract_serial_number_from_certificate(certificate)
return nil if certificate.subject.to_s.empty? && certificate.issuer.to_s.empty?
certificate.subject.to_a.each do |attribute|
return attribute[1] if attribute[0].include?('serialNumber')
end
nil
end

def create_request_payload(compact_jwe)
"{ \"encryptedRequest\": \"#{compact_jwe}\" }"
end
end