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 6 commits into
base: master
Choose a base branch
from
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
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check for Map value type should be <String,Boolean>

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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why same condition check for 2 times?

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?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if serial number is null then we can put default serial number from cert as same as implemented in Java.

@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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't pass merchant_config, pass only required parameter such as key_alias. IF it is generic function then try to put in certificate utility if available.

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