How to protect sensitive pages for paypal payment processing? - php

I'm using the paypal express checkout, which follows this flow:
When they submit on the paypal website it follows a return url on my site which shows the order review with a confirm button, and two GET variables are passed back from paypal: token and payerId. The token gives me permission to request shipping info & later finalize the payment.
The first problem is I can access the 'checkout with paypal' page directly by typing in the URL into the address bar and it will submit the request to paypal, if the $_SESSION['Payment_Amount'] variable is not set it processes with the payment amount as 0 and throws an error.
SetExpressCheckout API call failed. Detailed Error Message: This transaction cannot be processed. The amount to be charged is zero.
I know I can set another session variable on the cart page to make sure they visit the cart first, and then clear the variable after checking for it, but another problem remains that the user only needs to visit the cart page once and the variable will be set to allow them to visit the sensitive page which sends a token request to paypal.
The next problem is that after going through all the steps and the user pressing the 'confirm order' button, the request is sent to paypal to process the order/money for that token. The user can press the 'BACK' button on the page and see the order-review again, then the user can press confirm order again and an error will show that an order was already processed for that token.
GetExpressCheckoutDetails API call failed. Detailed Error Message: A successful transaction has already been completed for this token.
That's clearly a good thing but what should I implement to prevent the user from accessing sensitive pages? Will I need to track certain keys in my back-end database?
At the moment i'm working on localhost with paypal's sandbox.

You have to create a process somehow that guarantees that the user follows the needed steps in the right order and prevents him from jumping out of this order.
Tracking the steps in the users session seems like the natural thing to do. If the session does not allow the step requested, redirect him elsewhere instead of asking paypal.
The deluxe version would be you implemented a state machine for easier improvements later on. State machines have the disadvantage of looking like huge overhead at first, and are too much hassle to implement later if you initially took a different approach. That's why it is important to think about using one from the start.
What if you want to add another payment provider later? A state machine could be easily extended for this - anything else might be a mess then.
Edit:
Actually, the only thing paypal expects you to send to them after the user is back on your site is the amount you want to charge. This info can be passed by putting it into the return url you send to paypal. Try adding some checksum there to prevent data errors and easy tampering (Paypal lets the process fail if the amount is incorrect nevertheless), and you are basically done. No session at all needed.
Edit2: Here is an excerpt of my code that defines the nvp parameters for paypals first step. You need the necessary auth stuff inside, too.
public function preparePayment(...) {
$nvp = array(
'METHOD' => 'SetExpressCheckout',
'VERSION' => '52.0',
'RETURNURL' => 'https://'.$request->server['HTTP_HOST'].'/'.$request->getLanguage().'/paypal/success/'.$this->hashAmount($amount),
'CANCELURL' => 'https://'.$request->server['HTTP_HOST'].'/'.$request->getLanguage().'/paypal/cancel',
'CURRENCYCODE' => $amount->getCurrency(),
'AMT' => number_format($amount->getAmount(), 2, '.', ''),
'ITEMAMT' => number_format($amount->getNettoAmount(), 2, '.', ''),
'TAXAMT' => number_format($amount->getVatAmount(), 2, '.', ''),
'PAYMENTACTION' => 'Sale',
'LOCALECODE' => strtoupper($request->getLanguage())
);
}
protected function hashAmount(Currency_Class $amount) {
return urlencode(
sprintf(
'%s-%s-%s-%u',
number_format($amount->getNettoAmount(), 2, '', ''),
number_format($amount->getVatAmount(), 2, '', ''),
strtoupper($amount->getCurrency()),
$this->makeChecksumString(number_format($amount->getNettoAmount(), 2, '', ''), strtoupper($amount->getCurrency()))
)
);
}
protected function makeChecksumString($amount, $currency) {
return crc32(sprintf('%sSaltValue%s', $amount, $currency));
}
protected function dehashAmount($string) {
$parts = array();
$found = preg_match('/^(\d+)\-(\d+)\-([A-Z]+)\-(\d+)$/', $string, $parts);
if ($found) {
$check = sprintf('%u', $this->makeChecksumString($parts[1], $parts[3]));
if ($check == $parts[4]) {
$netto = floatval(substr($parts[1], 0, -2) .'.'. substr($parts[1], -2));
$vat = floatval(substr($parts[2], 0, -2) .'.'. substr($parts[2], -2));
}
}
return ...
}

Related

How to send funds from one personal PayPal account to another in PHP

I tried use checkout api in Paypal to send funds from one personal account to another, But after clicking the continue button, refreshed checkout page and didnt transfer the funds.
Here is the PHP code what I did:
private function sendDirectlyPayment(float $amount, string $currency, string $address, string $backUrl): array
{
$params = [
'intent' => 'CAPTURE',
'purchase_units' => [
[
'amount' => [
'currency_code' => $currency,
'value' => (string) $amount
],
'payee' => [
'email_address' => $address
]
]
]
];
$data = $this->encodeData($params);
$headers = $this->getAuthHeaders($data);
$res = $this->execute(self::POST, '/v2/checkout/orders', $data, $headers);
return $res;
}
Not sure how to make to work api properly.
You set up the payment correctly, and the payer is approving the payment. But you are missing the final 'capture' API step, so no transaction is created. You need to implement the v2/checkout/orders/.../capture API call after approval.
The best way is to make two callable routes on your server, one for 'Create Order' and one for 'Capture Order', documented here. These routes should return only JSON data (no HTML or text). The latter one should (on success) store the payment details in your database before it does the return (particularly purchase_units[0].payments.captures[0].id, the PayPal transaction ID)
Pair those two routes with the following approval flow: https://developer.paypal.com/demo/checkout/#/pattern/server
----
The smart button gives the best user experience -- however, if for some reason your use case is better without it, then what you need to do is specify a return_url in the create order call. When the payer clicks continue and reaches the return_url, it is there when you would display an order review page and have a button that triggers the capture API call for the final action.
If you want to skip having a review page and do the capture immediately, this is also possible to do, but you need to set the application_context object's user_action parameter to PAY_NOW, so that the last button has appropriate text instead of 'Continue'.

PayPal IPN POST results in 0 PHP Laravel

I'm struggling with a simple storage script that should be called via PayPal IPN to save the current payment.
I'm using
$input = json_encode($request->all());
$paylog = Paylog::create([
'data' => $input,
]);
and the result from any incoming POST is always "0".
Where is my fault?
Perhaps you mean to access $_POST
IPN messages should be verified with a postback to ipnpb.paypal.com before doing anything with their contents
IPN is a very old and clunky technology, why are you using it?
To obtain details on a 'current' payment, you should be using an API like v2/checkout/orders to facilitate the checkout. For this you need two routes on your server, one for 'Set Up Transaction' and one for 'Capture Transaction', documented here: https://developer.paypal.com/docs/checkout/reference/server-integration/
The best approval flow to pair with your two routes is https://developer.paypal.com/demo/checkout/#/pattern/server
Solution:
//Make sure encode values to UTF8
$input = json_encode(array_map('utf8_encode',$request->all()));
//And store
$paylog = Paylog::create([
'data' => $input,
]);
Or to make things a little more efficient:
$paylog = Paylog::create([
'data' => $json_encode(array_map('utf8_encode',$request->all())),
]);

Verify stripe payment amount

For my shopping site I have a fairy simple stripe implementation on the frontend where user hits "pay through stripe" button fills in CC details in a popup and hits proceed. If all goes well I get a token as $_POST['stripeToken'] which I process like below (for web payments)
try
{
$stripe = new Stripe();
$stripe = Stripe::make();
$customer = $stripe->customers()->create([
'email' => $_POST['email_id'],
'source' => $_POST['stripeToken']
]);
$charge = $stripe->charges()->create([
'customer' => $customer['id'],
'amount' => $total_amount, // note this is calculated again on server to prevent fraud
'currency' => 'sgd'
]);
}
catch (\Cartalyst\Stripe\Exception\CardErrorException $e)
{
return view('front.payment_error')->with('message', $e->getMessage());
}
Thing is we cannot rely on $total_amount coming from frontend form submission as user can easily spoof this amount and pay just $1 in stripe and get a token and spoof $total_amount to 1 thus getting products at whatever price he wants ,that's why I need to calculate his `$total_amount again on the server (from his cart) and use that to process stripe on the server.If that amount doesn't match what the token stands for , stripe would automatically raise an exception and prevent fraud.
So far good.. but the problem comes when dealing with API , mobile app will process stripe using their own libs. on the client side they would just send the final (for recording in db) but obviously I cannot use it to charge since that is already done on the mobile.Since these days it's very easy to change app behaviour (by patching apks) or craft custom HTTP request (postman) , server check is a must in case of payments.
So my question is how can I verify from the token the actual amount user paid
ie. reverse convert stripeToken => actual paid amount
Update:
This is what I am looking in case of Stripe
https://developer.paypal.com/docs/integration/mobile/verify-mobile-payment/

How do I get the Security Token for PayPal Payments Advanced gateway integration?

We have PayPal Payments Advanced and I'm unable to get past the first gateway integration step. Perhaps I'm missing something simple that should be obvious.
All the official PayPal documents I've been able to find for integrating the gateway for Advanced say the first step is to obtain a Secure Token. The page at
https://developer.paypal.com/webapps/developer/docs/classic/payflow/gs_ppa_hosted_pages/
for example.
I'm posting my test script below (sensitive info modified).
Every time I run the test script, I get a "Error: Your transaction can no longer be processed. Please return to the merchant's web site or contact the merchant. Error: 160" error message.
According to the PayPal Gateway Developer Guide and Reference, error 160 is, "Secure Token already been used. Indicates that the secure token has expired due to either a successful transaction or the token has been used three times while trying to successfully process a transaction. You must generate a new secure token."
Yet, the secure token has not already been used. A new one is generated every time the script is run.
"Enable Secure Token" is set to "Yes" in PayPal Manager.
Here is the script. What am I doing wrong?
<?php
$url = 'https://payflowlink.paypal.com';
#$url = 'https://pilot-payflowlink.paypal.com';
$token = md5( 'Will Bontrager' . time() );
/* $info assignment is all one line. Multi-line here for readability */
$info = "PARTNER=PayPal&
VENDOR=CertainReservations&
USER=ABC123&
PWD=321cba&
TRXTYPE=S&
AMT=23.45&
CREATESECURETOKEN=Y&
SECURETOKENID=$token";
echo "<pre>Value:$info</pre>";
$options = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_VERBOSE => false,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $info
);
$ch = curl_init($url);
curl_setopt_array($ch,$options);
$content = curl_exec($ch);
$err = curl_errno($ch);
$errmsg = curl_error($ch) ;
$info = curl_getinfo($ch);
curl_close($ch);
if( $err )
{
echo "<pre>Error. $err\n$errmsg\n";
print_r($info);
echo '</pre>';
}
echo $content;
?>
Thank you very much for any guidance.
I think I must be missing some critical information.
Will
Just wanted to add that error 160 is also thrown when no secure token is passed to step 2.
Had this happen: host blocked curl calls, so the Paypal iframe was requested without the secure token which resulted in error 160.
According to Page 31 of this Payflow Gateway documentation this might work. I've copied the content here in the event he PDF is removed or moved without a proper 301 redirect.
To create a secure token, pass all parameters that you need to process
the transaction except for payment details parameters such as the
credit card number, expiration date, and check number.
In addition, pass the following Payflow parameters to create the
secure token.
Set SECURETOKENID to a unique alphanumeric value up to 36 characters in length.
SECURETOKENID=9a9ea8208de1413abc3d60c86cb1f4c5
Set CREATESECURETOKEN to the value Y to request that the Gateway server return a token.
CREATESECURETOKEN=Y
Set SILENTTRAN to the value TRUE to suppress the display of hosted pages.
SILENTTRAN=TRUE
Successful transactions will return RESULT=0. From page 33.
A Payflow Secure Token will expire:
If the same Secure Token is passed to Payflow a total of 3 times.
20 minutes after the Secure Token was generated.
When the token is used in a successful transaction
It's likely the formatting... here's a little about the parameters you're passing (from page 51):
Because the ampersand (&) and equal sign (=) characters have special
meanings, they are invalid in a name-value pair value.
The following are invalid:
COMPANYNAME=Ruff & Johnson COMMENT1=Level=5
To include special characters in the value portion of a name-value
pair, use a length tag. The length tag specifies the exact number of
characters and spaces that appear in the value. The following are
valid.
COMPANYNAME[14]=Ruff & Johnson
COMMENT1[7]=Level=5
NOTE: Do not use quotation marks ("") even if you use a length tag.
As far as I can tell (this is not documented clearly that I have found though this is helpful) you can't get a secure token from https://pilot-payflowlink.paypal.com but https://pilot-payflowpro.paypal.com seems to work just fine. Once you have your token you can use it with payflowlink.

Verifying a Paypal transaction via POST information

I'm at a complete loss. I think I might be getting "mis-informed", but I'll try explain my situation as best I can.
The Idea
We have a form for users to purchase credits. Type in credit number,
click pp button.
Upon click of button, a post is made to set the
transaction log information and set it as pending (works fine).
Upon valid post return it continues to submit the paypal form (works also).
The user is redirected to paypal page and makes payment (so far so good).
after payment made, they click the return and are directed toward the "success" page (still working).
upon reaching this page I take in post data from pp (uh oh, here's where it gets sticky)
verify the data is "true" pp data and update the transaction log (HOW!?)
What I'm being told & what i've tried
I was initially going to use IPN to do a post back to paypal to verify the recieved data (ensure it wasn't spoofed), however, I'm being told that for cost purposes and having to setup an "ipn server" we can't use IPN ....
Ok, so I was gonna use PDT, except either I missed a major step in my attempt or it ISNT working right at all because I'm not doing somthing right. Here is where I'm lost, i've tried a dozen different things, including a direct link post, using sparks (for CI) to set the data and call to paypal link, and etc ...
I've looked over every paypal question on here and a half dozen other forums and can't seem to get anything going.
Can anyone "clearly" tell me how I can verify the POST data of a successful paypal transaction and maybe even tell me if i'm being misinformed about the IPN, cause I looked over the docs and I can't find what i've been told, nor can I really find my solution.
I feel stupid, please help.
When your user clicks a PayPal button and goes to PayPal, when they complete the transaction, an IPN POST is made to a URL of your choosing. So you don't have to have another web server.
When the IPN request comes in, PayPal wants you to re-send the entire POST they made to you back to them, including all of the fields, in the exact order, at which point they will return the word 'VERIFIED' or 'INVALID.' If verified, then do whatever it is that you need to do to toggle your txn log from pending to verified. Also, any information you include in your button (your button is actually a form so you can include your own fields) is included in the POST. Useful for keeping a 'transaction id' or some other identifier for mapping back to your transaction.
If the IPN fails it will resend in n+4 minute increments (where n is how long it waited the last time - 4 minutes, next after 8 minutes, next after 12 minutes, etc) for a few days.
Finally made it work correctly thanks to the update in info on IPN.
My solution added the following line to my form:
<input type="hidden" name="notify_url" value="<?= site_url('payment/notifyTest'); ?>">
Then in the notifyTest function i ran this:
$pDat = $this->input->post(NULL, TRUE);
$isSandBox = array_key_exists('test_ipn', $pDat) && 1 === (int)$pDat['test_ipn'] ? TRUE : FALSE;
$verifyURL = $isSandBox ? 'https://www.sandbox.paypal.com/cgi-bin/webscr' : 'https://www.paypal.com/cgi-bin/webscr';
$token = random_string('unique');
$request = curl_init();
curl_setopt_array($request, array
(
CURLOPT_URL => $verifyURL,
CURLOPT_POST => 0,
CURLOPT_POSTFIELDS => http_build_query(array('cmd' => '_notify-validate') + $pDat),
CURLOPT_RETURNTRANSFER => 0,
CURLOPT_HEADER => 0,
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
CURLOPT_CAINFO => 'cacert.pem',
));
$response = curl_exec($request);
$status = curl_getinfo($request, CURLINFO_HTTP_CODE);
curl_close($request);
if($status == 200 && $response == 'VERIFIED') {
// SUCCESS
$data = array (
... => ...
);
$this->db->insert('transactions', $data);
}
else {
// FAILED
$data = array (
... => ...
);
$this->db->insert('transactions', $data);
};
THE IMPORTANT DIFFERENCE AS WE FOUND -> DO NOT SET YOUR CURL VARS TO TRUE OR FALSE
USE 0 FOR TRUE AND 1 FOR FALSE, IT MIGHT SOUND STUPID, BUT IT WOIKED!!!

Categories