Opayo (SagePay) Amex payments fail since 3DSv2 (Using Direct V4) - opayo

Is anybody else having issues taking Amex payments with Opayo (SagePay) Direct protocol V4 since the 6th July?
Everything was fine before then, seems to be related to 3DSv2 but I'm successfully using the V4 protocol and taking 3DSv2 payments against Visa/Mastercard.
I get all the way through the transaction including successfully completing the 3DSv2/Safekey pages and then when I pass the CRes back to Opayo I get a "5086 : Unexpected CRes" response
When I base64 decode the CRes I get the below. (trimmed to remove full ID's)
{"threeDSServerTransID":"5bfe447a-5443-...","acsTransID":"00f9a54d-84f0-...","messageType":"CRes","messageVersion":"2.1.0","transStatus":"N"}
The data I send to SafeKey looks like the below (form auto submits with JS) threeDSSessionData is the VPSTxID base64 encoded including the {} brackets
<form name="form" action="https://acs-safekey.americanexpress.com/GravityACS/processing3ds" method="POST">
<input type="hidden" name="TermUrl" value="https://...?action=return3d&VendorTxCode=XXX">
<input type="hidden" name="creq" value="ewog...ICIwMiIKfQ">
<input type="hidden" name="threeDSSessionData" value="ezg1NTA5MzM.......UU1OH0=">
<input class="button" type="submit" value="Continue">
</form>
The transStatus:N response makes me wonder if its something on the way to SafeKey and Opayo aren't handling the decline? i.e. is it because I'm doing the following and hence they are declining the transaction - but then wouldn't it do that before it got to 3DS?
COFUsage=FIRST&InitiatedType=CIT&MITType=UNSCHEDULED
Thanks
Dave

Related

Opayo 3ds2 - sagepay PI integration rsa3dsauth.co.uk fails to return the threeDSSessionData

Opayo returns from our payment request asking us to contact https://www.rsa3dsauth.co.uk/3ds2/cReqWebBased?issuer=barclays" for 3D Secure auth, so we send this:
<form id="c-form" method="POST" action="https://www.rsa3dsauth.co.uk/3ds2/cReqWebBased?issuer=barclays">
<input type="hidden" name="creq" value="*removed for data protection*" />
<input type="hidden" name="threeDSSessionData" value="tQtpVHCcCVGEhPNDaeCtMK9I%2fREJERnarovmuZPsM4M6xy6gks9rOYix36waoxOn1wukcobCFbfd2jpmVDVDwZjrd3MzJtmpyFDEAu5R9azVveH6kBEXc5F2ETnFijQfEj5l6EzmH7EnMzbTlFHgbkDGR%2fH9CtURo0K2VSUKHN4%3d" />
<script>
document.addEventListener("DOMContentLoaded", function()
{
var b = document.getElementById("c-form");
b && b.submit();
});
</script>
</form>
And we get back
POST https://xxxxx.xxxxxxx.xxx/api/payment/3dsecurechallenge?eid=ctl00_cphMain_Payment_SagePay_ThreeD
cres=*removed for data protection*&threeDSSessionData=
Note there is nothing following &threeDSSessionData=, where it should be returning our string back to us.
The documentation says:
Anyone else having this issue?
Just to add some more context - we had the same issue but only with rsasecure.co.uk at the start of June. Other banks worked correctly. We have an open case with Opayo about this but there has not been a lot of communication.
We turned off 3DS while this issue is occurring but have been advised to try again today (23.07.21) as it may have been an issue at the banks side.
threeDSSessionData seems to be URL encoded in the input tag. Have you tried this without the URL encoding? I believe it should only be base64 encoded.
I've now received a response from Opayo. I also think they may have changed something because I'm positive I tried this previously and it didn't work...
threeDSSessionData is the base64 encoded of VPSTxId (including the curled braces). It is not base64url encoded - talking about url encoding is a massive red herring because the browser auto submit handles this without us having to do anything.
Here's an example that worked for me.
I curl post the card details to vpsdirect-register.vsp (or whatever flavour you're using). This must include a ThreeDSNotificationURL value that contains the URL of the your callback page.
The following response was received.
Status=3DAUTH
StatusDetail=StatusDetail=2021 : Please redirect your customer to the ACSURL, passing CReq.
VPSTxId={1F3F8015-FFEB-E897-0643-58841AD5DB03}
ACSURL=https://test.sagepay.com/3ds-simulator/html_challenge
CReq=ewogICJtZXNzY...iA6ICIwNSIKfQ
I base64 encode the VPSTxId value into a threeDSSessionData value and get the customers browser to autosubmit the following form
<form action="https://test.sagepay.com/3ds-simulator/html_challenge" method="post" id="autopostform">
<input type="hidden" name="threeDSSessionData" value="ezFGM0Y4MDE1LUZGRUItRTg5Ny0wNjQzLTU4ODQxQUQ1REIwM30=">
<input type="text" name="creq" value="ewogICJtZXNzY...iA6ICIwNSIKfQ">
</form>
Customer will then do the necessary gubbins and the callback URL will have something similar to the following posted to it
cres=ewogICJtZXNzYWd...OiAiWSIKfQ
threeDSSessionData=ezFGM0Y4MDE1LUZGRUItRTg5Ny0wNjQzLTU4ODQxQUQ1REIwM30=
Both of these values are base64 encoded (the cres actually contains the acsTransID that was in the original response). We now need to base64 decode the threeDSSessionData back into a VPSTxId which we then need to curl post along with the (still encoded) cres value.
VPSTxId={1F3F8015-FFEB-E897-0643-58841AD5DB03}
CRes=ewogICJtZXNzYWd...OiAiWSIKfQ
Hopefully that'll give us a successful response and the transaction should go through.
In short it's a complete pita. In reality the encoding element should be dealt with via Opayo's API rather than us needing to code this up.
Edit: Original Reply
Yes! We flicked over to v4 with 3DSv2 last week and have had a couple of dozen customers reporting that they try to pay, and basically just returned back to the checkout. Looking at one now for barclays, but have had others too - including one where they 3dauth and don't get returned to our checkout at all...

FormUrlEncodedMediaTypeFormatter vs JQueryMvcFormUrlEncodedFormatter

I have created a default web api project with at least those 2 media formatters loaded. Both are for the same content-type:
FormUrlEncodedMediaTypeFormatter: application/x-www-form-urlencoded
JQueryMvcFormUrlEncodedFormatter: application/x-www-form-urlencoded
When I do a simple http post form with enctype="application/x-www-form-urlencoded" it works only with the JQueryMvcFormUrlEncodedFormatter, that means my sent complex object is not null at server side.
When I remove the formatter JQueryMvcFormUrlEncodedFormatter at application startup and do the simple http post form again I expect it to work again but it does not.
I get an exception that no appropriate formatter is loaded.
Thats not true -actually-
Why does it not work?
P.S.
I found that this is the difference:
– System.Net.Http.Formatting.FormUrlEncodedMediaTypeFormatter, for handling HTML form URL-encoded data
– System.Web.Http.ModelBinding.JQueryMvcFormUrlEncodedFormatter, for handling model-bound HTML form URL-encoded data
but I do not understand the difference!
I do not even use jquery to post my form:
<form role="form" method="post" action="api/values" enctype="application/x-www-form-urlencoded">
<input type="text" class="form-control" name="firstName" placeholder="Enter first name">
<input type="text" class="form-control" name="lastName" placeholder="Enter last name">
<button type="submit" class="btn btn-default">Submit</button>
</form>
FormUrlEncodedMediaTypeFormatter binds application/x-www-form-urlencoded body to FormDataCollection type, and only that type.
JQueryMvcFormUrlEncodedFormatter however uses available ModelBinders to first parse the body to FormDataCollection and then use the first compatible ModelBinder to parse that into the final model. It's like mixing Model Binding approach and Media Type Formatting approach, which AFAIK is not explained anywhere in the WebAPI docs.
Both formatters are registered by default. This is what I inferred reading WebAPI source code.
There are 4 out of box formatters
JsonMediaTypeFormatter
XMLMediaTypeFormatter
FormUrlEncodedMediaTypeFormatter
JQueryMvcFormUrlEncodedFormatter
The first two media type formatters can serialize and deserialize CLR types to request/response and vice versa. But 3rd one neither serializes nor deserializes to/from any CLR type. 4th formatter can deserialize incoming raw data to CLR type.
Hence in your case, you are experiencing with error after you remove JQueryMvcFormUrlEncodedFormatter since 3rd formatter could not able to deserialize incoming request data to CLR type.

How to PATCH or DELETE from a form without javascript

I am writing a simple crud app on Phoenix. How does one submit PATCH and DELETE requests from a form without using javascript?
Ah I figured it out, same as rails:
<form method="POST">
<input name="_method" type="hidden" value="patch" />
...
Handled in Plug.MethodOverride: https://github.com/elixir-lang/plug/blob/master/lib/plug/method_override.ex
<form> elements only support sending GET and POST requests. The workaround that Rails uses is to read the request method from the _method request parameter, overriding the actual request method (the GET or POST method).
Phoenix does exactly the same through Plug, the Rack-like framework that Phoenix is built on. Long story short, Plug provides middlewares and one of the middlewares it provides is Plug.MethodOverride, which does exactly what we discussed. Doing so in a middleware, the Phoenix app barely knows that the original request was not a GET/POST.
You can see Plug.MethodOverride used in Phoenix's source code.
As the others have mentioned, Phoenix handles this in the routing via the MethodOverride plug.
In order to change this in the template with the form_for helper, use the :method parameter:
<%= form_for #changeset, path(#conn, :update), [multipart: true, method: "patch"], fn f -> %>
This will add the hidden input to the HTML, as noted by #greggreg:
<input name="_method" type="hidden" value="patch" />

How to test CAS with jmeter?

request:
I send a "get" http request, has no parameters;
path: http://192.168.22.139:8080/KSP
response:
<input id="username" type="text" name="username" required/>
<input id="password" type="password" name="password" required/>
<input type="hidden" name="lt" value="LT-697-vbNmm6NKOZ0cd7Wg1ywzbMKLa0DnOc" />
<input type="hidden" name="execution" value="e1s1" />
<input type="hidden" name="_eventId" value="submit" />
And I add two XPath Extractor to get lt and execution:
reference name: lt;
xPath query: //*[#id="login"]/input[3]
reference name: execution;
xPath query: //*[#id="login"]/input[4]
Then I send another request with username/password/service/It/execution/_eventId:
Error: ticket and execution I get is empty, I get the xpath by chrome, so what is the problem?
From the first response, can use Regular Expression Extractor to extract ticke and lt.
Then send the second request with parameter:
username/password/It/execution/_eventId/_eventId
By the way, you should add HTTP Cookie Manager,
I didnot see parameters of your first request. At least I didnot see the lt.
The second request should be a GET request, and the parameters are: service and ticket(This is not jsessionid, the value is generated by CAS, and it is the same with the parameter lt in the first request to the CAS login ).
There is a few things you need to change in the second request:
add execution parameter (extract it from the first request, eg. with XPath Extractor or Regular Expression Extractor),
the XPath expression would be something like .//*[#id='fm1']//*[#name='execution']/#value
add _eventId=submit parameter,
the lt parameter (the login ticket) should be extracted from the login form, too - it changes each time; see XPath Extractor or Regular Expression Extractor,
XPath expression: .//*[#id='fm1']//*[#name='lt']/#value,
not required, but will look nicer - change GET to POST.
If you're testing CAS login itself I can suggest using JMeter HTTP Proxy server to record login process, catch all values which are being passed, detect those, which are dynamic and deal with them via Regular Expression Extractor, XPath, Beanshell, etc. Post Processors - what you're most comfortable with.
In case if your application is behind CAS I guess that CAS login simply adds a cookie and your application considers user authenticated basing on it. In this case you can simply store cookies somewhere in CSV file and use them via HTTP Cookie Manager to simulate authenticated users.

PayPal IPN validation

Following is from PayPal Order Management Integration Guide:
Processing the PayPal Response to Your Postback
PayPal responds to your postbacks with a single word in the body of the response: VERIFIED
or INVALID.
When you receive a VERIFIED postback response, perform the following checks on data in
the IPN:
Check that the payment_status is Completed.
If the payment_status is Completed, check the txn_id against the previous PayPal
transaction that you processed to ensure it is not a duplicate.
Check that the receiver_email is an email address registered in your PayPal account.
Check that the price, carried in mc_gross, and the currency, carried in mc_currency, are correct for the item, carried in item_name or item_number.
After you complete the above checks, notification validation is complete. You can update your database with the information provided, and you can initiate other appropriate automated back-end processing.
<form action="https://www.paypal.com/cgi-bin/webscr" method="post">
<input type="hidden" name="cmd" value="_cart" />
<input type="hidden" name="upload" value="1" />
<input type="hidden" name="business" value="GXLC9H9VFPLQE">
.....
<input type="submit" name="Submit" value="Submit" />
</form>
In step 3 I should check receiver_email, but I don't want to.
I don't want to keep my paypal account email in my application.
My question is: can I check business variable instead?
I have an answer here:
The 'checks' provided are just recommendation and not a requirement to use the IPN. You can check for "business" instead of "receiver_email". However, if you pass in the form, the value of "business" that is returned to you in the IPN will be the merchant email address on your account while the value of "receiver_id" will be GXLC9H9VFPLQE.
So if you want to check for your merchant ID instead of email address, then check for "receiver_id".

Resources