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

Redsys: 3ds support #3336

Merged
merged 1 commit into from
Sep 12, 2019
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 @@ -5,6 +5,7 @@
* Adyen: Send "NA" instead of "N/A" [leila-alderman] #3339
* Stripe Payment Intents: Set application fee or transfer amount on capture [britth] #3340
* TNS: Support Europe endpoint [curiousepic] #3346
* Redsys: Add 3DS support to gateway [britth] #3336

== Version 1.98.0 (Sep 9, 2019)
* Stripe Payment Intents: Add new gateway [britth] #3290
Expand Down
134 changes: 104 additions & 30 deletions lib/active_merchant/billing/gateways/redsys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -193,28 +193,30 @@ def purchase(money, payment, options = {})
requires!(options, :order_id)

data = {}
add_action(data, :purchase)
add_action(data, :purchase, options)
add_amount(data, money, options)
add_order(data, options[:order_id])
add_payment(data, payment)
add_threeds(data, options) if options[:execute_threed]
data[:description] = options[:description]
data[:store_in_vault] = options[:store]

commit data
commit data, options
end

def authorize(money, payment, options = {})
requires!(options, :order_id)

data = {}
add_action(data, :authorize)
add_action(data, :authorize, options)
add_amount(data, money, options)
add_order(data, options[:order_id])
add_payment(data, payment)
add_threeds(data, options) if options[:execute_threed]
data[:description] = options[:description]
data[:store_in_vault] = options[:store]

commit data
commit data, options
end

def capture(money, authorization, options = {})
Expand All @@ -225,7 +227,7 @@ def capture(money, authorization, options = {})
add_order(data, order_id)
data[:description] = options[:description]

commit data
commit data, options
end

def void(authorization, options = {})
Expand All @@ -236,7 +238,7 @@ def void(authorization, options = {})
add_order(data, order_id)
data[:description] = options[:description]

commit data
commit data, options
end

def refund(money, authorization, options = {})
Expand All @@ -247,7 +249,7 @@ def refund(money, authorization, options = {})
add_order(data, order_id)
data[:description] = options[:description]

commit data
commit data, options
end

def verify(creditcard, options = {})
Expand Down Expand Up @@ -278,8 +280,8 @@ def scrub(transcript)

private

def add_action(data, action)
data[:action] = transaction_code(action)
def add_action(data, action, options = {})
data[:action] = options[:execute_threed].present? ? '0' : transaction_code(action)
end

def add_amount(data, money, options)
Expand All @@ -295,6 +297,10 @@ def url
test? ? test_url : live_url
end

def threeds_url
test? ? 'https://sis-t.redsys.es:25443/sis/services/SerClsWSEntradaV2': 'https://sis.redsys.es/sis/services/SerClsWSEntradaV2'
end

def add_payment(data, card)
if card.is_a?(String)
data[:credit_card_token] = card
Expand All @@ -311,21 +317,57 @@ def add_payment(data, card)
end
end

def commit(data)
parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data))}", headers))
def add_threeds(data, options)
if options[:execute_threed] == true
data[:threeds] = {threeDSInfo: 'CardData'}
end
end

def headers
{
'Content-Type' => 'application/x-www-form-urlencoded'
}
def determine_3ds_action(threeds_hash)
return 'iniciaPeticion' if threeds_hash[:threeDSInfo] == 'CardData'
return 'trataPeticion' if threeds_hash[:threeDSInfo] == 'AuthenticationData' ||
threeds_hash[:threeDSInfo] == 'ChallengeResponse'
end

def commit(data, options = {})
if data[:threeds]
action = determine_3ds_action(data[:threeds])
request = <<-EOS
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:apachesoap="http://xml.apache.org/xml-soap" xmlns:impl="http://webservice.sis.sermepa.es" xmlns:intf="http://webservice.sis.sermepa.es" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:wsdlsoap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" >
<soapenv:Header/>
<soapenv:Body>
<intf:#{action} xmlns:intf="http://webservice.sis.sermepa.es">
<intf:datoEntrada>
<![CDATA[#{xml_request_from(data, options)}]]>
</intf:datoEntrada>
</intf:#{action}>
</soapenv:Body>
</soapenv:Envelope>
EOS
parse(ssl_post(threeds_url, request, headers(action)), action)
else
parse(ssl_post(url, "entrada=#{CGI.escape(xml_request_from(data, options))}", headers), action)
end
end

def xml_request_from(data)
def headers(action=nil)
if action
{
'Content-Type' => 'text/xml',
'SOAPAction' => action
}
else
{
'Content-Type' => 'application/x-www-form-urlencoded'
}
end
end

def xml_request_from(data, options = {})
if sha256_authentication?
build_sha256_xml_request(data)
build_sha256_xml_request(data, options)
else
build_sha1_xml_request(data)
build_sha1_xml_request(data, options)
end
end

Expand All @@ -351,30 +393,30 @@ def build_signature(data)
Digest::SHA1.hexdigest(str)
end

def build_sha256_xml_request(data)
def build_sha256_xml_request(data, options = {})
xml = Builder::XmlMarkup.new
xml.instruct!
xml.REQUEST do
build_merchant_data(xml, data)
build_merchant_data(xml, data, options)
xml.DS_SIGNATUREVERSION 'HMAC_SHA256_V1'
xml.DS_SIGNATURE sign_request(merchant_data_xml(data), data[:order_id])
xml.DS_SIGNATURE sign_request(merchant_data_xml(data, options), data[:order_id])
end
xml.target!
end

def build_sha1_xml_request(data)
def build_sha1_xml_request(data, options = {})
xml = Builder::XmlMarkup.new :indent => 2
build_merchant_data(xml, data)
build_merchant_data(xml, data, options)
xml.target!
end

def merchant_data_xml(data)
def merchant_data_xml(data, options = {})
xml = Builder::XmlMarkup.new
build_merchant_data(xml, data)
build_merchant_data(xml, data, options)
xml.target!
end

def build_merchant_data(xml, data)
def build_merchant_data(xml, data, options = {})
xml.DATOSENTRADA do
# Basic elements
xml.DS_Version 0.1
Expand All @@ -383,7 +425,7 @@ def build_merchant_data(xml, data)
xml.DS_MERCHANT_ORDER data[:order_id]
xml.DS_MERCHANT_TRANSACTIONTYPE data[:action]
xml.DS_MERCHANT_PRODUCTDESCRIPTION data[:description]
xml.DS_MERCHANT_TERMINAL @options[:terminal]
xml.DS_MERCHANT_TERMINAL options[:terminal] || @options[:terminal]
xml.DS_MERCHANT_MERCHANTCODE @options[:login]
xml.DS_MERCHANT_MERCHANTSIGNATURE build_signature(data) unless sha256_authentication?

Expand All @@ -398,22 +440,36 @@ def build_merchant_data(xml, data)
xml.DS_MERCHANT_IDENTIFIER data[:credit_card_token]
xml.DS_MERCHANT_DIRECTPAYMENT 'true'
end

if data[:threeds]
xml.DS_MERCHANT_EMV3DS data[:threeds].to_json
end
end
end

def parse(data)
def parse(data, action)
params = {}
success = false
message = ''
options = @options.merge(:test => test?)
xml = Nokogiri::XML(data)
code = xml.xpath('//RETORNOXML/CODIGO').text
if code == '0'

if ['iniciaPeticion', 'trataPeticion'].include?(action)
vxml = Nokogiri::XML(data).remove_namespaces!.xpath("//Envelope/Body/#{action}Response/#{action}Return").inner_text
xml = Nokogiri::XML(vxml)
node = (action == 'iniciaPeticion' ? 'INFOTARJETA' : 'OPERACION')
op = xml.xpath("//RETORNOXML/#{node}")
op.children.each do |element|
params[element.name.downcase.to_sym] = element.text
end
message = response_text_3ds(xml, params)
success = params.size > 0 && is_success_response?(params[:ds_response])
elsif code == '0'
op = xml.xpath('//RETORNOXML/OPERACION')
op.children.each do |element|
params[element.name.downcase.to_sym] = element.text
end

if validate_signature(params)
message = response_text(params[:ds_response])
options[:authorization] = build_authorization(params)
Expand Down Expand Up @@ -474,6 +530,20 @@ def response_text(code)
RESPONSE_TEXTS[code] || 'Unkown code, please check in manual'
end

def response_text_3ds(xml, params)
code = xml.xpath('//RETORNOXML/CODIGO').text
message = ''
if code != '0'
message = "#{code} ERROR"
elsif params[:ds_emv3ds]
three_ds_data = JSON.parse(params[:ds_emv3ds])
message = three_ds_data['threeDSInfo']
elsif params[:ds_response]
message = response_text(params[:ds_response])
end
message
end

def is_success_response?(code)
(code.to_i < 100) || [400, 481, 500, 900].include?(code.to_i)
end
Expand Down Expand Up @@ -523,6 +593,10 @@ def xml_signed_fields(data)
xml_signed_fields += data[:ds_cardnumber]
end

if data[:ds_emv3ds]
xml_signed_fields += data[:ds_emv3ds]
end

xml_signed_fields + data[:ds_transactiontype] + data[:ds_securepayment]
end

Expand Down
20 changes: 20 additions & 0 deletions test/remote/gateways/remote_redsys_sha256_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ def setup
@gateway = RedsysGateway.new(fixtures(:redsys_sha256))
@credit_card = credit_card('4548812049400004')
@declined_card = credit_card
@threeds2_credit_card = credit_card('4918019199883839')
@options = {
order_id: generate_order_id,
}
Expand All @@ -16,6 +17,25 @@ def test_successful_purchase
assert_equal 'Transaction Approved', response.message
end

def test_successful_authorize_3ds
options = @options.merge(execute_threed: true, terminal: 12)
response = @gateway.authorize(100, @credit_card, options)
assert_success response
assert response.params['ds_emv3ds']
assert_equal 'NO_3DS_v2', JSON.parse(response.params['ds_emv3ds'])['protocolVersion']
assert_equal 'CardConfiguration', response.message
end

def test_successful_purchase_3ds
options = @options.merge(execute_threed: true, terminal: 12)
response = @gateway.purchase(100, @threeds2_credit_card, options)
assert_success response
assert three_ds_data = JSON.parse(response.params['ds_emv3ds'])
assert_equal '2.1.0', three_ds_data['protocolVersion']
assert_equal 'https://sis-d.redsys.es/sis-simulador-web/threeDsMethod.jsp', three_ds_data['threeDSMethodURL']
assert_equal 'CardConfiguration', response.message
end

def test_purchase_with_invalid_order_id
response = @gateway.purchase(100, @credit_card, order_id: "a%4#{generate_order_id}")
assert_success response
Expand Down
22 changes: 22 additions & 0 deletions test/unit/gateways/redsys_sha256_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ def test_authorize_without_order_id
end
end

def test_successful_authorize_with_3ds
@gateway.expects(:ssl_post).returns(successful_authorize_with_3ds_response)
response = @gateway.authorize(100, credit_card, { execute_threed: true, order_id: '156201452719' })
assert response.test?
assert response.params['ds_emv3ds']
assert_equal response.message, 'CardConfiguration'
end

def test_3ds_data_passed
stub_comms(@gateway, :ssl_request) do
@gateway.authorize(100, credit_card, { execute_threed: true, order_id: '156201452719', terminal: 12 })
end.check_request do |method, endpoint, data, headers|
assert_match(/iniciaPeticion/, data)
assert_match(/<DS_MERCHANT_TERMINAL>12<\/DS_MERCHANT_TERMINAL>/, data)
assert_match(/\"threeDSInfo\":\"CardData\"/, data)
end.respond_with(successful_authorize_with_3ds_response)
end

def test_bad_order_id_format
stub_comms(@gateway, :ssl_request) do
@gateway.authorize(100, credit_card, order_id: 'Una#cce-ptable44Format')
Expand Down Expand Up @@ -289,6 +307,10 @@ def successful_authorize_response
"<?xml version='1.0' encoding=\"UTF-8\" ?><RETORNOXML><CODIGO>0</CODIGO><Ds_Version>0.1</Ds_Version><OPERACION><Ds_Amount>100</Ds_Amount><Ds_Currency>978</Ds_Currency><Ds_Order>144743367273</Ds_Order><Ds_Signature>29qv8K/6k3P1zyk5F+ZYmMel0uuOzC58kXCgp5rcnhI=</Ds_Signature><Ds_MerchantCode>091952713</Ds_MerchantCode><Ds_Terminal>1</Ds_Terminal><Ds_Response>0000</Ds_Response><Ds_AuthorisationCode>399957</Ds_AuthorisationCode><Ds_TransactionType>1</Ds_TransactionType><Ds_SecurePayment>0</Ds_SecurePayment><Ds_Language>1</Ds_Language><Ds_MerchantData></Ds_MerchantData><Ds_Card_Country>724</Ds_Card_Country></OPERACION></RETORNOXML>\n"
end

def successful_authorize_with_3ds_response
'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soapenv:Header/><soapenv:Body><p231:iniciaPeticionResponse xmlns:p231="http://webservice.sis.sermepa.es"><p231:iniciaPeticionReturn>&lt;RETORNOXML&gt;&lt;CODIGO&gt;0&lt;/CODIGO&gt;&lt;INFOTARJETA&gt;&lt;Ds_Order&gt;156270437866&lt;/Ds_Order&gt;&lt;Ds_MerchantCode&gt;091952713&lt;/Ds_MerchantCode&gt;&lt;Ds_Terminal&gt;1&lt;/Ds_Terminal&gt;&lt;Ds_TransactionType&gt;0&lt;/Ds_TransactionType&gt;&lt;Ds_EMV3DS&gt;{&quot;protocolVersion&quot;:&quot;NO_3DS_v2&quot;,&quot;threeDSInfo&quot;:&quot;CardConfiguration&quot;}&lt;/Ds_EMV3DS&gt;&lt;Ds_Signature&gt;LIWUaQh+lwsE0DBNpv2EOYALCY6ZxHDQ6gLvOcWiSB4=&lt;/Ds_Signature&gt;&lt;/INFOTARJETA&gt;&lt;/RETORNOXML&gt;</p231:iniciaPeticionReturn></p231:iniciaPeticionResponse></soapenv:Body></soapenv:Envelope>'
end

def failed_authorize_response
"<?xml version='1.0' encoding=\"ISO-8859-1\" ?><RETORNOXML><CODIGO>SIS0093</CODIGO><RECIBIDO><DATOSENTRADA>\n <DS_Version>0.1</DS_Version>\n <DS_MERCHANT_CURRENCY>978</DS_MERCHANT_CURRENCY>\n <DS_MERCHANT_AMOUNT>100</DS_MERCHANT_AMOUNT>\n <DS_MERCHANT_ORDER>141278225678</DS_MERCHANT_ORDER>\n <DS_MERCHANT_TRANSACTIONTYPE>1</DS_MERCHANT_TRANSACTIONTYPE>\n <DS_MERCHANT_TERMINAL>1</DS_MERCHANT_TERMINAL>\n <DS_MERCHANT_MERCHANTCODE>91952713</DS_MERCHANT_MERCHANTCODE>\n <DS_MERCHANT_MERCHANTSIGNATURE>1c34699589507802f800b929ea314dc143b0b8a5</DS_MERCHANT_MERCHANTSIGNATURE>\n <DS_MERCHANT_TITULAR>Longbob Longsen</DS_MERCHANT_TITULAR>\n <DS_MERCHANT_PAN>4242424242424242</DS_MERCHANT_PAN>\n <DS_MERCHANT_EXPIRYDATE>1509</DS_MERCHANT_EXPIRYDATE>\n <DS_MERCHANT_CVV2>123</DS_MERCHANT_CVV2>\n</DATOSENTRADA>\n</RECIBIDO></RETORNOXML>"
end
Expand Down