Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hmac verification on ipn #18

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"license": "MIT",
"require": {
"php": "~7.3 || ~7.4 || ~8.0 || ~8.1 || ~8.2",
"alma/alma-php-client": ">=1.11.2",
"alma/alma-php-client": ">=2.2.0",
"sylius/sylius": ">=v1.9.0",
"ext-json": "*"
},
Expand Down
8 changes: 8 additions & 0 deletions src/Exception/AlmaException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Alma\SyliusPaymentPlugin\Exception;

class AlmaException extends \Exception
{

}
8 changes: 8 additions & 0 deletions src/Exception/SecurityException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Alma\SyliusPaymentPlugin\Exception;

class SecurityException extends AlmaException
{

}
36 changes: 36 additions & 0 deletions src/Helper/SecurityHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Alma\SyliusPaymentPlugin\Helper;

use Alma\API\Lib\PaymentValidator;
use Alma\SyliusPaymentPlugin\Exception\SecurityException;

class SecurityHelper
{
/**
* @var PaymentValidator
*/
protected $paymentValidator;

public function __construct(PaymentValidator $paymentValidator)
{
$this->paymentValidator = $paymentValidator;
}

/**
* @param string $paymentId
* @param string $key
* @param string $signature
* @throws SecurityException
*/
public function isHmacValidated(string $paymentId, string $key, string $signature): void
{
if (!$this->paymentValidator->isHmacValidated($paymentId, $key, $signature)) {
throw new SecurityException("HMAC validation failed for payment $paymentId - signature: $signature");
}
}


}
196 changes: 167 additions & 29 deletions src/Payum/Action/NotifyAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

namespace Alma\SyliusPaymentPlugin\Payum\Action;

use Alma\API\Lib\PaymentValidator;
use Alma\SyliusPaymentPlugin\Bridge\AlmaBridge;
use Alma\SyliusPaymentPlugin\Bridge\AlmaBridgeInterface;
use Alma\SyliusPaymentPlugin\Exception\SecurityException;
use Alma\SyliusPaymentPlugin\Helper\SecurityHelper;
use Alma\SyliusPaymentPlugin\Payum\Request\ValidatePayment;
use ArrayAccess;
use Payum\Core\Action\ActionInterface;
Expand All @@ -16,19 +19,44 @@
use Payum\Core\GatewayAwareInterface;
use Payum\Core\GatewayAwareTrait;
use Payum\Core\Reply\HttpResponse;
use Payum\Core\Request\GetHttpRequest;
use Payum\Core\Request\Notify;
use Sylius\Component\Core\Model\PaymentInterface;
use Sylius\Component\Payment\Model\PaymentInterface as PaymentInterfaceModel;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;

class NotifyAction implements ActionInterface, ApiAwareInterface, GatewayAwareInterface
{
use GatewayAwareTrait;
use ApiAwareTrait;

public function __construct()
/**
* @var AlmaBridgeInterface
*/
protected $api;

/**
* @var RequestStack
*/
protected $requestStack;

/**
* @var SecurityHelper
*/
protected $securityHelper;


/**
* NotifyAction constructor.
* @param RequestStack $requestStack
* @param SecurityHelper $securityHelper
*/
public function __construct(RequestStack $requestStack, SecurityHelper $securityHelper)
{
$this->apiClass = AlmaBridge::class;
$this->requestStack = $requestStack;
$this->securityHelper = $securityHelper;
}

/**
Expand All @@ -37,40 +65,24 @@ public function __construct()
public function execute($request): void
{
RequestNotSupportedException::assertSupports($this, $request);
$httpRequest = $this->getCurrentRequest();

/** @var PaymentInterface $payment */
$payment = $request->getFirstModel();
$payment_id = $this->getQueryPaymentId($httpRequest);
$signature = $this->getHeaderSignature($httpRequest);

$httpRequest = new GetHttpRequest();
$this->gateway->execute($httpRequest);
$query = ArrayObject::ensureArrayObject($httpRequest->query);
$this->checkSignature($payment_id, $signature);

/* if notification does not include a payment ID, just return */
if (!$query->offsetExists(AlmaBridgeInterface::QUERY_PARAM_PID)) {
return;
}
/** @var PaymentInterface $payment */
$payment = $request->getFirstModel();

// Make sure the payment's details include the Alma payment ID
$details = ArrayObject::ensureArrayObject($request->getModel());
$details[AlmaBridgeInterface::DETAILS_KEY_PAYMENT_ID] = (string) $query[AlmaBridgeInterface::QUERY_PARAM_PID];
$details[AlmaBridgeInterface::DETAILS_KEY_PAYMENT_ID] = $payment_id;
$payment->setDetails($details->getArrayCopy());

// If payment hasn't been validated yet, validate its status against Alma's payment state
if (in_array($payment->getState(), [PaymentInterface::STATE_NEW, PaymentInterface::STATE_PROCESSING], true)) {
try {
$this->gateway->execute(new ValidatePayment($payment));
} catch (\Exception $e) {
$error = [
"error" => true,
"message" => $e->getMessage()
];

throw new HttpResponse(
json_encode($error),
Response::HTTP_INTERNAL_SERVER_ERROR,
["content-type" => "application/json"]
);
}
if (in_array($payment->getState(), [PaymentInterfaceModel::STATE_NEW, PaymentInterfaceModel::STATE_PROCESSING], true)) {
$this->validatePayment($payment);
}

// $details is the request's model here, but we used a copy above passed down the ValidatePaymentAction through
Expand All @@ -80,13 +92,139 @@ public function execute($request): void
$details->replace($payment->getDetails());

// Down here means the callback has been correctly handled, regardless of the final payment state
$this->returnHttpResponse(["success" => true, "state" => $payment->getDetails()[AlmaBridgeInterface::DETAILS_KEY_IS_VALID]]);
}

/**
* Get signature from header or return error
*
* @param Request $httpRequest
* @return string
* @throws HttpResponse
*/
private function getHeaderSignature(Request $httpRequest): string
{
$signature = $httpRequest->headers->get(strtolower(PaymentValidator::HEADER_SIGNATURE_KEY));
if (!$signature) {
$error = [
"error" => true,
"message" => 'No signature provided in IPN callback'
];
$this->returnHttpResponse($error, Response::HTTP_FORBIDDEN);
}

return $signature;
}

/**
* Get payment ID from query or return error
*
* @param Request $httpRequest
* @return string
* @throws HttpResponse
*/
private function getQueryPaymentId(Request $httpRequest): string
{
$payment_id = $httpRequest->query->get(AlmaBridgeInterface::QUERY_PARAM_PID);
if (!$payment_id) {
$error = [
"error" => true,
"message" => 'No payment ID provided in IPN callback'
];
$this->returnHttpResponse($error, Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $payment_id;
}

/**
* Get current request in requestStack or return error
*
* @return Request
* @throws HttpResponse
*/
private function getCurrentRequest(): Request
{
$httpRequest = $this->requestStack->getCurrentRequest();
if (!$httpRequest) {
$error = [
"error" => true,
"message" => 'No request found'
];
$this->returnHttpResponse([$error], Response::HTTP_INTERNAL_SERVER_ERROR);
}
return $httpRequest;
}

/**
* Check signature with php client library return forbidden if signature is not valid
*
* @param string $payment_id
* @param string $signature
* @return void
* @throws HttpResponse
*/
private function checkSignature(string $payment_id, string $signature): void
{

try {
$this->securityHelper->isHmacValidated(
$payment_id,
$this->api->getGatewayConfig()->getActiveApiKey(),
$signature
);
} catch (SecurityException $e) {
$error = [
"error" => true,
"message" => $e->getMessage()
];
$this->returnHttpResponse($error, Response::HTTP_FORBIDDEN);
}
}

/**
* Validate payment or return error
*
* @param PaymentInterface $payment
* @return void
* @throws HttpResponse
*/
private function validatePayment(PaymentInterface $payment): void
{
try {
$this->gateway->execute(new ValidatePayment($payment));
} catch (\Exception $e) {
$error = [
"error" => true,
"message" => $e->getMessage()
];
$this->returnHttpResponse($error, Response::HTTP_INTERNAL_SERVER_ERROR);
}
}

/**
* HTTP Response factory
*
* @param array $message
* @param int $code
* @return void
* @throws HttpResponse
*/
private function returnHttpResponse(array $message, int $code = Response::HTTP_OK): void
{
throw new HttpResponse(
json_encode(["success" => true, "state" => $payment->getDetails()[AlmaBridgeInterface::DETAILS_KEY_IS_VALID]]),
Response::HTTP_OK,
json_encode($message),
$code,
["content-type" => "application/json"]
);
}


/**
* Check if the request is supported
*
* @param $request
* @return bool
*/
public function supports($request): bool
{
return $request instanceof Notify
Expand Down
28 changes: 27 additions & 1 deletion src/Payum/Action/ValidatePaymentAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ public function execute($request): void
$paymentData
);

// Convert Alma's orders to an array to save on Sylius' Payment details
$paymentData->orders = $this->convertOrderToArrayForDBSave($paymentData->orders);

// Save Alma's payment data on Sylius' Payment details
$details[AlmaBridgeInterface::DETAILS_KEY_PAYMENT_DATA] = $paymentData;

$payment->setDetails($details);
}

Expand All @@ -63,4 +65,28 @@ public function supports($request): bool
return $request instanceof ValidatePayment
&& $request->getModel() instanceof PaymentInterface;
}

/**
* Convert Alma's orders to an array to save on Sylius' Payment details
*
* @param array $orders
* @return array
*/
private function convertOrderToArrayForDBSave(array $orders): array
{
$arrayOrders = [];
foreach ($orders as $order) {
$arrayOrders[] = [
'comment' => $order->getComment(),
'created' => $order->getCreatedAt(),
'customer_url' => $order->getCustomerUrl(),
'data' => $order->getOrderData(),
'id' => $order->getExternalId(),
'merchant_reference' => $order->getMerchantReference(),
'merchant_url' => $order->getMerchantUrl(),
'payment' =>$order->getPaymentId()
];
}
return $arrayOrders;
}
}
3 changes: 3 additions & 0 deletions src/Resources/config/services/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ services:

Alma\SyliusPaymentPlugin\Payum\Action\NotifyAction:
public: true
arguments:
$requestStack: '@request_stack'
$securityHelper: '@alma.sylius_payment_plugin.helper.security'
tags:
- name: payum.action
factory: alma_payments
Expand Down
3 changes: 3 additions & 0 deletions src/Resources/config/services/api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ services:
arguments:
$logger: '@logger'
$paymentDataBuilder: '@alma_sylius_payment_plugin.payment_data_builder'

alma_api.payment_validator:
class: Alma\API\Lib\PaymentValidator

Alma\SyliusPaymentPlugin\Bridge\AlmaBridgeInterface: '@alma_sylius_payment_plugin.bridge'
7 changes: 6 additions & 1 deletion src/Resources/config/services/helpers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ services:
class: Alma\SyliusPaymentPlugin\Helper\EligibilityHelper
arguments:
$almaBridge: '@alma_sylius_payment_plugin.bridge'
$eligibilityDataBuilder: '@alma_sylius_payment_plugin.eligibility_data_builder'
$eligibilityDataBuilder: '@alma_sylius_payment_plugin.eligibility_data_builder'

alma.sylius_payment_plugin.helper.security:
class: Alma\SyliusPaymentPlugin\Helper\SecurityHelper
arguments:
$paymentValidator: '@alma_api.payment_validator'