[IMP] payment_authorize: integration with accept js

add accept js of authorize.net to make s2s flow pci compliance

after clicking on pay now button, one popup display with card inputs
    popup is provided by a authorize with all validation facilities
    After submitting details, payment flow is
    - get the temp token information from authorize
    - create a token with that temp token information in odoo
    - make a request to authorize for charge
    - after successful request, payment will be charged for that card

task- 2025821
This commit is contained in:
Nikunj Ladava
2019-08-07 11:27:44 +00:00
committed by Damien Bouvy
parent 26f3d8465d
commit a4f8616a17
7 changed files with 207 additions and 94 deletions
@@ -30,19 +30,6 @@ class AuthorizeController(http.Controller):
'return_url': urls.url_join(base_url, "/payment/process")
})
@http.route(['/payment/authorize/s2s/create_json'], type='json', auth='public')
def authorize_s2s_create_json(self, **kwargs):
acquirer_id = int(kwargs.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
try:
return acquirer.s2s_process(kwargs).id
except (ValidationError, UserError) as e:
return {
'error': e.name,
}
@http.route(['/payment/authorize/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def authorize_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
token = False
@@ -14,6 +14,7 @@
<p>You will be redirected to the Authorize website after clicking on the payment button.</p>]]></field>
<field name='authorize_login'>dummy</field>
<field name="authorize_transaction_key">dummy</field>
<field name="authorize_signature_key">dummy</field>
</record>
</data>
@@ -34,6 +34,8 @@ class AuthorizeAPI():
self.url = 'https://apitest.authorize.net/xml/v1/request.api'
else:
self.url = 'https://api.authorize.net/xml/v1/request.api'
self.environment = acquirer.environment
self.name = acquirer.authorize_login
self.transaction_key = acquirer.authorize_transaction_key
@@ -52,7 +54,7 @@ class AuthorizeAPI():
return resp
# Customer profiles
def create_customer_profile(self, partner, cardnumber, expiration_date, card_code):
def create_customer_profile(self, partner, opaqueData):
"""Create a payment and customer profile in the Authorize.net backend.
Creates a customer profile for the partner/credit card combination and links
@@ -90,15 +92,14 @@ class AuthorizeAPI():
'country': partner.country_id.name or None
},
'payment': {
'creditCard': {
'cardNumber': cardnumber,
'expirationDate': expiration_date,
'cardCode': card_code
'opaqueData': {
'dataDescriptor': opaqueData.get('dataDescriptor'),
'dataValue': opaqueData.get('dataValue')
}
}
}
},
'validationMode': 'liveMode'
'validationMode': 'liveMode' if self.environment == 'prod' else 'testMode'
}
}
@@ -375,3 +376,18 @@ class AuthorizeAPI():
if response and response.get('err_code'):
return False
return True
# Client Key
def get_client_secret(self):
""" Create a client secret that will be needed for the AcceptJS integration. """
values = {
"getMerchantDetailsRequest": {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key,
}
}
}
response = self._authorize_request(values)
client_secret = response.get('publicClientKey')
return client_secret
+16 -57
View File
@@ -2,18 +2,15 @@
from werkzeug import urls
from .authorize_request import AuthorizeAPI
from datetime import datetime
import hashlib
import hmac
import logging
import string
import time
from odoo import _, api, fields, models
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_authorize.controllers.main import AuthorizeController
from odoo.tools.float_utils import float_compare, float_repr
from odoo.tools.safe_eval import safe_eval
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -25,7 +22,8 @@ class PaymentAcquirerAuthorize(models.Model):
provider = fields.Selection(selection_add=[('authorize', 'Authorize.Net')])
authorize_login = fields.Char(string='API Login Id', required_if_provider='authorize', groups='base.group_user')
authorize_transaction_key = fields.Char(string='API Transaction Key', required_if_provider='authorize', groups='base.group_user')
authorize_signature_key = fields.Char(string='API Signature Key', groups='base.group_user', compute="_compute_auth_signature_key", inverse="_inverse_auth_signature_key")
authorize_signature_key = fields.Char(string='API Signature Key', required_if_provider='authorize', groups='base.group_user')
authorize_client_key = fields.Char(string='API Client Key', groups='base.group_user')
@api.onchange('provider', 'check_validity')
def onchange_check_validity(self):
@@ -35,6 +33,13 @@ class PaymentAcquirerAuthorize(models.Model):
'title': _("Warning"),
'message': ('This option is not supported for Authorize.net')}}
def action_client_secret(self):
api = AuthorizeAPI(self)
if not api.test_authenticate():
raise UserError(_('Unable to fetch Client Key, make sure the API Login and Transaction Key are correct.'))
self.authorize_client_key = api.get_client_secret()
return True
def _get_feature_support(self):
"""Get advanced feature support by provider.
@@ -51,16 +56,6 @@ class PaymentAcquirerAuthorize(models.Model):
res['tokenize'].append('authorize')
return res
def _compute_auth_signature_key(self):
ICP = self.env['ir.config_parameter'].sudo()
for acquirer in self.filtered(lambda a: a.provider == 'authorize'):
acquirer.authorize_signature_key = ICP.get_param('payment_authorize.signature_key_%s' % acquirer.id)
def _inverse_auth_signature_key(self):
ICP = self.env['ir.config_parameter'].sudo()
for acquirer in self.filtered(lambda a: a.provider == 'authorize'):
ICP.set_param('payment_authorize.signature_key_%s' % acquirer.id, acquirer.authorize_signature_key)
def _get_authorize_urls(self, environment):
""" Authorize URLs """
if environment == 'prod':
@@ -76,24 +71,7 @@ class PaymentAcquirerAuthorize(models.Model):
values['x_amount'],
values['x_currency_code']]).encode('utf-8')
# [BACKWARD COMPATIBILITY, 2nd edition]
# The signature key is now '128-character hexadecimal format', while the
# transaction key was only 16-character.
# One of 2 things should have happened:
# 1/ the Transaction Key has been replaced with the Signature Key value (patch from March 2019)
# => Use that to sign, but server-to-server won't work since it uses transaction key
# as its credentials
# 2/ the Signature key is a new field (patch from July 2019)
# => Use that field for the signature
# FORWARD-PORT NOTE: forward part to saas-12.4 but no further
if len(values['x_trans_key']) == 128 and not self.authorize_signature_key:
self.authorize_signature_key = values['x_trans_key'] # store in the correct field
return hmac.new(bytes.fromhex(values['x_trans_key']), data, hashlib.sha512).hexdigest().upper()
elif self.authorize_signature_key:
return hmac.new(bytes.fromhex(self.authorize_signature_key), data, hashlib.sha512).hexdigest().upper()
else:
return hmac.new(values['x_trans_key'].encode('utf-8'), data, hashlib.md5).hexdigest()
return hmac.new(bytes.fromhex(self.authorize_signature_key), data, hashlib.sha512).hexdigest().upper()
def authorize_form_generate_values(self, values):
self.ensure_one()
@@ -110,7 +88,6 @@ class PaymentAcquirerAuthorize(models.Model):
authorize_tx_values = dict(values)
temp_authorize_tx_values = {
'x_login': self.authorize_login,
'x_trans_key': self.authorize_transaction_key,
'x_amount': float_repr(values['amount'], values['currency'].decimal_places if values['currency'] else 2),
'x_show_form': 'PAYMENT_FORM',
'x_type': 'AUTH_CAPTURE' if not self.capture_manually else 'AUTH_ONLY',
@@ -143,7 +120,6 @@ class PaymentAcquirerAuthorize(models.Model):
}
temp_authorize_tx_values['returndata'] = authorize_tx_values.pop('return_url', '')
temp_authorize_tx_values['x_fp_hash'] = self._authorize_generate_hashing(temp_authorize_tx_values)
temp_authorize_tx_values.pop('x_trans_key') # We remove this value since it is secret and isn't needed on the form
authorize_tx_values.update(temp_authorize_tx_values)
return authorize_tx_values
@@ -154,11 +130,8 @@ class PaymentAcquirerAuthorize(models.Model):
@api.model
def authorize_s2s_form_process(self, data):
values = {
'cc_number': data.get('cc_number'),
'cc_holder_name': data.get('cc_holder_name'),
'cc_expiry': data.get('cc_expiry'),
'cc_cvc': data.get('cc_cvc'),
'cc_brand': data.get('cc_brand'),
'opaqueData': data.get('opaqueData'),
'encryptedCardData': data.get('encryptedCardData'),
'acquirer_id': int(data.get('acquirer_id')),
'partner_id': int(data.get('partner_id'))
}
@@ -167,23 +140,11 @@ class PaymentAcquirerAuthorize(models.Model):
def authorize_s2s_form_validate(self, data):
error = dict()
mandatory_fields = ["cc_number", "cc_cvc", "cc_holder_name", "cc_expiry", "cc_brand"]
mandatory_fields = ["opaqueData", "encryptedCardData"]
# Validation
for field_name in mandatory_fields:
if not data.get(field_name):
error[field_name] = 'missing'
if data['cc_expiry']:
# FIX we split the date into their components and check if there is two components containing only digits
# this fixes multiples crashes, if there was no space between the '/' and the components the code was crashing
# the code was also crashing if the customer was proving non digits to the date.
cc_expiry = [i.strip() for i in data['cc_expiry'].split('/')]
if len(cc_expiry) != 2 or any(not i.isdigit() for i in cc_expiry):
return False
try:
if datetime.now().strftime('%y%m') > datetime.strptime('/'.join(cc_expiry), '%m/%y').strftime('%y%m'):
return False
except ValueError:
return False
return False if error else True
def authorize_test_credentials(self):
@@ -364,17 +325,15 @@ class PaymentToken(models.Model):
@api.model
def authorize_create(self, values):
if values.get('cc_number'):
values['cc_number'] = values['cc_number'].replace(' ', '')
if values.get('opaqueData') and values.get('encryptedCardData'):
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
expiry = str(values['cc_expiry'][:2]) + str(values['cc_expiry'][-2:])
partner = self.env['res.partner'].browse(values['partner_id'])
transaction = AuthorizeAPI(acquirer)
res = transaction.create_customer_profile(partner, values['cc_number'], expiry, values['cc_cvc'])
res = transaction.create_customer_profile(partner, values['opaqueData'])
if res.get('profile_id') and res.get('payment_profile_id'):
return {
'authorize_profile': res.get('profile_id'),
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name']),
'name': values['encryptedCardData'].get('cardNumber'),
'acquirer_ref': res.get('payment_profile_id'),
'verified': True
}
@@ -0,0 +1,153 @@
odoo.define('payment_authorize.payment_form', function (require) {
"use strict";
var ajax = require('web.ajax');
var core = require('web.core');
var PaymentForm = require('payment.payment_form');
var _t = core._t;
PaymentForm.include({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* called when clicking on pay now or add payment event to create token for credit card/debit card.
*
* @private
* @param {Event} ev
* @param {DOMElement} checkedRadio
* @param {Boolean} addPmEvent
*/
_createAuthorizeToken: function (ev, $checkedRadio, addPmEvent) {
var self = this;
if (ev.type === 'submit') {
var button = $(ev.target).find('*[type="submit"]')[0]
} else {
var button = ev.target;
}
this.disableButton(button);
var acquirerID = this.getAcquirerIdFromRadio($checkedRadio);
var acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
var inputsForm = $('input', acquirerForm);
var formData = self.getFormData(inputsForm);
if (this.options.partnerId === undefined) {
console.warn('payment_form: unset partner_id when adding new token; things could go wrong');
}
var AcceptJs = false;
if (formData.acquirer_environment === 'prod') {
AcceptJs = 'https://js.authorize.net/v3/AcceptUI.js';
} else {
AcceptJs = 'https://jstest.authorize.net/v3/AcceptUI.js';
}
window.responseHandler = function (response) {
_.extend(formData, response);
if (response.messages.resultCode === "Error") {
var errorMessage = "";
_.each(response.messages.message, function (message) {
errorMessage += message.code + ": " + message.text;
})
acquirerForm.removeClass('d-none');
self.enableButton(button);
return self.displayError(_t('Server Error'), errorMessage);
}
self._rpc({
route: formData.data_set,
params: formData
}).then (function (data) {
if (addPmEvent) {
if (formData.return_url) {
window.location = formData.return_url;
} else {
window.location.reload();
}
} else {
$checkedRadio.val(data.id);
self.el.submit();
}
}).guardedCatch(function (error) {
// if the rpc fails, pretty obvious
error.event.preventDefault();
acquirerForm.removeClass('d-none');
self.enableButton(button);
self.displayError(
_t('Server Error'),
_t("We are not able to add your payment method at the moment.") +
error.message.data.message
);
});
};
if (this.$button === undefined) {
var params = {
'class': 'AcceptUI d-none',
'data-apiLoginID': formData.login_id,
'data-clientKey': formData.client_key,
'data-billingAddressOptions': '{"show": false, "required": false}',
'data-responseHandler': 'responseHandler'
};
this.$button = $('<button>', params);
this.$button.appendTo('body');
}
ajax.loadJS(AcceptJs).then(function () {
self.$button.trigger('click');
});
},
/**
* @override
*/
updateNewPaymentDisplayStatus: function () {
var $checkedRadio = this.$('input[type="radio"]:checked');
if ($checkedRadio.length !== 1) {
return;
}
// hide add token form for authorize
if ($checkedRadio.data('provider') === 'authorize' && this.isNewPaymentRadio($checkedRadio)) {
this.$('[id*="o_payment_add_token_acq_"]').addClass('d-none');
} else {
this._super.apply(this, arguments);
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
*/
payEvent: function (ev) {
ev.preventDefault();
var $checkedRadio = this.$('input[type="radio"]:checked');
// first we check that the user has selected a authorize as s2s payment method
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'authorize') {
this._createAuthorizeToken(ev, $checkedRadio);
} else {
this._super.apply(this, arguments);
}
},
/**
* @override
*/
addPmEvent: function (ev) {
ev.stopPropagation();
ev.preventDefault();
var $checkedRadio = this.$('input[type="radio"]:checked');
// first we check that the user has selected a authorize as add payment method
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'authorize') {
this._createAuthorizeToken(ev, $checkedRadio, true);
} else {
this._super.apply(this, arguments);
}
},
});
});
@@ -51,28 +51,20 @@
</template>
<template id="authorize_s2s_form">
<input type="hidden" name="data_set" data-create-route="/payment/authorize/s2s/create_json_3ds"/>
<div t-attf-class="row mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
<div t-att-class="'form-group col-lg-12' if bootstrap_formatting else 'form-group'">
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
<div class="card_placeholder"></div>
<div class="visa"></div>
<input type="hidden" name="cc_brand" value=""/>
</div>
<div t-att-class="'form-group col-lg-5' if bootstrap_formatting else 'form-group'">
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name" data-is-required="true"/>
</div>
<div t-att-class="'form-group col-lg-3' if bootstrap_formatting else 'form-group'">
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)" data-is-required="true"/>
</div>
<div t-att-class="'form-group col-lg-4' if bootstrap_formatting else 'form-group'">
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC" data-is-required="true"/>
</div>
</div>
<input type="hidden" name="data_set" value="/payment/authorize/s2s/create_json_3ds"/>
<input type="hidden" name="acquirer_id" t-att-value="id"/>
<input type="hidden" name="acquirer_environment" t-att-value="acq.environment"/>
<input type="hidden" name="login_id" t-att-value="acq.sudo().authorize_login"/>
<input type="hidden" name="client_key" t-att-value="acq.sudo().authorize_client_key"/>
<input class="d-none" name="csrf_token" t-att-value="request.csrf_token()"/>
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
</template>
<template id="assets_frontend" inherit_id="web.assets_frontend">
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/payment_authorize/static/src/js/payment_form.js"></script>
</xpath>
</template>
</data>
</odoo>
@@ -10,6 +10,11 @@
<field name="authorize_login"/>
<field name="authorize_transaction_key" password="True"/>
<field name="authorize_signature_key" password="True"/>
<label for="authorize_client_key"/>
<div>
<field name="authorize_client_key" password="True"/>
<button class="oe_link" icon="fa-refresh" type="object" name="action_client_secret" string="Generate Client Key" />
</div>
<a colspan="2" href="https://www.odoo.com/documentation/user/online/ecommerce/shopper_experience/authorize.html" target="_blank">How to get paid with Authorize.Net</a>
</group>
</xpath>