Skip to content

Commit f71ec2b

Browse files
authored
Merge pull request #143 from academe/issue131
Issue131 fix - meant to merge this in a while ago, but been really busy with other things.
2 parents af116fd + 0454e93 commit f71ec2b

File tree

3 files changed

+89
-10
lines changed

3 files changed

+89
-10
lines changed

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Table of Contents
3434
* [Server Notification Handler](#server-notification-handler)
3535
* [Sage Pay Form Methods](#sage-pay-form-methods)
3636
* [Form Authorize](#form-authorize)
37-
* [Form completeAuthorise](#form-completeauthorise)
37+
* [Form completeAuthorize](#form-completeauthorize)
3838
* [Form Purchase](#form-purchase)
3939
* [Sage Pay Shared Methods (Direct and Server)](#sage-pay-shared-methods-direct-and-server)
4040
* [Repeat Authorize/Purchase](#repeat-authorizepurchase)
@@ -682,26 +682,38 @@ $response = $gateway->authorize([
682682
```
683683

684684
The `$response` will be a `POST` redirect, which will take the user to the gateway.
685-
At the gateway, the user will authenticate or authorise their credit card,
685+
At the gateway, the user will authenticate or authorize their credit card,
686686
perform any 3D Secure actions that may be requested, then will return to the
687687
merchant site.
688688

689-
### Form completeAuthorise
689+
Like `Server` and `Direct`, you can use either the `DEFERRED` or the `AUTHENTICATE`
690+
method to reserve the amount.
691+
692+
### Form completeAuthorize
690693

691694
To get the result details, the transaction is "completed" on the
692695
user's return. This will be at your `returnUrl` endpoint:
693696

694697
```php
695698
// The result will be read and decrypted from the return URL (or failure URL)
696-
// query parameters:
699+
// query parameters.
700+
// You MUST provide the original expected transactionId, which is validated
701+
// against the transactionId provided in the server request.
702+
// This prevents different payments getting mixed up.
697703

698-
$result = $gateway->completeAuthorize()->send();
704+
$completeRequest = $gateway->completeAuthorize(['transactionId' => $originalTransactionId]);
705+
$result = $completeRequest->send();
699706

700707
$result->isSuccessful();
701708
$result->getTransactionReference();
702709
// etc.
703710
```
704711

712+
Note that if `send()` throws an exception here due to a `transactionId` mismatch,
713+
you can still access the decryoted data that was brought back with the user as
714+
`$completeRequest->getData()`.
715+
You will need to log this for later analysis.
716+
705717
If you already have the encrypted response string, then it can be passed in.
706718
However, you would normally leave it for the driver to read it for you from
707719
the current server request, so the following would not normally be necessary:
@@ -728,9 +740,6 @@ In a future release, the `completeAuthorize()` method will expect the
728740
`transactionId` to be supplied and it must match before it will
729741
return a success status.
730742

731-
Like `Server` and `Direct`, you can use either the `DEFERRED` or the `AUTHENTICATE`
732-
method to reserve the amount.
733-
734743
### Form Purchase
735744

736745
This is the same as `authorize()`, but the `purchase()` request is used instead,

src/Message/Form/CompleteAuthorizeRequest.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Omnipay\SagePay\Message\AbstractRequest;
1010
use Omnipay\SagePay\Message\Response as GenericResponse;
1111
use Omnipay\Common\Exception\InvalidResponseException;
12+
use Omnipay\Common\Exception\InvalidRequestException;
1213

1314
class CompleteAuthorizeRequest extends AbstractRequest
1415
{
@@ -72,14 +73,37 @@ public function getData()
7273

7374
/**
7475
* Nothing to send to gateway - we have the result data in the server request.
76+
*
77+
* @throws InvalidResponseException
78+
* @throws InvalidResponseException
7579
*/
7680
public function sendData($data)
7781
{
82+
$this->response = new GenericResponse($this, $data);
83+
84+
// Issue #131: confirm the response is for the transaction ID we are
85+
// expecting, and not replayed from another transaction.
86+
87+
$originalTransactionId = $this->getTransactionId();
88+
$returnedTransactionId = $this->response->getTransactionId();
89+
90+
if (empty($originalTransactionId)) {
91+
throw new InvalidRequestException('Missing expected transactionId parameter');
92+
}
93+
94+
if ($originalTransactionId !== $returnedTransactionId) {
95+
throw new InvalidResponseException(sprintf(
96+
'Unexpected transactionId; expected "%s" received "%s"',
97+
$originalTransactionId,
98+
$returnedTransactionId
99+
));
100+
}
101+
78102
// The Response in the current namespace conflicts with
79103
// the Response in the namespace one level down, but only
80104
// for PHP 5.6. This alias works around it.
81105

82-
return $this->response = new GenericResponse($this, $data);
106+
return $this->response;
83107
}
84108

85109
/**

tests/FormGatewayTest.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public function setUp()
3434

3535
$this->completePurchaseOptions = [
3636
'encryptionKey' => '2f52208a25a1facf',
37+
'transactionId' => 'phpne-demo-53922585',
3738
];
3839
}
3940

@@ -235,7 +236,12 @@ public function testCompletePurchaseSuccess()
235236

236237
$this->getHttpRequest()->initialize(['crypt' => '@5548276239c33e937e4d9d847d0a01f4c05f1b71dd5cd32568b6985a6d6834aca672315cf3eec01bb20d34cd1ccd7bdd69a9cd89047f7f875103b46efd8f7b97847eea6b6bab5eb8b61da9130a75fffa1c9152b7d39f77e534ea870281b8e280ea1fdbd49a8f5a7c67d1f512fe7a030e81ae6bd2beed762ad074edcd5d7eb4456a6797911ec78e4d16e7d3ac96b919052a764b7ee4940fd6976346608ad8fed1eb6b0b14d84d802c594b3fd94378a26837df66b328f01cfd144f2e7bc166370bf7a833862173412d2798e8739ee7ef9b0568afab0fc69f66af19864480bf3e74fe2fd2043ec90396e40ab62dc9c1f32dee0e309af7561d2286380ebb497105bde2860d401ccfb4cfcd7047ad32e9408d37f5d0fe9a67bd964d5b138b2546a7d54647467c59384eaa20728cf240c460e36db68afdcf0291135f9d5ff58563f14856fd28534a5478ba2579234b247d0d5862c5742495a2ae18c5ca0d6461d641c5215b07e690280fa3eaf5d392e1d6e2791b181a500964d4bc6c76310e47468ae72edddc3c04d83363514c908624747118']);
237238

238-
$response = $this->gateway->completePurchase($this->completePurchaseOptions)->send();
239+
$options = $this->completePurchaseOptions;
240+
241+
// Switch to the transaction ID actually encrypted in the server request.
242+
$options['transactionId'] = 'phpne-demo-56260425';
243+
244+
$response = $this->gateway->completePurchase($options)->send();
239245

240246
$this->assertTrue($response->isSuccessful());
241247
$this->assertFalse($response->isRedirect());
@@ -266,4 +272,44 @@ public function testCompletePurchaseSuccess()
266272
$response->getData()
267273
);
268274
}
275+
276+
/**
277+
* The wrong transaction ID is supplied with the server request.
278+
*
279+
* @expectedException Omnipay\Common\Exception\InvalidResponseException
280+
*/
281+
public function testCompletePurchaseReplayAttack()
282+
{
283+
//$this->expectException(Complicated::class);
284+
285+
// Set the "crypt" query parameter.
286+
287+
$this->getHttpRequest()->initialize(['crypt' => '@5548276239c33e937e4d9d847d0a01f4c05f1b71dd5cd32568b6985a6d6834aca672315cf3eec01bb20d34cd1ccd7bdd69a9cd89047f7f875103b46efd8f7b97847eea6b6bab5eb8b61da9130a75fffa1c9152b7d39f77e534ea870281b8e280ea1fdbd49a8f5a7c67d1f512fe7a030e81ae6bd2beed762ad074edcd5d7eb4456a6797911ec78e4d16e7d3ac96b919052a764b7ee4940fd6976346608ad8fed1eb6b0b14d84d802c594b3fd94378a26837df66b328f01cfd144f2e7bc166370bf7a833862173412d2798e8739ee7ef9b0568afab0fc69f66af19864480bf3e74fe2fd2043ec90396e40ab62dc9c1f32dee0e309af7561d2286380ebb497105bde2860d401ccfb4cfcd7047ad32e9408d37f5d0fe9a67bd964d5b138b2546a7d54647467c59384eaa20728cf240c460e36db68afdcf0291135f9d5ff58563f14856fd28534a5478ba2579234b247d0d5862c5742495a2ae18c5ca0d6461d641c5215b07e690280fa3eaf5d392e1d6e2791b181a500964d4bc6c76310e47468ae72edddc3c04d83363514c908624747118']);
288+
289+
// These options contain a different transactionId from the once expected.
290+
291+
$options = $this->completePurchaseOptions;
292+
293+
$response = $this->gateway->completePurchase($options)->send();
294+
}
295+
296+
/**
297+
* The missing expected transaction ID supplied by the app.
298+
*
299+
* @expectedException Omnipay\Common\Exception\InvalidRequestException
300+
*/
301+
public function testCompletePurchaseMissingExpectedParam()
302+
{
303+
//$this->expectException(Complicated::class);
304+
305+
// Set the "crypt" query parameter.
306+
307+
$this->getHttpRequest()->initialize(['crypt' => '@5548276239c33e937e4d9d847d0a01f4c05f1b71dd5cd32568b6985a6d6834aca672315cf3eec01bb20d34cd1ccd7bdd69a9cd89047f7f875103b46efd8f7b97847eea6b6bab5eb8b61da9130a75fffa1c9152b7d39f77e534ea870281b8e280ea1fdbd49a8f5a7c67d1f512fe7a030e81ae6bd2beed762ad074edcd5d7eb4456a6797911ec78e4d16e7d3ac96b919052a764b7ee4940fd6976346608ad8fed1eb6b0b14d84d802c594b3fd94378a26837df66b328f01cfd144f2e7bc166370bf7a833862173412d2798e8739ee7ef9b0568afab0fc69f66af19864480bf3e74fe2fd2043ec90396e40ab62dc9c1f32dee0e309af7561d2286380ebb497105bde2860d401ccfb4cfcd7047ad32e9408d37f5d0fe9a67bd964d5b138b2546a7d54647467c59384eaa20728cf240c460e36db68afdcf0291135f9d5ff58563f14856fd28534a5478ba2579234b247d0d5862c5742495a2ae18c5ca0d6461d641c5215b07e690280fa3eaf5d392e1d6e2791b181a500964d4bc6c76310e47468ae72edddc3c04d83363514c908624747118']);
308+
309+
$options = $this->completePurchaseOptions;
310+
311+
unset($options['transactionId']);
312+
313+
$response = $this->gateway->completePurchase($options)->send();
314+
}
269315
}

0 commit comments

Comments
 (0)