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

Worldpay add encrypted apple google #5271

Merged
merged 1 commit into from
Oct 10, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
* Worldpay: Add customStringFields [jcreiff] #5284
* Airwallex: truncate descriptor field to 32 characters [jcreiff] #5292
* DLocal: Add the description field for refund [yunnydang] #5296
* Worldpay: Add support for Worldpay decrypted apple pay and google pay [dustinhaefele] #5271

== Version 1.137.0 (August 2, 2024)
* Unlock dependency on `rexml` to allow fixing a CVE (#5181).
Expand Down
12 changes: 12 additions & 0 deletions lib/active_merchant/billing/credit_card.rb
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,18 @@ def allow_spaces_in_card?(number = nil)
BRANDS_WITH_SPACES_IN_NUMBER.include?(self.class.brand?(self.number || number))
end

def network_token?
false
end

def mobile_wallet?
false
end

def encrypted_wallet?
false
end

private

def filter_number(value)
Expand Down
2 changes: 1 addition & 1 deletion lib/active_merchant/billing/gateways/elavon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ def add_txn_id(xml, authorization)
end

def add_network_token(xml, payment_method)
payment = payment_method.payment_data&.gsub('=>', ':')
payment = payment_method.payment_data.to_s&.gsub('=>', ':')
bradbroge marked this conversation as resolved.
Show resolved Hide resolved
case payment_method.source
when :apple_pay
xml.ssl_applepay_web url_encode(payment)
Expand Down
4 changes: 2 additions & 2 deletions lib/active_merchant/billing/gateways/global_collect.rb
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ def add_mobile_credit_card(post, payment, options, specifics_inputs, expirydate)
post['mobilePaymentMethodSpecificInput'] = specifics_inputs

if options[:use_encrypted_payment_data]
post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] = payment.payment_data
post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'] = payment.payment_data.to_s&.gsub('=>', ':')
bradbroge marked this conversation as resolved.
Show resolved Hide resolved
else
add_decrypted_payment_data(post, payment, options, expirydate)
end
Expand All @@ -315,7 +315,7 @@ def add_decrypted_payment_data(post, payment, options, expirydate)
'dpan' => payment.number
}
when :google_pay
payment.payment_data
payment.payment_data.to_s&.gsub('=>', ':')
end

post['mobilePaymentMethodSpecificInput']["#{data_type}PaymentData"] = data if data
Expand Down
59 changes: 52 additions & 7 deletions lib/active_merchant/billing/gateways/worldpay.rb
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,8 @@ def add_payment_method(xml, amount, payment_method, options)
case options[:payment_type]
when :pay_as_order
add_amount_for_pay_as_order(xml, amount, payment_method, options)
when :encrypted_wallet
add_encrypted_wallet(xml, payment_method)
when :network_token
add_network_tokenization_card(xml, payment_method, options)
else
Expand Down Expand Up @@ -683,6 +685,37 @@ def merchant_initiated?(options)
options.dig(:stored_credential, :initiator) == 'merchant'
end

def add_encrypted_wallet(xml, payment_method)
source = encrypted_wallet_source(payment_method.source)

xml.paymentDetails do
xml.tag! "#{source}-SSL" do
if source == 'APPLEPAY'
add_encrypted_apple_pay(xml, payment_method)
else
add_encrypted_google_pay(xml, payment_method)
end
end
end
end

def add_encrypted_apple_pay(xml, payment_method)
xml.header do
xml.ephemeralPublicKey payment_method.payment_data.dig(:header, :ephemeralPublicKey)
xml.publicKeyHash payment_method.payment_data.dig(:header, :publicKeyHash)
xml.transactionId payment_method.payment_data.dig(:header, :transactionId)
end
xml.signature payment_method.payment_data[:signature]
xml.version payment_method.payment_data[:version]
xml.data payment_method.payment_data[:data]
end

def add_encrypted_google_pay(xml, payment_method)
xml.protocolVersion payment_method.payment_data[:version]
xml.signature payment_method.payment_data[:signature]
xml.signedMessage payment_method.payment_data[:signed_message]
end

def add_card_or_token(xml, payment_method, options)
xml.paymentDetails credit_fund_transfer_attribute(options) do
if options[:payment_type] == :token
Expand Down Expand Up @@ -1066,16 +1099,17 @@ def payment_details(payment_method, options = {})
when String
DustinHaefele marked this conversation as resolved.
Show resolved Hide resolved
token_type_and_details(payment_method)
else
type = network_token?(payment_method) || wallet_type_google_pay?(options) || payment_method_apple_pay?(payment_method) ? :network_token : :credit

{ payment_type: type }
payment_method_type(payment_method, options)
end
end

def network_token?(payment_method)
payment_method.respond_to?(:source) &&
payment_method.respond_to?(:payment_cryptogram) &&
payment_method.respond_to?(:eci)
def payment_method_type(payment_method, options)
type = if payment_method.is_a?(NetworkTokenizationCreditCard)
payment_method.encrypted_wallet? ? :encrypted_wallet : :network_token
else
wallet_type_google_pay?(options) ? :network_token : :credit
end
{ payment_type: type }
end

def payment_method_apple_pay?(payment_method)
Expand Down Expand Up @@ -1129,6 +1163,17 @@ def generate_stored_credential_params(is_initial_transaction, reason = nil, init
stored_credential_params[customer_or_merchant] = reason if reason
stored_credential_params
end

def encrypted_wallet_source(source)
case source
when :apple_pay
'APPLEPAY'
when :google_pay
'PAYWITHGOOGLE'
else
raise ArgumentError, 'Invalid encrypted wallet source'
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ def mobile_wallet?
%i[apple_pay android_pay google_pay].include?(source)
end

def encrypted_wallet?
payment_data.present?
end

def type
'network_tokenization'
end
Expand Down
8 changes: 4 additions & 4 deletions test/unit/gateways/elavon_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ def setup

@google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :google_pay,
payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}"
payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' }
})

@apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :apple_pay,
payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}"
payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' }
})
end

Expand Down Expand Up @@ -163,15 +163,15 @@ def test_successful_purchase_with_apple_pay
stub_comms do
@gateway.purchase(@amount, @apple_pay, @options)
end.check_request do |_endpoint, data, _headers|
assert_match(/<ssl_applepay_web>%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_applepay_web>/, data)
assert_match(/<ssl_applepay_web>%7B%22version%22%3A%22EC_v1%22%2C %22data%22%3A%22QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%22%7D<\/ssl_applepay_web>/, data)
end.respond_with(successful_purchase_response)
end

def test_successful_purchase_with_google_pay
stub_comms do
@gateway.purchase(@amount, @google_pay, @options)
end.check_request do |_endpoint, data, _headers|
assert_match(/<ssl_google_pay>%7B %27version%27%3A %27EC_v1%27%2C %27data%27%3A %27QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%27%7D<\/ssl_google_pay>/, data)
assert_match(/<ssl_google_pay>%7B%22version%22%3A%22EC_v1%22%2C %22data%22%3A%22QlzLxRFnNP9%2FGTaMhBwgmZ2ywntbr9%22%7D<\/ssl_google_pay>/, data)
end.respond_with(successful_purchase_response)
end

Expand Down
8 changes: 4 additions & 4 deletions test/unit/gateways/global_collect_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def setup

@google_pay_network_token = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :google_pay,
payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}"
payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' }
})

@declined_card = credit_card('5424180279791732')
Expand Down Expand Up @@ -91,14 +91,14 @@ def test_successful_purchase_with_requires_approval_true
def test_purchase_request_with_encrypted_google_pay
google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :google_pay,
payment_data: "{ 'version': 'EC_v1', 'data': 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9'}"
payment_data: { 'version' => 'EC_v1', 'data' => 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9' }
})

stub_comms(@gateway, :ssl_request) do
@gateway.purchase(@accepted_amount, google_pay, { use_encrypted_payment_data: true })
end.check_request(skip_response: true) do |_method, _endpoint, data, _headers|
assert_equal '320', JSON.parse(data)['mobilePaymentMethodSpecificInput']['paymentProductId']
assert_equal google_pay.payment_data, JSON.parse(data)['mobilePaymentMethodSpecificInput']['encryptedPaymentData']
assert_equal google_pay.payment_data.to_s&.gsub('=>', ':'), JSON.parse(data)['mobilePaymentMethodSpecificInput']['encryptedPaymentData']
end
end

Expand Down Expand Up @@ -131,7 +131,7 @@ def test_add_payment_for_google_pay
assert_includes post.keys.first, 'mobilePaymentMethodSpecificInput'
assert_equal post['mobilePaymentMethodSpecificInput']['paymentProductId'], '320'
assert_equal post['mobilePaymentMethodSpecificInput']['authorizationMode'], 'FINAL_AUTHORIZATION'
assert_equal post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'], @google_pay_network_token.payment_data
assert_equal post['mobilePaymentMethodSpecificInput']['encryptedPaymentData'], @google_pay_network_token.payment_data.to_s&.gsub('=>', ':')
end

def test_add_payment_for_apple_pay
Expand Down
76 changes: 73 additions & 3 deletions test/unit/gateways/worldpay_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ def test_payment_type_for_network_card
assert_equal payment, :network_token
end

def test_payment_type_returns_network_token_if_the_payment_method_responds_to_source_payment_cryptogram_and_eci
payment_method = mock
payment_method.stubs(source: nil, payment_cryptogram: nil, eci: nil)
def test_payment_type_returns_network_token_if_payment_method_is_nt_credit_card_and_not_encrypted
payment_method = @nt_credit_card
payment_method.stubs(source: nil, payment_cryptogram: nil, eci: nil, payment_data: nil)
result = @gateway.send(:payment_details, payment_method)
assert_equal({ payment_type: :network_token }, result)
end
Expand Down Expand Up @@ -243,6 +243,76 @@ def test_successful_authorize_without_name
assert_equal 'R50704213207145707', response.authorization
end

def test_successful_authorize_encrypted_apple_pay
apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :apple_pay,
payment_data: {
version: 'EC_v1',
data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9',
signature: 'signature',
header: {
ephemeralPublicKey: 'ephemeralPublicKey',
publicKeyHash: 'publicKeyHash',
transactionId: 'transactionId'
}
}
})

stub_comms do
@gateway.authorize(@amount, apple_pay, @options)
end.check_request do |_endpoint, data, _headers|
assert_match(/APPLEPAY-SSL/, data)
assert_match(%r(<version>EC_v1</version>), data)
assert_match(%r(<transactionId>transactionId</transactionId>), data)
assert_match(%r(<data>QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9</data>), data)
end.respond_with(successful_authorize_response)
end

def test_successful_authorize_encrypted_google_pay
google_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :google_pay,
payment_data: {
version: 'EC_v1',
data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9',
signature: 'signature',
header: {
ephemeralPublicKey: 'ephemeralPublicKey',
publicKeyHash: 'publicKeyHash',
transactionId: 'transactionId'
}
}
})

stub_comms do
@gateway.authorize(@amount, google_pay, @options)
end.check_request do |_endpoint, data, _headers|
assert_match(/PAYWITHGOOGLE-SSL/, data)
assert_match(%r(<protocolVersion>EC_v1</protocolVersion>), data)
assert_match(%r(<signature>signature</signature>), data)
end.respond_with(successful_authorize_response)
end

def test_encrypted_payment_with_invalid_source
apple_pay = ActiveMerchant::Billing::NetworkTokenizationCreditCard.new({
source: :android_pay,
payment_data: {
version: 'EC_v1',
data: 'QlzLxRFnNP9/GTaMhBwgmZ2ywntbr9',
signature: 'signature',
header: {
ephemeralPublicKey: 'ephemeralPublicKey',
publicKeyHash: 'publicKeyHash',
transactionId: 'transactionId'
}
}
})
error = assert_raises(ArgumentError) do
@gateway.authorize(@amount, apple_pay, @options)
end

assert_equal 'Invalid encrypted wallet source', error.message
end

def test_successful_authorize_by_reference
response = stub_comms do
@gateway.authorize(@amount, @options[:order_id].to_s, @options)
Expand Down
Loading