diff --git a/Controller/Process/Json.php b/Controller/Process/Json.php
index d0f6aa59e..afb9c9091 100755
--- a/Controller/Process/Json.php
+++ b/Controller/Process/Json.php
@@ -23,6 +23,7 @@
namespace Adyen\Payment\Controller\Process;
+use Adyen\Util\HmacSignature;
use Symfony\Component\Config\Definition\Exception\Exception;
use Magento\Framework\App\Request\Http as Http;
@@ -56,6 +57,21 @@ class Json extends \Magento\Framework\App\Action\Action
*/
private $serializer;
+ /**
+ * @var \Adyen\Payment\Helper\Config
+ */
+ protected $configHelper;
+
+ /**
+ * @var \Adyen\Payment\Helper\IpAddress
+ */
+ protected $ipAddressHelper;
+
+ /**
+ * @var HmacSignature
+ */
+ private $hmacSignature;
+
/**
* Json constructor.
*
@@ -63,12 +79,18 @@ class Json extends \Magento\Framework\App\Action\Action
* @param \Adyen\Payment\Helper\Data $adyenHelper
* @param \Adyen\Payment\Logger\AdyenLogger $adyenLogger
* @param \Magento\Framework\Serialize\SerializerInterface $serializer
+ * @param \Adyen\Payment\Helper\Config $configHelper
+ * @param \Adyen\Payment\Helper\IpAddress $ipAddressHelper
+ * @param HmacSignature $hmacSignature
*/
public function __construct(
\Magento\Framework\App\Action\Context $context,
\Adyen\Payment\Helper\Data $adyenHelper,
\Adyen\Payment\Logger\AdyenLogger $adyenLogger,
- \Magento\Framework\Serialize\SerializerInterface $serializer
+ \Magento\Framework\Serialize\SerializerInterface $serializer,
+ \Adyen\Payment\Helper\Config $configHelper,
+ \Adyen\Payment\Helper\IpAddress $ipAddressHelper,
+ HmacSignature $hmacSignature
) {
parent::__construct($context);
$this->_objectManager = $context->getObjectManager();
@@ -76,6 +98,9 @@ public function __construct(
$this->_adyenHelper = $adyenHelper;
$this->_adyenLogger = $adyenLogger;
$this->serializer = $serializer;
+ $this->configHelper = $configHelper;
+ $this->ipAddressHelper = $ipAddressHelper;
+ $this->hmacSignature = $hmacSignature;
// Fix for Magento2.3 adding isAjax to the request params
if (interface_exists(\Magento\Framework\App\CsrfAwareActionInterface::class)) {
@@ -180,6 +205,25 @@ protected function _validateNotificationMode($notificationMode)
*/
protected function _processNotification($response, $notificationMode)
{
+ if ($this->configHelper->getNotificationsIpHmacCheck()) {
+ //Validate if the notification comes from a verified IP
+ if (!$this->isIpValid()) {
+ $this->_adyenLogger->addAdyenNotification(
+ "Notification has been rejected because the IP address could not be verified"
+ );
+ return false;
+ }
+
+ if ($this->hmacSignature->isHmacSupportedEventCode($response)) {
+ //Validate the Hmac calculation
+ if (!$this->hmacSignature->isValidNotificationHMAC($this->configHelper->getNotificationsHmacKey(),
+ $response)) {
+ $this->_adyenLogger->addAdyenNotification('HMAC key validation failed ' . print_r($response, 1));
+ return false;
+ }
+ }
+ }
+
// validate the notification
if ($this->authorised($response)) {
// log the notification
@@ -271,18 +315,18 @@ protected function authorised($response)
}
// validate username and password
- if ((!isset($_SERVER['PHP_AUTH_USER']) && !isset($_SERVER['PHP_AUTH_PW']))) {
+ if ((!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW']))) {
if ($this->_isTestNotification($response['pspReference'])) {
$this->_returnResult(
- 'Authentication failed: PHP_AUTH_USER and PHP_AUTH_PW are empty. See Adyen Magento manual CGI mode'
+ 'Authentication failed: PHP_AUTH_USER or PHP_AUTH_PW are empty. See Adyen Magento manual CGI mode'
);
}
return false;
}
- $usernameCmp = strcmp($_SERVER['PHP_AUTH_USER'], $username);
- $passwordCmp = strcmp($_SERVER['PHP_AUTH_PW'], $password);
- if ($usernameCmp === 0 && $passwordCmp === 0) {
+ $usernameIsValid = hash_equals($username, $_SERVER['PHP_AUTH_USER']);
+ $passwordIsValid = hash_equals($password, $_SERVER['PHP_AUTH_PW']);
+ if ($usernameIsValid && $passwordIsValid) {
return true;
}
@@ -295,6 +339,29 @@ protected function authorised($response)
return false;
}
+ /**
+ * Checks if any of the possible remote IP address sending the notification is verified and returns the validation result
+ *
+ * @return bool
+ */
+ protected function isIpValid()
+ {
+ $ipAddress = [];
+
+ //Getting remote and possibly forwarded IP addresses
+ if (!empty($_SERVER['REMOTE_ADDR'])) {
+ array_push($ipAddress, $_SERVER['REMOTE_ADDR']);
+ }
+ if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+ array_push($ipAddress, $_SERVER['HTTP_X_FORWARDED_FOR']);
+ }
+ if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
+ array_push($ipAddress, $_SERVER['HTTP_CLIENT_IP']);
+ }
+
+ return $this->ipAddressHelper->isIpAddressValid($ipAddress);
+ }
+
/**
* If notification is already saved ignore it
*
diff --git a/Controller/Process/ResultPos.php b/Controller/Process/ResultPos.php
deleted file mode 100644
index 258671700..000000000
--- a/Controller/Process/ResultPos.php
+++ /dev/null
@@ -1,283 +0,0 @@
-
- */
-
-namespace Adyen\Payment\Controller\Process;
-
-class ResultPos extends \Magento\Framework\App\Action\Action
-{
- /**
- * @var \Adyen\Payment\Helper\Data
- */
- protected $_adyenHelper;
-
- /**
- * @var \Magento\Sales\Model\OrderFactory
- */
- protected $_orderFactory;
-
- /**
- * @var \Magento\Sales\Model\Order
- */
- protected $_order;
-
- /**
- * @var \Magento\Sales\Model\Order\Status\HistoryFactory
- */
- protected $_orderHistoryFactory;
-
- /**
- * @var \Magento\Checkout\Model\Session
- */
- protected $_session;
-
- /**
- * @var \Adyen\Payment\Logger\AdyenLogger
- */
- protected $_adyenLogger;
-
- /**
- * ResultPos constructor.
- *
- * @param \Magento\Framework\App\Action\Context $context
- * @param \Adyen\Payment\Helper\Data $adyenHelper
- * @param \Magento\Sales\Model\OrderFactory $orderFactory
- * @param \Magento\Sales\Model\Order\Status\HistoryFactory $orderHistoryFactory
- * @param \Magento\Checkout\Model\Session $session
- * @param \Adyen\Payment\Logger\AdyenLogger $adyenLogger
- */
- public function __construct(
- \Magento\Framework\App\Action\Context $context,
- \Adyen\Payment\Helper\Data $adyenHelper,
- \Magento\Sales\Model\OrderFactory $orderFactory,
- \Magento\Sales\Model\Order\Status\HistoryFactory $orderHistoryFactory,
- \Magento\Checkout\Model\Session $session,
- \Adyen\Payment\Logger\AdyenLogger $adyenLogger
- ) {
- $this->_adyenHelper = $adyenHelper;
- $this->_orderFactory = $orderFactory;
- $this->_orderHistoryFactory = $orderHistoryFactory;
- $this->_session = $session;
- $this->_adyenLogger = $adyenLogger;
- parent::__construct($context);
- }
-
- /**
- * Return result
- */
- public function execute()
- {
- $response = $this->getRequest()->getParams();
- $this->_adyenLogger->addAdyenResult(print_r($response, true));
-
- $result = $this->_validateResponse($response);
-
- if ($result) {
- $session = $this->_session;
- $session->getQuote()->setIsActive(false)->save();
- $this->_redirect('checkout/onepage/success', ['_query' => ['utm_nooverride' => '1']]);
- } else {
- $this->_cancel($response);
- $this->_redirect($this->_adyenHelper->getAdyenAbstractConfigData('return_path'));
- }
- }
-
- /**
- * @param $response
- * @return bool
- */
- private function _validateResponse($response)
- {
- $result = false;
-
- if ($response != null && $response['result'] != "" && $this->_validateChecksum($response)) {
- $incrementId = $response['merchantReference'];
- $responseResult = $response['result'];
-
- if ($incrementId) {
- $order = $this->_getOrder($incrementId);
- if ($order->getId()) {
- $comment = __(
- '%1
Result: %2
paymentMethod: %3',
- 'Adyen App Result URL Notification:',
- $responseResult,
- 'POS'
- );
-
- if ($responseResult == 'APPROVED') {
- $this->_adyenLogger->addAdyenResult('Result is approved');
-
- $history = $this->_orderHistoryFactory->create()
- //->setStatus($status)
- ->setComment($comment)
- ->setEntityName('order')
- ->setOrder($order);
- $history->save();
-
- // needed becuase then we need to save $order objects
- $order->setAdyenResulturlEventCode("POS_APPROVED");
-
- // save order
- $order->save();
-
- return true;
- } else {
- $this->_adyenLogger->addAdyenResult('Result is:' . $responseResult);
-
- $history = $this->_orderHistoryFactory->create()
- //->setStatus($status)
- ->setComment($comment)
- ->setEntityName('order')
- ->setOrder($order);
- $history->save();
-
- // cancel the order
- if ($order->canCancel()) {
- $order->cancel()->save();
- $this->_adyenLogger->addAdyenResult('Order is cancelled');
- } else {
- $this->_adyenLogger->addAdyenResult('Order can not be cancelled');
- }
- }
- } else {
- $this->_adyenLogger->addAdyenResult('Order does not exists with increment_id: ' . $incrementId);
- }
- } else {
- $this->_adyenLogger->addAdyenResult('Empty merchantReference');
- }
- } else {
- $this->_adyenLogger->addAdyenResult('actionName or checksum failed or response is empty');
- }
- return $result;
- }
-
- /**
- * Validate checksum from result parameters
- *
- * @param $response
- * @return bool
- */
- protected function _validateChecksum($response)
- {
- $checksum = $response['cs'];
- $result = $response['result'];
- $amount = $response['originalCustomAmount'];
- $currency = $response['originalCustomCurrency'];
- $sessionId = $response['sessionId'];
-
- // for android sessionis is with low i
- if ($sessionId == "") {
- $sessionId = $response['sessionid'];
- }
-
- // calculate amount checksum
- $amountChecksum = 0;
-
- $amountLength = strlen($amount);
- for ($i = 0; $i < $amountLength; $i++) {
- // ASCII value use ord
- $checksumCalc = ord($amount[$i]) - 48;
- $amountChecksum += $checksumCalc;
- }
-
- $currencyChecksum = 0;
- $currencyLength = strlen($currency);
- for ($i = 0; $i < $currencyLength; $i++) {
- $checksumCalc = ord($currency[$i]) - 64;
- $currencyChecksum += $checksumCalc;
- }
-
- $resultChecksum = 0;
- $resultLength = strlen($result);
- for ($i = 0; $i < $resultLength; $i++) {
- $checksumCalc = ord($result[$i]) - 64;
- $resultChecksum += $checksumCalc;
- }
-
- $sessionIdChecksum = 0;
- $sessionIdLength = strlen($sessionId);
- for ($i = 0; $i < $sessionIdLength; $i++) {
- $checksumCalc = $this->_getAscii2Int($sessionId[$i]);
- $sessionIdChecksum += $checksumCalc;
- }
-
- $totalResultChecksum = (($amountChecksum + $currencyChecksum + $resultChecksum) * $sessionIdChecksum) % 100;
-
- // check if request is valid
- if ($totalResultChecksum == $checksum) {
- $this->_adyenLogger->addAdyenResult('Checksum is valid');
- return true;
- }
- $this->_adyenLogger->addAdyenResult('Checksum is invalid!');
- return false;
- }
-
- /**
- * @param $ascii
- * @return int
- */
- protected function _getAscii2Int($ascii)
- {
- if (is_numeric($ascii)) {
- $int = ord($ascii) - 48;
- } else {
- $int = ord($ascii) - 64;
- }
- return $int;
- }
-
- /**
- * @param $incrementId
- * @return \Magento\Sales\Model\Order
- */
- protected function _getOrder($incrementId)
- {
- if (!$this->_order) {
- $this->_order = $this->_orderFactory->create()->loadByIncrementId($incrementId);
- }
- return $this->_order;
- }
-
- /**
- * @param $response
- */
- protected function _cancel($response)
- {
- $session = $this->_session;
-
- // restore the quote
- $session->restoreQuote();
-
- $order = $this->_order;
-
- if ($order) {
- $this->_adyenHelper->cancelOrder($order);
-
- if (isset($response['authResult']) &&
- $response['authResult'] == \Adyen\Payment\Model\Notification::CANCELLED) {
- $this->messageManager->addError(__('You have cancelled the order. Please try again'));
- } else {
- $this->messageManager->addError(__('Your payment failed, Please try again later'));
- }
- }
- }
-}
diff --git a/Cron/ServerIpAddress.php b/Cron/ServerIpAddress.php
new file mode 100644
index 000000000..de545b10a
--- /dev/null
+++ b/Cron/ServerIpAddress.php
@@ -0,0 +1,65 @@
+
+ */
+
+namespace Adyen\Payment\Cron;
+
+use Adyen\Payment\Helper\IpAddress;
+use Adyen\Payment\Logger\AdyenLogger;
+
+class ServerIpAddress
+{
+
+ /**
+ * @var IpAddress $ipAddressHelper
+ */
+ protected $ipAddressHelper;
+
+ /**
+ * @var AdyenLogger $adyenLogger
+ */
+ protected $adyenLogger;
+
+ /**
+ * ServerIpAddress constructor.
+ * @param IpAddress $ipAddressHelper
+ * @param AdyenLogger $adyenLogger
+ */
+ public function __construct(
+ IpAddress $ipAddressHelper,
+ AdyenLogger $adyenLogger
+ ) {
+ $this->ipAddressHelper = $ipAddressHelper;
+ $this->adyenLogger = $adyenLogger;
+ }
+
+ public function execute()
+ {
+ //Check if there are already verified IP addresses in cache and refresh when empty
+ if (empty($this->ipAddressHelper->getIpAddressesFromCache())) {
+ $this->adyenLogger->addAdyenNotificationCronjob(
+ 'There are no verified Adyen IP addresses in cache. Updating IP records.'
+ );
+ $this->ipAddressHelper->updateCachedIpAddresses();
+ }
+ }
+}
diff --git a/Helper/Config.php b/Helper/Config.php
index 38a71b55d..d57a6d2fd 100644
--- a/Helper/Config.php
+++ b/Helper/Config.php
@@ -24,27 +24,47 @@
namespace Adyen\Payment\Helper;
use Magento\Framework\App\Config\ScopeConfigInterface;
+use Magento\Framework\Encryption\EncryptorInterface;
class Config
{
const XML_PAYMENT_PREFIX = "payment";
const XML_ADYEN_ABSTRACT_PREFIX = "adyen_abstract";
const XML_NOTIFICATIONS_CAN_CANCEL_FIELD = "notifications_can_cancel";
+ const XML_NOTIFICATIONS_IP_HMAC_CHECK = "notifications_ip_hmac_check";
+ const XML_NOTIFICATIONS_HMAC_KEY_LIVE = "notification_hmac_key_live";
+ const XML_NOTIFICATIONS_HMAC_KEY_TEST = "notification_hmac_key_test";
/**
* @var Magento\Framework\App\Config\ScopeConfigInterface
*/
protected $scopeConfig;
+ /**
+ * @var EncryptorInterface
+ */
+ private $encryptor;
+
+ /**
+ * @var \Adyen\Payment\Helper\Data
+ */
+ private $adyenHelper;
+
/**
* Config constructor.
*
* @param Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
+ * @param EncryptorInterface $encryptor
+ * @param \Adyen\Payment\Helper\Data $adyenHelper
*/
public function __construct(
- ScopeConfigInterface $scopeConfig
+ ScopeConfigInterface $scopeConfig,
+ EncryptorInterface $encryptor,
+ \Adyen\Payment\Helper\Data $adyenHelper
) {
$this->scopeConfig = $scopeConfig;
+ $this->encryptor = $encryptor;
+ $this->adyenHelper = $adyenHelper;
}
/**
@@ -63,6 +83,48 @@ public function getNotificationsCanCancel($storeId = null)
);
}
+ /**
+ * Retrieve flag for notifications_ip_hmac_check
+ *
+ * @param int $storeId
+ * @return bool
+ */
+ public function getNotificationsIpHmacCheck($storeId = null)
+ {
+ return (bool)$this->getConfigData(
+ self::XML_NOTIFICATIONS_IP_HMAC_CHECK,
+ self::XML_ADYEN_ABSTRACT_PREFIX,
+ $storeId,
+ true
+ );
+ }
+
+ /**
+ * Retrieve key for notifications_hmac_key
+ *
+ * @param int $storeId
+ * @return string
+ */
+ public function getNotificationsHmacKey($storeId = null)
+ {
+ if ($this->adyenHelper->isDemoMode($storeId)) {
+ $key = $this->getConfigData(
+ self::XML_NOTIFICATIONS_HMAC_KEY_TEST,
+ self::XML_ADYEN_ABSTRACT_PREFIX,
+ $storeId,
+ false
+ );
+ } else {
+ $key = $this->getConfigData(
+ self::XML_NOTIFICATIONS_HMAC_KEY_LIVE,
+ self::XML_ADYEN_ABSTRACT_PREFIX,
+ $storeId,
+ false
+ );
+ }
+ return $this->encryptor->decrypt(trim($key));
+ }
+
/**
* Retrieve information from payment configuration
*
diff --git a/Helper/IpAddress.php b/Helper/IpAddress.php
new file mode 100644
index 000000000..0ec8500c3
--- /dev/null
+++ b/Helper/IpAddress.php
@@ -0,0 +1,148 @@
+
+ */
+
+namespace Adyen\Payment\Helper;
+
+use Adyen\Util\IpAddress as IpAddressUtil;
+use Adyen\Payment\Logger\AdyenLogger;
+use Magento\Framework\App\CacheInterface;
+use Magento\Framework\Serialize\SerializerInterface;
+
+/**
+ * Class IpAddress
+ * @package Adyen\Payment\Helper
+ */
+class IpAddress
+{
+
+ const IP_ADDRESS_CACHE_ID = "Adyen_ip_address";
+ const IP_ADDRESS_CACHE_LIFETIME = 86400;
+
+ /**
+ * @var IpAddressUtil
+ */
+ private $ipAddressUtil;
+
+ /**
+ * @var CacheInterface
+ */
+ private $cache;
+
+ /**
+ * @var SerializerInterface
+ */
+ private $serializer;
+
+ /**
+ * @var AdyenLogger $adyenLogger
+ */
+ protected $adyenLogger;
+
+ /**
+ * IpAddress constructor.
+ *
+ * @param IpAddressUtil $ipAddressUtil
+ * @param CacheInterface $cache
+ * @param SerializerInterface $serializer
+ * @param AdyenLogger $adyenLogger
+ */
+ public function __construct(
+ IpAddressUtil $ipAddressUtil,
+ CacheInterface $cache,
+ SerializerInterface $serializer,
+ AdyenLogger $adyenLogger
+ ) {
+ $this->ipAddressUtil = $ipAddressUtil;
+ $this->cache = $cache;
+ $this->serializer = $serializer;
+ $this->adyenLogger = $adyenLogger;
+ }
+
+ /**
+ * Checks if the provided array of IPs addresses has been validated
+ *
+ * @param string[] $ipAddresses
+ * @return bool
+ */
+ public function isIpAddressValid($ipAddresses)
+ {
+ if (empty($ipAddresses)) {
+ return false;
+ }
+
+ $cachedIpsArray = $this->getIpAddressesFromCache();
+
+ if (empty($cachedIpsArray)) {
+ $this->adyenLogger->addAdyenDebug(
+ 'There are no verified Adyen IP addresses in cache. Updating IP records.'
+ );
+ $this->updateCachedIpAddresses();
+ }
+
+ foreach ($ipAddresses as $ipAddress) {
+ //If the IP is already cached return true
+ if (in_array($ipAddress, $cachedIpsArray)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Updates cache key containing Adyen webhook IP addresses with newly resolved records
+ */
+ public function updateCachedIpAddresses()
+ {
+ $this->saveIpAddressesToCache($this->ipAddressUtil->getAdyenIpAddresses());
+ }
+
+ /**
+ * Saves array of IP addresses in cache key
+ *
+ * @param string[] $ipAddresses
+ */
+ public function saveIpAddressesToCache($ipAddresses)
+ {
+ $this->cache->save(
+ $this->serializer->serialize($ipAddresses),
+ self::IP_ADDRESS_CACHE_ID,
+ [],
+ self::IP_ADDRESS_CACHE_LIFETIME
+ );
+ }
+
+ /**
+ * Loads value of IP addresses cache key and returns it as array
+ *
+ * @return array|bool|float|int|string|null
+ */
+ public function getIpAddressesFromCache()
+ {
+ $serializedIpAddresses = $this->cache->load(self::IP_ADDRESS_CACHE_ID);
+ if (!empty($serializedIpAddresses)) {
+ return $this->serializer->unserialize($serializedIpAddresses);
+ }
+ return [];
+ }
+}
diff --git a/README.md b/README.md
index 8553e0493..fab14c71f 100644
--- a/README.md
+++ b/README.md
@@ -37,18 +37,23 @@ For more information see our [installation section](https://docs.adyen.com/devel
## Setup Cron
-Make sure that your Magento cron is running every minute. We are using a cronjob to process the notifications, our webhook service. The cronjob will be executed every minute. It only executes the notifications that have been received at least 2 minutes ago. This is to ensure that Magento has created the order, and all save after events are executed. A handy tool to get insight into your cronjobs is AOE scheduler. You can download this tool through Magento Connect or GitHub.
-If you need to setup your cronjob in Magento this is described here
+Make sure that your Magento cron is running every minute. We are using a cronjob to process the notifications (our webhook service) and to update Adyen servers' IP addresses. The cronjobs will be executed every minute.
-We have defined this:
```