Files
odoo_source/addons/payment_ogone/controllers/main.py
T
Antoine Vandevenne (anv)andLucie Van Nieuwenhuyze 00259dc44a [IMP] payment_*: improve handling of webhook notifications
Notification handling in some acquirers presents a subset of the
following issues:
1. The signature of synchronous notifications (redirect payloads) is not
   checked. (Alipay, Authorize, Buckaroo, Mollie, PayU money, PayULatam)
2. When the signature check fails, we raise a ValidationError which
   counts as an HTTP 200 for some providers (it's not the case if they
   expect a specific string). (Adyen, Paypal,  Sips, Stripe)
3. If a ValidationError is raised when processing the feedback data, it
   is allowed to bubble up to the provider. (Alipay, Ogone)

The issues are respectively addressed as follows:
1. If the acquirer implements payments with redirection, make sure that
   if either makes a request to the provider to validate the data or
   that it verifies the signature. Verifying the origin of the request
   is not enough: the payload must be checked too.
2. Instead of raising ValidationError's, raise an HTTP 403 FORBIDDEN
   error if the signature check fails.
3. Wrap the call to `_handle_feedback_data` of the webhook method inside
   a try/except clause to catch any ValidationError, log a warning, and
   acknowledge the notification to avoid having the provider disable the
   webhook because of too many failures.

task-2688139
task-2693293

closes odoo/odoo#81607

Signed-off-by: Antoine Vandevenne (anv) <anv@odoo.com>
Co-authored-by: Lucie Van Nieuwenhuyze <luvn@odoo.com>
2022-01-27 17:11:52 +00:00

93 lines
4.0 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hmac
import logging
import pprint
import re
from werkzeug.exceptions import Forbidden
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class OgoneController(http.Controller):
_return_url = '/payment/ogone/return'
_backward_compatibility_urls = [
'/payment/ogone/accept', '/payment/ogone/test/accept',
'/payment/ogone/decline', '/payment/ogone/test/decline',
'/payment/ogone/exception', '/payment/ogone/test/exception',
'/payment/ogone/cancel', '/payment/ogone/test/cancel',
'/payment/ogone/validate/accept',
'/payment/ogone/validate/decline',
'/payment/ogone/validate/exception',
] # Facilitates the migration of users who registered the URLs in Ogone's backend prior to 14.3
@http.route(
[_return_url] + _backward_compatibility_urls, type='http', auth='public',
methods=['GET', 'POST'], csrf=False
) # Redirect are made with GET requests only. Webhook notifications can be set to GET or POST.
def ogone_return_from_checkout(self, **raw_data):
""" Process the notification data sent by Ogone after redirection from checkout.
This route can also accept S2S notifications from Ogone if it is configured as a webhook in
Ogone's backend. The user can choose between GET or POST for the webhook notifications.
:param dict raw_data: The un-formatted notification data
"""
_logger.info("handling redirection from Ogone with data:\n%s", pprint.pformat(raw_data))
data = self._normalize_data_keys(raw_data)
# Check the integrity of the notification
received_signature = data.get('SHASIGN')
tx_sudo = request.env['payment.transaction'].sudo()._get_tx_from_feedback_data(
'ogone', data
)
self._verify_notification_signature(raw_data, received_signature, tx_sudo)
# Handle the notification data
request.env['payment.transaction'].sudo()._handle_feedback_data('ogone', data)
return request.redirect('/payment/status')
@staticmethod
def _normalize_data_keys(data):
""" Set all keys of a dictionary to upper-case.
The keys received from Ogone APIs have inconsistent formatting and must be homogenized to
allow re-using the same methods. We reformat them to follow a unified nomenclature inspired
by Ogone Directlink API.
Formatting steps:
1) Uppercase key strings: 'Something' -> 'SOMETHING', 'something' -> 'SOMETHING'
2) Remove the prefix: 'CARD.SOMETHING' -> 'SOMETHING', 'ALIAS.SOMETHING' -> 'SOMETHING'
:param dict data: The data whose keys to normalize
:return: The normalized data
:rtype: dict
"""
return {re.sub(r'.*\.', '', k.upper()): v for k, v in data.items()}
@staticmethod
def _verify_notification_signature(notification_data, received_signature, tx_sudo):
""" Check that the received signature matches the expected one.
:param dict notification_data: The notification data
:param str received_signature: The signature received with the notification data
:param recordset tx_sudo: The sudoed transaction referenced by the notification data, as a
`payment.transaction` record
:return: None
:raise: :class:`werkzeug.exceptions.Forbidden` if the signatures don't match
"""
# Check for the received signature
if not received_signature:
_logger.warning("received notification with missing signature")
raise Forbidden()
# Compare the received signature with the expected signature computed from the data
expected_signature = tx_sudo.acquirer_id._ogone_generate_signature(notification_data)
if not hmac.compare_digest(received_signature, expected_signature.upper()):
_logger.warning("received notification with invalid signature")
raise Forbidden()