I'm integrating Amazon Pay php SDK from documentation, but getting this error.
Here's my php implementation code:
$amazonpay_config = array(
'public_key_id' => 'XXXXXXXX',
'private_key' => 'my_private_key_path',
'region' => 'US',
'sandbox' => true
);
$payload = array(
'webCheckoutDetails' => array(
'checkoutReviewReturnUrl' => 'https://www.example.com/review',
'checkoutResultReturnUrl' => 'https://www.example.com/result'
),
'storeId' => 'amzn1.application-oa2-client.XXXXXXXXX'
);
$headers = array('x-amz-pay-Idempotency-Key' => uniqid());
$requestResult = [
'error' => 1,
'msg' => 'Error. Can not create checkout session.',
'checkoutSession' => null,
'payloadSign' => null
];
$client = new Client($amazonpay_config);
$resultCheckOut = $client->createCheckoutSession($payload, $headers);
$resultSignPayload = $client->generateButtonSignature($payload);
if($resultCheckOut['status'] !== 201) {
return json_encode($requestResult, true);
}
else {
$requestResult = [
'error' => 0,
'msg' => null,
'checkoutSession' => json_decode($resultCheckOut['response']),
'payloadSign' => $resultSignPayload
];
return $requestResult;
}
Here's JS implementation code for generating Amazon Pay button.
amazon.Pay.renderButton('#amazon-pay-btn', {
// set checkout environment
merchantId: 'XXXXXXXX',
ledgerCurrency: 'USD',
sandbox: true,
checkoutLanguage: 'en_US',
productType: 'PayOnly',
placement: 'Cart',
buttonColor: 'Gold',
createCheckoutSessionConfig: {
payloadJSON: jsonResult['checkoutSession'],
signature: jsonResult['payloadSign'],
publicKeyId: 'XXXXXXXXXXX'
}
});
Couple of problems with the code, mainly that you aren't passing the payload and signature to the front-end correctly. For the payload, you're using jsonResult['checkoutSession'], while it should be jsonResult['payloadSign']. This doesn't contain the payload though but from the PHP code it's apparently the signature that you have put in there. The full code sample should more like this (not tested).
Back-end:
$headers = array('x-amz-pay-Idempotency-Key' => uniqid());
$requestResult = [
'error' => 1,
'msg' => 'Error. Can not create checkout session.',
'signature' => null,
'payload' => null
];
$client = new Client($amazonpay_config);
$resultCheckOut = $client->createCheckoutSession($payload, $headers);
$resultSignature = $client->generateButtonSignature($payload);
if($resultCheckOut['status'] !== 201) {
return json_encode($requestResult, true);
}
else {
$requestResult = [
'error' => 0,
'msg' => null,
'signature' => $resultSignature,
'payload' => $payload
];
return json_encode($requestResult);
}
Front-end:
amazon.Pay.renderButton('#amazon-pay-btn', {
// set checkout environment
merchantId: 'XXXXXXXX',
ledgerCurrency: 'USD',
sandbox: true,
checkoutLanguage: 'en_US',
productType: 'PayOnly',
placement: 'Cart',
buttonColor: 'Gold',
createCheckoutSessionConfig: {
payloadJSON: JSON.stringify(jsonResult['payload']),
signature: jsonResult['signature'],
publicKeyId: 'XXXXXXXXXXX'
}
});
I'm not sure how you're passing $requestResult back to the front-end, potentially there's some additional JSON encoding/decoding required to get the right string. To prevent a signature mismatch error, please make sure that the payload string used for the signature generation in the backend, and the payload string assigned to the 'payloadJSON' parameter match exactly (especially pay attention to whitespaces, escape characters, line breaks, etc.).
Two comments about this issue:
I have defined the payload as an string (that's the way current AmazonPay doc states - Link).
$payload = '{
"webCheckoutDetails": {
"checkoutReviewReturnUrl": "https://www.example.com/review",
"checkoutResultReturnUrl": "https://www.example.com/result"
},
"storeId": "amzn1.application-oa2-client.XXXXXXXXX"
}';
instead of array
$payload = array(
'webCheckoutDetails' => array(
'checkoutReviewReturnUrl' => 'https://www.example.com/review',
'checkoutResultReturnUrl' => 'https://www.example.com/result'
),
'storeId' => 'amzn1.application-oa2-client.XXXXXXXXX'
);
The signature was created, but when rendering the button and clicking on it I get the following error.
Error Message: Signature Dk4qznkoiTVqjcY8Yn1l0iLbsoIj2pEAHWVtgYrphLtFXR9BKhJJPD53It4qYOswS1T/STYMHRy5jtCHGqvLntDjuy0MrhkpoHTpYEtwdOqGHA2qk+QnSGV5LoYldQ/UkAxSG7m8s2iOr11q2sWxUjrk2M3fgzAIxDeZRjJYeAr97eGANYva3jtGDfM6cJdieInBM4dEWWxKqGIh6HxOrY5K/ga26494vAwZAGvXRhZG48FOVp/XCr0mbu6V5pkEOzRJSc+hN5WKAs/c49UsfKPx75Ce7QbaBCZZT1UiczfyYx/mBuZuysUlGmnXPhLOLTPw4+SIizH/pOQyClOQyw== does not match signedString AMZN-PAY-RSASSA-PSS dfff7a87b93cfa78685a233f2dd59e18ad0451b2e3a90af11e500fcc0ceee924 for merchant XXXXXXXX
I was some time till I realized that this was the reason of the error. Actually, while writing this, the new lines in the string were the reason. If string is only in one line, it works.
The button only needs the payload and the signed payload. The $client->createCheckoutSession is not needed. More over, the checkoutSessionId of the resultCheckOut is different from the one obtained when the checkoutReviewReturnUrl is called.
Related
I am working Paypal checkout php sdk. I follow the document from here https://github.com/paypal/Checkout-PHP-SDK
First I create an order
$env = new SandboxEnvironment($clientId, $secretKey);
$client = new PayPalHttpClient($env);
$request = new OrdersCreateRequest();
$request->prefer('return=representation');
$request->body = buildOrder($order);
//buildOrder has this param:
/*
"application_context" => [
"return_url" => 'domain/paypal/return.php',
"cancel_url" => 'domain/paypal/cancel.php'
]
*/
//request body
$body = array (
'intent' => 'CAPTURE',
'purchase_units' =>
array (
0 =>
array (
'reference_id' => 9,
'amount' =>
array (
'value' => 125.63,
'currency_code' => 'USD',
'breakdown' =>
array (
'item_total' =>
array (
'currency_code' => 'USD',
'value' => 125.63,
),
),
),
'items' =>
array (
0 =>
array (
'name' => 'Demo 46',
'description' => NULL,
'sku' => NULL,
'unit_amount' =>
array (
'currency_code' => 'USD',
'value' => 98.0,
),
'quantity' => '1',
),
1 =>
array (
'name' => 'Demo 28',
'description' => NULL,
'sku' => NULL,
'unit_amount' =>
array (
'currency_code' => 'USD',
'value' => 12.22,
),
'quantity' => '1',
),
2 =>
array (
'name' => 'Addon 33',
'description' => NULL,
'sku' => NULL,
'unit_amount' =>
array (
'currency_code' => 'USD',
'value' => 15.41,
),
'quantity' => '1',
),
),
),
),
'application_context' =>
array (
'return_url' => 'http://domain.test/paypal/return',
'cancel_url' => 'http://domain.test/paypal/canceled',
),
)
$response = $client->execute($request);
Create Order response:
{"statusCode":201,"result":{"id":"10M47599SM3059709","intent":"CAPTURE","status":"CREATED","purchase_units":[{"reference_id":"9","amount":{"currency_code":"USD","value":"125.63","breakdown":{"item_total":{"currency_code":"USD","value":"125.63"}}},"payee":{"email_address":"sb-kpo1v7959755#business.example.com","merchant_id":"XEH8BEAE3FXPW"},"items":[{"name":"Demo 46","unit_amount":{"currency_code":"USD","value":"98.00"},"quantity":"1"},{"name":"Demo 28","unit_amount":{"currency_code":"USD","value":"12.22"},"quantity":"1"},{"name":"Addon 33","unit_amount":{"currency_code":"USD","value":"15.41"},"quantity":"1"}]}],"create_time":"2021-09-30T22:59:31Z","links":[{"href":"https:\/\/api.sandbox.paypal.com\/v2\/checkout\/orders\/10M47599SM3059709","rel":"self","method":"GET"},{"href":"https:\/\/www.sandbox.paypal.com\/checkoutnow?token=10M47599SM3059709","rel":"approve","method":"GET"},{"href":"https:\/\/api.sandbox.paypal.com\/v2\/checkout\/orders\/10M47599SM3059709","rel":"update","method":"PATCH"},{"href":"https:\/\/api.sandbox.paypal.com\/v2\/checkout\/orders\/10M47599SM3059709\/capture","rel":"capture","method":"POST"}]},"headers":{"":"","Content-Type":"application\/json","Content-Length":"1085","Connection":"keep-alive","Date":"Thu, 30 Sep 2021 22","Application_id":"APP-80W284485P519543T","Cache-Control":"max-age=0, no-cache, no-store, must-revalidate","Caller_acct_num":"XEH8BEAE3FXPW","Paypal-Debug-Id":"95be3b11c12e7","Strict-Transport-Security":"max-age=31536000; includeSubDomains"}}
Then I can get orderID, I store it in session. Next I redirect buyer to approve url from Paypal response.
Next, buyer makes payment and Paypal drives buyer to my return url above.
In return.php I capture order by this piece of code
$env = new SandboxEnvironment($clientId, $secretKey);
$client = new PayPalHttpClient($env);
//$orderId can get from session or from `token` param in return url
$request = new OrdersCaptureRequest($orderId);
$request->prefer('return=representation');
$response = $client->execute($request);
New response from OrdersCaptureRequest:
{"name":"NOT_AUTHORIZED","details":[{"issue":"PERMISSION_DENIED","description":"You do not have permission to access or perform operations on this resource."}],"message":"Authorization failed due to insufficient permissions.","debug_id":"e8021203038f1","links":[{"href":"https://developer.paypal.com/docs/api/orders/v2/#error-PERMISSION_DENIED","rel":"information_link"}]} {"exception":"[object] (PayPalHttp\\HttpException(code: 0): {\"name\":\"NOT_AUTHORIZED\",\"details\":[{\"issue\":\"PERMISSION_DENIED\",\"description\":\"You do not have permission to access or perform operations on this resource.\"}],\"message\":\"Authorization failed due to insufficient permissions.\",\"debug_id\":\"e8021203038f1\",\"links\":[{\"href\":\"https://developer.paypal.com/docs/api/orders/v2/#error-PERMISSION_DENIED\",\"rel\":\"information_link\"}]}
The response I have is (old):
{"name":"NOT_AUTHORIZED","details":[{"issue":"PERMISSION_DENIED","description":"You do not have permission to access or perform operations on this resource."}],"message":"Authorization failed due to insufficient permissions.","debug_id":"ff1bfd34831cb","links":[{"href":"https://developer.paypal.com/docs/api/orders/v2/#error-PERMISSION_DENIED","rel":"information_link"}]} {"exception":"[object] (PayPalHttp\\HttpException(code: 0): {\"name\":\"NOT_AUTHORIZED\",\"details\":[{\"issue\":\"PERMISSION_DENIED\",\"description\":\"You do not have permission to access or perform operations on this resource.\"}],\"message\":\"Authorization failed due to insufficient permissions.\",\"debug_id\":\"ff1bfd34831cb\",\"links\":[{\"href\":\"https://developer.paypal.com/docs/api/orders/v2/#error-PERMISSION_DENIED\",\"rel\":\"information_link\"}]}
Then I came with solution to combine between client and server
I embed the Paypal checkout button
Create order from server
Handle approval from js client
Capture order by Paypal js function.
And Paypal responses with COMPLETED status.
<script
src="https://www.paypal.com/sdk/js?client-id=[SANDBOX_CLIENT_ID]"></script>
<script>
paypal.Buttons({
createOrder: function(data, actions){
return fetch('create-order.php', {
method: 'post',
headers: {
'content-type': 'application/json'
},
}).then(function(res) {
return res.json();
}).then(function(data) {
return data.token;
});
},
onApprove: function(data, actions){
return actions.order.capture().then(function(details){
console.log(details);
});
}
});
</script>
Even I tried to send orderID from onApprove event of Paypal js to my server for capturing order, the same issue with PERMISSION-DENIED happens.
Please help on this.
Redirecting away to a rel:approve URL is for old websites. You should use the PayPal button rather than redirecting away, it's a much nicer and more modern in-context experience for the buyer, keeping your site loaded in the background/lightbox fade.
The problem you are having with capturing an order on the server side appears to be a matter of using the wrong request object:
//$orderId can get from session or from `token` param in return url
$request = new OrdersCreateRequest($orderId);
At this stage, you need an OrdersCaptureRequest instead. See the sample bundled as part of the SDK.
I just had this same situation and the problem was my access token.
Generating an access token via /v1/oauth2/token
and passing the
Request Header "Authorization", "Basic " & Base64(clientID) & ":" & Base64(clientSecret)
Works and returns an access token.
But this token will only allow you to Create an Order not Capture it!
You need to Base64 encode the ":" as well!
Changing the request header to
"Authorization", "Basic " & Base64(clientID & ":" & clientSecret)
Creates a token that will allows capture as well.
This took me ages to debug as the token was returning and working to create the order! I found this by checking the scope returned when creating the token.
I'm trying to secure my callback url when completed event is triggered.
My Controller:
public function callbackSubscriptionCompleted(
int $subscriptionId,
DocusignService $docusignService,
Request $request
) {
$signature = $request->headers->get("X-DocuSign-Signature-1");
$payload = file_get_contents('php://input');
$isValid = $docusignService->isValidHash($signature, $payload);
if (!$isValid) {
throw new ApiException(
Response::HTTP_BAD_REQUEST,
'invalid_subscription',
'Signature not OK'
);
}
return new Response("Signature OK", Response::HTTP_OK);
}
My DocusignService functions:
private function createEnvelope(Company $company, Subscription $subscription, LegalRepresentative $legalRepresentative, Correspondent $correspondent, $correspondents) : array
{
// ...
$data = [
'disableResponsiveDocument' => 'false',
'emailSubject' => 'Your Subscription',
'emailBlurb' => 'Subscription pending',
'status' => 'sent',
'notification' => [
'useAccountDefaults' => 'false',
'reminders' => [
'reminderEnabled' => 'true',
'reminderDelay' => '1',
'reminderFrequency' => '1'
],
'expirations' => [
'expireEnabled' => 'True',
'expireAfter' => '250',
'expireWarn' => '2'
]
],
'compositeTemplates' => [
[
'serverTemplates' => [
[
'sequence' => '1',
'templateId' => $this->templateId
]
],
'inlineTemplates' => [
[
'sequence' => '2',
'recipients' => [
'signers' => [
[
'email' => $legalRepresentative->getEmail(),
'name' => $legalRepresentative->getLastname(),
'recipientId' => '1',
'recipientSignatureProviders' => [
[
'signatureProviderName' => 'universalsignaturepen_opentrust_hash_tsp',
'signatureProviderOptions' => [
'sms' => substr($legalRepresentative->getCellphone(), 0, 3) == '+33' ? $legalRepresentative->getCellphone() : '+33' . substr($legalRepresentative->getCellphone(), 1),
]
]
],
'roleName' => 'Client',
'clientUserId' => $legalRepresentative->getId(),
'tabs' => [
'textTabs' => $textTabs,
'radioGroupTabs' => $radioTabs,
'checkboxTabs' => $checkboxTabs
]
]
]
]
]
]
]
],
'eventNotification' => [
"url" => $this->router->generate("api_post_subscription_completed_callback", [
"subscriptionId" => $subscription->getId()
], UrlGeneratorInterface::ABSOLUTE_URL),
"includeCertificateOfCompletion" => "false",
"includeDocuments" => "true",
"includeDocumentFields" => "true",
"includeHMAC" => "true",
"requireAcknowledgment" => "true",
"envelopeEvents" => [
[
"envelopeEventStatusCode" => "completed"
]
]
]
];
$response = $this->sendRequest(
'POST',
$this->getBaseUri() . '/envelopes',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->getCacheToken()
],
json_encode($data)
);
}
public function isValidHash(string $signature, string $payload): bool
{
$hexHash = hash_hmac('sha256',utf8_encode($payload),utf8_encode($this->hmacKey));
$base64Hash = base64_encode(hex2bin($hexHash));
return $signature === $base64Hash;
}
I've created my hmac key in my Docusign Connect and i'm receiving the signature in the header and the payload but the verification always failed.
I've followed the Docusign documentation here
What's wrong ?
PS: Sorry for my bad english
Your code looks good to me. Make sure that you are only sending one HMAC signature. That way your hmacKey is the correct one.
As a check, I'd print out the utf8_encode($payload) and check that it looks right (it should be the incoming XML, no headers). Also, I don't think it should have a CR/NL in it at the beginning. That's the separator between the HTTP header and body.
Update
I have verified that the PHP code from the DocuSign web site works correctly.
The payload value must not contain either a leading or trailing newline. It should start with <?xml and end with >
I suspect that your software is adding a leading or trailing newline.
The secret (from DocuSign) ends with an =. It is a Base64 encoded value. Do not decode it. Just use it as a string.
Another update
The payload (the body of the request) contains zero new lines.
If you're printing the payload, you'll need to wrap it in <pre> since it includes < characters. Or look at the page source.
It contains UTF-8 XML such as
<?xml version="1.0" encoding="utf-8"?><DocuSignEnvelopeInformation xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.docusign.net/API/3.0"><EnvelopeStatus><RecipientStatuses><RecipientStatus><Type>Signer</Type><Email>larry#worldwidecorp.us</Email><UserName>Larry Kluger</UserName><RoutingOrder>1</RoutingOrder><Sent>2020-08-05T03:11:13.057</Sent><Delivered>2020-08-05T03:11:27.657</Delivered><DeclineReason xsi:nil="true" /><Status>Delivered</Status><RecipientIPAddress>5.102.239.40</RecipientIPAddress><CustomFields /><TabStatuses><TabStatus><TabType>Custom</TabType><Status>Active</Status><XPosition>223</XPosition><YPosition>744....
We've done some more testing, and the line
$payload = file_get_contents('php://input');
should be very early in your script. The problem is that a framework can munge the php://input stream so it won't work properly thereafter.
Note this page from the Symfony site -- it indicates that the right way to get the request body is:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ParameterBag;
$app->before(function (Request $request) {
$payload = $request->getContent();
hmac_verify($payload, $secret);
});
I would try to use the Symfony code instead of file_get_contents('php://input');
I have a simple POST request using Guzzle from Laravel to send. I tested the request with Postman but it keeps sending request until timeout without returning any response data. I set up some URLs to return sample data and they work fine when testing separately. But when bring the URLs to the request to call from Guzzle, then this sending-request-till-forever happened. Hope you guys can help me shed some light on this problem.
Here is my request code:
$client = new Client();
$req = new Request('POST', 'http://13.114.233.87/api/ocr');
$reqData = $client->send($req, ['timeout' => 5]);
return $reqData;
Here are the handlers for each sample URL:
(1) http://13.114.233.87/api/ocr
Route::post('ocr', 'OcrController#postOcrData'); // routing
public function postOcrData()
{
return response()->json(collect([
'request_id' => rand(0, 9),
'result' => 0,
'error_code' => '',
'error_message' => ''
]));
}
(2) http://13.114.233.87/api/ocr/getResult/{id}
Route::get('ocr/getResult/{id}', 'OcrController#getOcrData'); // routing
public function getOcrData($id)
{
return response()->json(collect([
'request_id' => $id,
'result' => 0,
'error_code' => '',
'error_message' => '',
'fields' => []
]));
}
I have an object function to send news to an API:
class Manager
{
public function __construct(GuzzleHttp\Client $client)
{
$this->client = $client;
}
public function uploadNews($title, $text, $timestamp = null)
{
// DEFAULT FOR PUBLISH TIMESTAMP IS SET IF VALUE IS NULL
$timestamp = $timestamp ?? time();
$requestBody = [
'header' => [
'Accept' => 'application/json',
],
'timeout' => 300,
'body' => [
'title' => $title,
'text' => $text,
'publish_date' => $timestamp,
]
];
return $this->client->request('POST', 'http://api/endpoint', $requestBody);
}
}
How do I write a PHPUnit unit test to test the final $request passed to the client's request() function is of the expected format, when the $timestamp is not set (the API expects a timestamp)? I've written a unit test. But is not ideal, since there could be a difference in the tow timestamps.
class ManagerTest extends \PHPUnit\Framework\TestCase
{
public function testCanUploadNewsWithoutTimestamp()
{
$expectedRequestBody = [
'header' => [
'Accept' => 'application/json',
],
'timeout' => 300,
'body' => [
'title' => 'News title',
'text' => 'News article sample text.',
'publish_date' => time(),
]
];
$guzzleMock = $this->createMock(GuzzleHttp\Client::class);
$guzzleMock->expects($this->once())
->method('request')
->with(
'POST',
'http://api/endpoint',
$expectedRequestBody
);
(new Manager($guzzleMock))->uploadNews(
'News title',
'News article sample text.'
);
}
}
This is my current test result:
Parameter 2 for invocation GuzzleHttp\Client::request('POST', 'http://api/endpoint', Array (...)) does not match expected value.
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
## ##
'body' => Array (
'title' => 'News title'
'text' => 'News article sample text.'
- 'publish_date' => 1574550178
+ 'publish_date' => 1574550179
)
)
Please note that the only thing I need to make sure is that the timestamp is passed with request. The value of the timestamp doesn't matter.
PS: I'm really new to Test Driven Development, so the answer might be obvious and I might still not know it.
I am currently building a e-mail client (inbound and outbound sending) using Mandrill as the e-mail sending / inbound service and Laravel 3.x.
In order to send messages, I am using the HTTPful bundle with the Mandrill using the following code in my mail/compose POST method.
$url = 'https://mandrillapp.com/api/1.0/messages/send.json';
$data = array(
'key' => '{removedAPIkey}',
'message' => array (
'to' => array( array( "email" => $_to ) ),
'from_name' => Auth::user()->name,
'from_email' => Auth::user()->email,
'subject' => $_subject,
'html' => $_body
),
'async' => true
);
$request = Httpful::post($url)->sendsJson()->body($data)->send();
Link to better formatted code above: http://paste.laravel.com/m79
Now as far as I can tell from the API log, the request is correctly made (with the expected JSON) and a response of the following format is sent back:
[
{
"email": "test#test.com",
"status": "queued",
"_id": "longmessageID"
}
]
However, what I am trying to do is access the response from the request (specifically the _id attribute), which is in JSON. Now as far as I'm aware, the HTTPful class should do this automatically (using json_decode()). However, accessing:
$request->_id;
is not working and I'm not entirely sure how to get this data out (it is required so I can record this for soft-bounce, hard-bounce and rejection messages for postmaster-like functionality)
Any assistance would be appreciated.
Edit
Using the following code, results in the mail being sent but an error returned:
$url = 'https://mandrillapp.com/api/1.0/messages/send.json';
$data = array(
'key' => '{removedAPIkey}',
'message' => array (
'to' => array( array( "email" => $_to ) ),
'from_name' => Auth::user()->name,
'from_email' => Auth::user()->email,
'subject' => $_subject,
'html' => $_body
),
'async' => true
);
$request = Httpful::post($url)->sendsJson()->body($data)->send();
if ( $request[0]->status == "queued" ) {
$success = true;
}
Results in an exception being thrown: Cannot use object of type Httpful\Response as array
I must say, a huge thanks to Aiias for his assistance. I managed to fix this myself (I must have spent hours looking at this). For anyone who wants to know, the HTTPful bundle has a body array, where the response is kept. Therefore, the code below works:
$url = 'https://mandrillapp.com/api/1.0/messages/send.json';
$data = array(
'key' => '{removedAPIkey}',
'message' => array (
'to' => array( array( "email" => $_to ) ),
'from_name' => Auth::user()->name,
'from_email' => Auth::user()->email,
'subject' => $_subject,
'html' => $_body
),
'async' => true
);
$request = Httpful::post($url)->sendsJson()->body($data)->send();
if ( $request->body[0]->status == "queued" ) {
$success = true;
}
Again, huge thanks to Aiias for clearing some major confusion up for me!