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

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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()
{
$this->paymentValidator = new PaymentValidator();
Benjamin-Freoua-Alma marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @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");
Benjamin-Freoua-Alma marked this conversation as resolved.
Show resolved Hide resolved
}
}


}
183 changes: 113 additions & 70 deletions src/Payum/Action/NotifyAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Alma\SyliusPaymentPlugin\Bridge\AlmaBridge;
use Alma\SyliusPaymentPlugin\Bridge\AlmaBridgeInterface;
use Alma\SyliusPaymentPlugin\Helper\SecurityHelper;
use Alma\SyliusPaymentPlugin\Payum\Request\ValidatePayment;
use ArrayAccess;
use Payum\Core\Action\ActionInterface;
Expand All @@ -19,78 +20,120 @@
use Payum\Core\Request\GetHttpRequest;
use Payum\Core\Request\Notify;
use Sylius\Component\Core\Model\PaymentInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;

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

public function __construct()
{
$this->apiClass = AlmaBridge::class;
}

/**
* @param Notify $request
*/
public function execute($request): void
{
RequestNotSupportedException::assertSupports($this, $request);

/** @var PaymentInterface $payment */
$payment = $request->getFirstModel();

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

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

// 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];
$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"]
);
}
}

// $details is the request's model here, but we used a copy above passed down the ValidatePaymentAction through
// the $payment object, which is the model of our own request.
// Since Payum will use whatever is in the original details model to overwrite the payment's details data, we
// need to make sure we copy everything that was set on the payment itself back to the NotifyRequest model.
$details->replace($payment->getDetails());

// Down here means the callback has been correctly handled, regardless of the final payment state
throw new HttpResponse(
json_encode(["success" => true, "state" => $payment->getDetails()[AlmaBridgeInterface::DETAILS_KEY_IS_VALID]]),
Response::HTTP_OK,
["content-type" => "application/json"]
);
}

public function supports($request): bool
{
return $request instanceof Notify
&& $request->getModel() instanceof ArrayAccess
&& $request->getFirstModel() instanceof PaymentInterface;
}
use GatewayAwareTrait;
use ApiAwareTrait;

/**
* @var AlmaBridgeInterface
*/
protected $api;

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

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

public function __construct(RequestStack $requestStack)
{
$this->apiClass = AlmaBridge::class;
$this->requestStack = $requestStack;
$this->securityHelper = new SecurityHelper();
}

/**
* @param Notify $request
*/
public function execute($request): void
{
RequestNotSupportedException::assertSupports($this, $request);
$httpRequest = new GetHttpRequest();
$this->gateway->execute($httpRequest);

/** @var string $signature */
$signature = ArrayObject::ensureArrayObject($httpRequest->headers)->get('x-alma-signature')[0];
$query = ArrayObject::ensureArrayObject($httpRequest->query);

/** @var string $payment_id */
$payment_id = $query->get(AlmaBridgeInterface::QUERY_PARAM_PID);

/** @var PaymentInterface $payment */
$payment = $request->getFirstModel();

try {
$this->securityHelper->isHmacValidated(
Francois-Gomis marked this conversation as resolved.
Show resolved Hide resolved
$payment_id,
$this->api->getGatewayConfig()->getActiveApiKey(),
$signature
);
} 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 notification does not include a payment ID, just return */
if (!$query->offsetExists(AlmaBridgeInterface::QUERY_PARAM_PID)) {
return;
}

// 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];
$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"]
);
}
}

// $details is the request's model here, but we used a copy above passed down the ValidatePaymentAction through
// the $payment object, which is the model of our own request.
// Since Payum will use whatever is in the original details model to overwrite the payment's details data, we
// need to make sure we copy everything that was set on the payment itself back to the NotifyRequest model.
$details->replace($payment->getDetails());

// Down here means the callback has been correctly handled, regardless of the final payment state
throw new HttpResponse(
json_encode(["success" => true, "state" => $payment->getDetails()[AlmaBridgeInterface::DETAILS_KEY_IS_VALID]]),
Response::HTTP_OK,
["content-type" => "application/json"]
);
}

public function supports($request): bool
{
return $request instanceof Notify
&& $request->getModel() instanceof ArrayAccess
&& $request->getFirstModel() instanceof PaymentInterface;
}
}
2 changes: 2 additions & 0 deletions src/Resources/config/services/actions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ services:

Alma\SyliusPaymentPlugin\Payum\Action\NotifyAction:
public: true
arguments:
$requestStack: '@request_stack'
tags:
- name: payum.action
factory: alma_payments
Expand Down