omnipay-authorizenet icon indicating copy to clipboard operation
omnipay-authorizenet copied to clipboard

New API available - Accept.js

Open judgej opened this issue 8 years ago • 49 comments

This new API makes use of JavaScript on the front end to build forms and (presumably) to avoid the need to send credit card numbers to your own server. It is an extension to the DPM API.

An example application is available here:

https://github.com/AuthorizeNet/accept-sample-app

I'm not sure what documentation is available apart from that. Authorize.Net are keen to help us incorporate this API into the gateway, so I'm opening this issue to log any discussions on what that may involve or any ideas or issues that anyone may have.

OmniPay has traditionally not got involved with the front end part of the UI, but most gateways are moving towards a very front-end centric approach (e.g. with "drop-in" forms generated through JavaScript, and card tokenisation APIs that can be used by the front end to avoid card numbers going anywhere near your server). How this fits into OmniPay 2.x or whether it should be something to expand OmniPay 3.0 with, is up for question. I personally don't have an answer for that at this time.

judgej avatar Jun 20 '16 22:06 judgej

As an aside, AIM is now called "Payment Transactions". AIM looks to be a name being slowly phased out. I'm wondering if the old AIM should have been left untouched in this gateway driver, and a separate new gateway created for the XML upgrade. JSON is available in beta as an alternative to XML - would have been nice to have had the JSON first, as it is much easier to handle in PHP. All done now, I guess.

judgej avatar Jun 21 '16 09:06 judgej

+1 to adding it as a new gateway driver and not replacing the previous AIM gateway with any new features.

lukeholder avatar Aug 10 '16 04:08 lukeholder

Some details here:

http://developer.authorize.net/api/reference/features/acceptjs.html

If I understand it correctly, the AcceptJS interface sits in the web client only and just tokenises card details using AJAX. Those tokens can then be used in place of card numbers in the normal AIM gateway, which may need some small changes to accept a cut-down card detail (just a token, instead of all the card number/dates/CVV details).

I've done something similar for PAYONE - there is a back end server-to-server direct payments gateway (called "Server") that can take either full card details (with PCI compliance needed) or a card token. The card token is generated on the web client with the help of a separate gateway (called "Client") that helps to generate the tokenization form.

judgej avatar Aug 17 '16 11:08 judgej

There are a lot of gateways that are moving towards having JS interfaces that sit on the client side and do the card tokenisation there. It's a really bad idea for a whole bunch of reasons but it does make some sense from a PCI compliance viewpoint (although base PCI compliance for card processing without local storage is not overly complicated and something that every eCommerce store should be doing IMHO). Anyway, I won't debate the politics of it.

I agree with @lukeholder -- +1 to making anything that uses AcceptJS a separate gateway and not replacing the existing working code.

delatbabel avatar Aug 17 '16 22:08 delatbabel

Agreed

barryvdh avatar Aug 18 '16 06:08 barryvdh

Assuming someone has set up their frontend forms and js to generate the "opaqueData", does anyone know where exactly to pass this into the gateways? I'm finding the Authorize.net docs rather vague, and so far I've only found this code in the example app, but I haven't found the related code in the omnipay driver... from what I've read, it sounds like it might tie in with the cardReference on the AIM gateway requests, but that does not seem to be translated into similar XML anywhere that I can find.

There's been talk about a new driver specific for accept.js in this issue, but it looks like that hasn't happened.

Does anyone have any pointers on where to go from here? It is supposed to be possible but just not documented, or is support for it unavailable?

Mark-H avatar Feb 13 '17 16:02 Mark-H

Hi @Mark-H we'll get some example code together and get you working. I use the DPM API mostly, which POSTs direct to the gateway, and has a "nofify" redirect to feed the result back to your site. Is it SIM or AIM you are using, or DPM?

The first step is probably to raise a new issue specifically on getting a working example into the documentation. We can follow it up there. If it is Accept.JS you need, then that would be a bit more involved if we need to write a new gateway driver for it, but I guess it's on the wishlist, so maybe worth it if you don't have and extreme deadline. When I first created this issue, Accept.JS was in beta and had little documentation. I haven't checked it since, so hopefully it is more stable now.

judgej avatar Feb 13 '17 21:02 judgej

Hi @judgej thanks for the reply :)

This is for a new ecommerce addon for a CMS. There's no pressing deadline, but I was hoping to integrate Auth.net into it within the next few weeks as it's a popular request. I've used SIM in the past, and I'm trying to avoid AIM as I don't want to force PCI compliance on users. DPM is a bit tricky to implement, as the app doesn't create a transaction record until a payment method is selected and posted back to the server, so I can't track the transaction status the same way other gateways are handled.

Accept.js seems like the most user-friendly offering from Auth.net at the moment (no off-site payment form, no raw credit card info on server) that fits the structure, so I've set my sights on that ;)

The Accept.js documentation is probably a lot better than it was back in June, but still somewhat basic, especially the final part which seems to require prior knowledge of Auth.net's APIs that I'm missing.

I'd be happy to contribute where I can, just a little lost in the auth.net woods. I'll see if I can figure out how it should work, and how to build an omnipay gateway in the next few days, but if that doesn't work out I'll gladly offer a bounty for you Jason or someone else to build support for Accept.js.

Mark-H avatar Feb 13 '17 22:02 Mark-H

I'm hoping that once we work out the front end, you can then just use AIM for the back end, but using the tokenised card rather than the original card details. I'm trying to get an Accept.JS working to try it out, but the examples are not quite there yet (e.g. the submit button does not wait for the response to the card tokenising before submitting the form to the server, so there are some things to be worked out there). It seems that the merchant site needs to create its own JS functions for submitting the card details and then putting the result back into the form to be submitted. Other gateways tend to include that type of functionality, with some options to change IDs of form items etc. This is lower level, but still workable.

When I can get that working, we can see if the OmnaPay AIM gateway driver needs any tweaks to be able to accept a tokenised card.

judgej avatar Feb 15 '17 13:02 judgej

Okay so I've managed to get the client-side done (I can try to clean it up a bit by getting rid of app-specific code, and submit it for some documentation) and also managed to get the transaction to go through with the following tweaks to Message/AIMAuthorizeRequest.php, in the method addPayment:


    protected function addPayment(\SimpleXMLElement $data)
    {
        if ($this->getDataDescriptor() && $this->getDataValue()) {
            $data->transactionRequest->payment->opaqueData->dataDescriptor = $this->getDataDescriptor();
            $data->transactionRequest->payment->opaqueData->dataValue = $this->getDataValue();
        }
        else {
            $this->validate('card');
            /** @var CreditCard $card */
            $card = $this->getCard();
            $card->validate();
            $data->transactionRequest->payment->creditCard->cardNumber = $card->getNumber();
            $data->transactionRequest->payment->creditCard->expirationDate = $card->getExpiryDate('my');
            $data->transactionRequest->payment->creditCard->cardCode = $card->getCvv();
        }
    }

and adding these methods (I added them to the AIMAuthorizeRequest, but could also go on AIMAbstractRequest):

    public function getDataDescriptor()
    {
        return $this->getParameter('dataDescriptor') ? $this->getParameter('dataDescriptor') : $this->httpRequest->request->get('dataDescriptor');
    }

    public function getDataValue()
    {
        return $this->getParameter('dataValue') ? $this->getParameter('dataValue') : $this->httpRequest->request->get('dataValue');
    }

This allows implementations to pass the dataDescriptor and dataValue in the authorize()/purchase() methods, or it gets them from POST values with the name dataDescriptor/dataValue directly. If neither are available, it uses AIM like it did before.

Mark-H avatar Feb 15 '17 20:02 Mark-H

Looks great :-)

This is where I wish we had different payment types - card, tokenisedCard, PayPal, blank account, ApplePay, saved card etc. that could handle all these things invisibly. Something for a future OmnPay...

This dataDescriptor/getDataValue pair seem very low level, but I'm not sure how else it could be done at this time. The known dataDescriptor values at least should go into the AIMAbstractRequest as constants for easy and consistent use.

Another approach could be to support set/get cardReference methods and have that set both the dataDescriptor and getDataValue pairs. Separate methods to set/get applyPayToken and payPalHash (or whatever it is) would do similar things. Not sure, what do you think? Trying to get as close as possible to a "standard" approach that other gateways would take. The cardReference is certainly used to input a tokenised credit card in other gateway drivers, so that could make sense to use, but we may need to be able to distinguish between saved card tokens and the nonce we have here at some point.

judgej avatar Feb 15 '17 20:02 judgej

It sure would be nice if all things were standardised, but I personally think sticking to the names provided by the gateway is best until that day comes.

  • Stripe expects a token parameter from its client-side tokenisation
  • PayPal Express expects a token parameter, or gets it directly from the query string
  • Paymill is similar to stripe, also expects the token to be provided as parameter.
  • Mollie looks for a transactionReference parameter, or gets the id POSTed to its notification url
  • Braintree seems to have both a paymentMethodToken + paymentMethodNone and a token parameter

Those are the gateways I've worked with recently. In the case of Accept.js it's kinda vague what the names should be, so I went with the "suggestion" in the last example:

    // This is where you would set the data descriptor & data value to be posted back to your server
    console.log(responseData.dataDescriptor);
    console.log(responseData.dataValue);

except they're written to a hidden input instead of logged in the console :)

The suggested implementation in my previous post does need the setters (ugh, and now I realise that's why I needed to get it from the request, because it lacked setters), but I'd go with setDataDescriptor and setDataValue instead of trying to make it too clever. People using the library will still need to build their front-end, so if they can use (where available, cough) official examples in doing so, that sounds like the most straight forward approach.

Mark-H avatar Feb 15 '17 21:02 Mark-H

Here's a simplified version of my javascript, run it after the right js file is loaded and on dom ready. Note: I haven't tested it in this form, I just edited out the bits and pieces specific to my app.

var form = document.getElementById('my-payment-form'),  // Change ID to fit your form
    btns = form.getElementsByTagName('button');  // Maybe change to class based selector if you don't use semantic <button>s
    
form.addEventListener('submit', function(e) {
    e.preventDefault();
    // Disable the submit button to prevent repeated clicks
    for (var j = 0; j < btns.length; j++) {
        btns[j].setAttribute('disabled', true);
    }
    var secureData = {}, authData = {}, cardData = {};

    cardData.cardNumber = document.getElementById('cc-number').value; // Change ID to fit your form
    cardData.month = document.getElementById('cc-exp-month').value; // Change ID to fit your form
    cardData.year = document.getElementById('cc-exp-year').value; // Change ID to fit your form
    secureData.cardData = cardData;

    authData.clientKey = 'YOUR CLIENT KEY HERE';
    authData.apiLoginID = 'YOUR API LOGIN ID HERE';
    secureData.authData = authData;

    Accept.dispatchData(secureData, 'responseHandler');
    return false;
});

function responseHandler(response) {
    if (response.messages.resultCode === 'Error') {
        var msgs = [];
        for (var i = 0; i < response.messages.message.length; i++) {
            msgs.push(response.messages.message[i].code + ': ' + response.messages.message[i].text);
        }
        msgs = msgs.join('<br>');
        var errorContainer = form.querySelector('.errors');
        if (errorContainer.textContent !== undefined) {
            errorContainer.textContent = msgs;
        }
        else {
            errorContainer.innerText = msgs;
        }
        return;
    }

    // Add the hitten inputs with the dataDescriptor and dataValue
    var input = document.createElement('input');
    input.setAttribute('type', 'hidden');
    input.setAttribute('name', 'dataDescriptor');
    input.setAttribute('value', response.opaqueData.dataDescriptor);
    form.appendChild(input);

    var input2 = document.createElement('input');
    input2.setAttribute('type', 'hidden');
    input2.setAttribute('name', 'dataValue');
    input2.setAttribute('value', response.opaqueData.dataValue);
    form.appendChild(input2);

   // Submit the form
    form.submit();
};

Mark-H avatar Feb 15 '17 22:02 Mark-H

Okay, agreed setDataDescriptor and setDataValue as gateway-specific setters. That would be a starting point and offer flexibility for other payment sources that we don't cater for now.

There is a mention of cardReference in the main docs here though that does seem more to be geared towards saved, reusable card tokens. There is still a little ambiguity over whether a temporary nonce is a Token or a CardReference. I guess we can sort that out afterwards - it's just a layer on top.

judgej avatar Feb 15 '17 22:02 judgej

I've been trying similar front end code to yours, but always get error:

E_WC_15:An error occurred during processing. Please try again.

The docs say that means:

Please provide valid CVV.

I've tried adding the CVV (cardCode) but get the same error. Not sure if you hit this and got around it? Sorry, this is a bit of a tangent.

judgej avatar Feb 15 '17 22:02 judgej

Hmm no I didn't get that error, but only now notice that the cvv isn't being added in my request code. Will try it out tomorrow.

Mark-H avatar Feb 15 '17 22:02 Mark-H

Silly me - I was using my account login ID instead of the API login ID. Works fine now after swapping that over. The error message was a little misleading. Just realised also the dataDescriptor (COMMON.ACCEPT.INAPP.PAYMENT) is provided by Accept.JS along with the card token, so it is not something that the merchant account needs to know. I was thinking before that the dataDescriptor would need to be coded in somewhere.

judgej avatar Feb 15 '17 22:02 judgej

Something has always bothered me about these submit handlers. Here, the submit is caught, the card is tokenised, and the handler getting the token response puts the result into the form, then submits the form. That is great, and works, so long as you do not have any other form validation on the form. If you do, and it's JavaScript powered, then it all gets skipped since the submit() will submit the form immediately and at the lowest level. The new SagePay JS front end does that too, and that one is very particular about the validation of all the other details that will be submitted (the billing address must be submitted with the purchase transaction and everything must be valid, and you get three grace attempts to submit, except you only get one error returned at a time, so had better hope that you don't have three errors in your address fields).

I'm wondering if the approach is to catch the submit, then if the card has already been tokenised, just return true so the remainder of any validation on the form can run, and then the form can submit to the server naturally. But if the card has not yet been tokenised, then do the tokenisation, use the asynchronous result to fill out the hidden fields, but then instead of directly submitting the form at this stage, go back and re-trigger the form submit action, e.g. the submit button. That way all events linked to the form can get their chance to run. I just don't know if that is possible. Any idea?

Edit 2: been playing with this, an come up with this solution, which seems to work as I wanted. Take a look and see if it is any use. Try submitting with an error in the email field, then correct it and submit again. Going to see if I can apply this to Sage Pay now, because their JS approach has been driving me mad. They use the preventDefault() function on the form, which is a bloody nightmare to work around, because any script issuing that function is now saying , "this form is MINE".


On top of this, if the form does submit with the tokenised card, then if the form fails any server-side validation, it would be nice to be able to get the form back for correction with the token intact and the credit card fields hidden so they don't need to be entered again. So many shops get this wrong - I've lost count of how uncomfortable it is entering my CC details multiple times because, say, the server did not like my postcode without a space in it. I've entered it once, godamnit, stop asking me to enter it again! :-) Sorry, ranting.

judgej avatar Feb 15 '17 23:02 judgej

Just a thought: the tokenised card consists of two strings, e.g.:

array(3) {
  ["dataDescriptor"]=>
  string(27) "COMMON.ACCEPT.INAPP.PAYMENT"
  ["dataValue"]=>
  string(22) "9487554895312816104603"
}

As well as accepting them as two separate values (no argument there) I wonder if the driver should also be able to accept them as a single string? For example "COMMON.ACCEPT.INAPP.PAYMENT 9487554895312816104603" (with whatever field separator) or as a JSON string.

judgej avatar Feb 20 '17 01:02 judgej

@Mark-H If you would like to submit a PR for the set/get methods, preferably with a test, that would be very useful. If not, I'll do one, since it's a small change and opens up a new gateway API nicely. The front end bit we can work on as a section on the README.

judgej avatar Feb 22 '17 10:02 judgej

Hey guys, I'm currently facing the situation, therefore I created PR https://github.com/thephpleague/omnipay-authorizenet/pull/77

I'm still wondering if we should pass opaque data via setters

$request->setOpaqueDataDescriptor($data_descriptor);
$request->setOpaqueDataValue($data_value);

or via the request params:

$request = $gateway->purchase(
    [
        'notifyUrl' => 'https://www.perdu.com',
        'amount' => $amount,
	'dataDescriptor' => $data_descriptor,
	'dataValue' => $data_value,
    ]
);

As far as I know, Omnipay Stripe does support a token parameter when creating the request.

felixmaier1989 avatar Mar 02 '17 08:03 felixmaier1989

The card details (which is what the "opaque data" is) belongs in the request object rather then the gateway object. The gateway configuration data is for invariant settings - switching modes, authentication details, system-wide callback/notification URLs etc. So the first option is definitely the way to do it.

judgej avatar Mar 02 '17 10:03 judgej

~I'm not sure if I agree @judgej - I would definitely expect the second example @felixmaier1989 posted over the first one, which is also how I have it working in my dev site. Internally that might rely on such a setter, but passing in the dataDescriptor/Value in with the purchase() method should most definitely work.~ They seem to be the same thing, after another look.

Sorry for not responding about doing a PR. Was trying to find the time/energy to spin up a fork and figure out how to add tests to omnipay but it's been a rough week productivity wise.

Mark-H avatar Mar 02 '17 14:03 Mark-H

Ah, yes, sorry you are right. I misread that. I thought one of those examples was passing the data into the gateway object, but looking again, they are both creating the request object. What I said was technically right, but totally out of context here - with the setters in place, both those approaches will work (though the second one needs the "opaque" prefix to map onto the setters, i.e.

$request = $gateway->purchase(
    [
        'notifyUrl' => 'https://www.perdu.com',
        'amount' => $amount,
	'opaqueDataDescriptor' => $data_descriptor,
	'opaqueDataValue' => $data_value,
    ]
);

judgej avatar Mar 02 '17 14:03 judgej

Thanks all - I'm just adding a few notes to the README about this then I'll merge. It's all hard work for everyone trying to fit all this sharing into a day job and life in general, and everyone understands that :-)

judgej avatar Mar 03 '17 12:03 judgej

A PR has been merged which allows the AIM gateway to accept tokenized card details in place of the card details in the CreditCard object. It seems, for the back-end OmniPay at least, this is all that is needed to support Accept.JS. There is obviously front-end details to this, and examples will be welcome to add to the documentation, but that functionality at least is out of scope for this gateway driver.

The method of giving this driver the card details (TWO values and not just one) is specific to this gateway. We can wrap those up in a method shared with other gateways next, but this gets people moving forward in the meantime. One thought is that the cardReference is used, and the two tokenization strings are just concatenated, or maybe JSON encoded together (which could give a lot of extra flexibility, though is a bit more clumsy).

The resulting card reference will be massive long though. I suspect it contains the CC details encrypted rather than just an index for a what is cached on the gateway, but that's just a wild guess.

So, thoughts on a wrapper function to make it fall in line with other gateways?

judgej avatar Mar 03 '17 12:03 judgej

Sorry to jump in and hijack this thread, but I'm Aaron (Developer Evangelist from Authorize.Net) and I wanted to clear up something in this comment from Jun 21:

As an aside, AIM is now called "Payment Transactions". AIM looks to be a name being slowly phased out. I'm wondering if the old AIM should have been left untouched in this gateway driver, and a separate new gateway created for the XML upgrade. JSON is available in beta as an alternative to XML - would have been nice to have had the JSON first, as it is much easier to handle in PHP. All done now, I guess.

AIM is not now being called "Payment Transactions" nor is it the same as anything in our current API. That's our fault, because in the upgrade guide you link to, there used to be some wording along the lines of "AIM is now called Payment Transactions". That's never been true, and I think the wording was due to some internal miscommunication and misunderstanding.

It's more correct to say that AIM is and always will be a name for an older API of ours that was just for straight payment type of transactions based on doing an HTTP post with a bunch of name value pairs. We then had other similar APIs (CIM, ARB, DPM, SIM) to do things like hosted payment forms, card on file profiles, subscriptions, etc.

Instead of maintaining a separate API for each type of interaction, we've supplanted all of these other APIs with one current API that serves all of those functions, and is hierarchical and extensible for future use. This current API can be used by sending requests as XML or JSON, but the parameter names and the hierarchy are the same in each. These older APIs like AIM are in some cases officially deprecated or EOL'ed, but in all cases will not be receiving any feature improvements.

That's apparent with Accept.js, which returns a token that can only be used in a payment transaction request to our current API. There's no way to use this token with AIM.

As to how this relates to this project, if we had been clear about this strategy from the start, perhaps you would have put the existing AIM implementation into maintenance mode and done the work for current XML/JSON requests in a separate project. Seems like you went the other way, so I'm really sorry about that.

Please accept my deepest apologies, and please allow me to make it up to you by offering any possible assistance. Anything you need me for, let me know.

adavidw avatar Mar 07 '17 23:03 adavidw

This doesn't even get into the window of time a few years ago where we were trying to brand the new XML API as "AIM XML" and the old one as "AIM NVP". Don't get me started on that.

adavidw avatar Mar 07 '17 23:03 adavidw

So in other words, to be consistent we should not maintain AIMGateway but develop a newer Omnipay gateway AuthorizeNetGateway (let's call it so). AuthorizeNetGateway would contain the AIM methods + Opaque data handling.

Assuming I'm right, technically, what we have to do is:

  • copying AIMGateway to AuthorizeNetGateway
  • copying the unit tests as well
  • reverting the Opaque Data handling in AIMGateway
  • and AIMGateway should not be maintained anymore

felixmaier1989 avatar Mar 08 '17 03:03 felixmaier1989

Well, maybe. It really depends on what your strategy is and how much backward compatibility you want maintain, things like that. Also, depends on how much XML you're already using in the AIM gateway vs the form posts with the name value pairs. I haven't been through the code yet to see what's XML and what's just form posts to the old endpoint.

Also, I haven't seen whether this driver attempts to support other old APIs other than the payment transactions in the AIM API (like SIM or CIM or ARB). If this gateway is only supporting the AIM transactions, and the actual transactions requests are being converted to XML or are already XML, there's no reason to fork the project.

The XML format for the AIM API is what was expanded into the current XML/JSON API. So, any "AIM" transactions that are using the XML format of AIM (what we used to call "AIM XML") are already structured correctly for the current API, and already going to the right endpoint. So, if you've already converted to XML, or were planning to anyway, then you don't really need to change course or fork the project. You just probably would want to drop "AIM" from the name since you're now including things like the opaque data from the current API. In that scenario, there's probably little value in keeping an old "AIM-only" version of the project since there's nothing that AIM did by itself that the new API can't do.

That's my general thinking, but I could have more specific recommendations depending on what you've already done or are planning to do.

adavidw avatar Mar 08 '17 03:03 adavidw