thephpleague / omnipay

A framework agnostic, multi-gateway payment processing library for PHP 5.6+
http://omnipay.thephpleague.com/
MIT License
5.92k stars 928 forks source link

Sagepay server and redirectionURL #255

Closed JoSquiders closed 8 years ago

JoSquiders commented 9 years ago

I'm getting error: 5006 Unable to redirect to Vendor's web site. The Vendor failed to provide a RedirectionURL.

I'm trying to connect to Sagepay Server and am setting 'returnUrl' but it is never hitting the URL I specify here. It does successfully connect to Sagepay and I go through the test payment all okay. I receive the error when clicking pay now. I know Sagepay works a bit different from the other gateways but I don't know what I'm missing.

My code looks like:

public function index()
        {
//the function where I initially start the payment process
            $this->params = array(
                'description'=> 'Online order',
                'currency'=> 'GBP',
                'transactionId'=> $this->orderNo,
                'amount'=> '10.00'
            );

            $this->params['returnUrl'] = site_url('/checkout/payment-gateway-process/' . $this->orderNo) .  '/';

            $this->params['card'] = array(
                'firstName' => 'xxx
            );

            try {
                $response = $this->gateway->purchase($this->params)->send();

                if ($response->isSuccessful()) :

                    redirect('/checkout/payment-gateway-success/' . $this->orderNo);

                elseif ($response->isRedirect()) :

                    $response->redirect();
                else :

                endif;

            } catch (\Exception $e) {

            }

        }

    public function process_payment($orderNo)
        {
        //the function that should be called by Sagepay after payment has been made, i.e., the returnURL
// it never reaches this function
              $this->params = array(
                'description'=> 'Online order',
                'currency'=> 'GBP',
                'transactionId'=> $orderNo,
                'amount'=> '10.00'
            );

            $this->params['card'] = array(
                'firstName' => 'xxx'
            );

            $response = $this->gateway->completePurchase($this->params)->send();

            if ($response->isSuccessful()) :

                $response->confirm();
                redirect('/checkout/payment-gateway-success/' . $orderNo);
            endif;

     }

I have cut out some of it to make it readable but the $orderNo and 'card' parameters are set (I've only put firstName here but I do send all the details). I initiate the Omnipay Sagepay server instance in the __construct.

judgej commented 9 years ago

NOTE: the notification URL is now set with notifyUrl on many gateway drivers. See the docs for each driver for more details.

If you are using SagePay Server, then you need to provide a notification URL, which is usually known as a callback (the "returnUrl" in OmniPay, which is a total misnomer - it translates to the "NotificationURL" on SagePay, which is a more accurate name). That URL is where SagePay will send the results of the authorisation, which you store, and then tell SagePay where to send the user next. That notification is called outside of the user's session, so the transaction and results must be stored in your database for it to be accessed between the callback and the user pages.

So, it looks like you have that, with the callback being handled by process_payment(). What I believe you are missing, is the the URL that SagePay should send the user to in your callback. In your $response->confirm() you need to pass the URL that you want the user to be sent to. Not providing that is the source of the error you are getting from SagePay.

In addition:

I generally send the user to the same URL for confirm/invalid/success and then read the final status of the payment from the database (as stored in the callback) then take appropriate action based on that.

SagePay may seem unusual, but - trust me - it is a hell of a lot more secure than many other gateways, but ONLY if implemented correctly.

judgej commented 9 years ago

Just to highlight what may be confusing many people - the returnUrl set on SagePay Server is NOT a return URL. It is the URL to your callback handler that SagePay will use to notify you of the results. It is the SagePay NotificationURL.

The name returnUrl was just used because it was already there in the OmniPay common message, so made sense to use for consistency between drivers.

greydnls commented 9 years ago

@judgej thanks for pointing that out. I think it's definitely a point of confusion. I'm adding it tot he list of things we need to specifically document for this gateway.

JoSquiders commented 9 years ago

Thank you for your help judgej. I will tidy up the handling of the Sagepay responses once I can get it working, am still trying to get past that error at the moment! Some useful tips about the correct process thank you.

It's not getting past the completePurchase() function at the moment so it's not even hitting $response->confirm(). Am logging each step to a database and this is where it gets to so I must be missing something else!

judgej commented 9 years ago

@JoSquiders yes, sorry, I forgot another important step. When you first register the transaction, you must get the getTransactionReference() from the response. That will be a JSON string that you must save.

In the callback, you must pass this transaction reference, along with the transaction ID, to the completePurchase() method. This will be used to check that all the data sent to you have been correctly signed and has not been manipulated by anyone en-route. If you don't do that, or if the data has been manipulated, then send() will throw an exception. You are probably getting the exception, and so what gets returned to SagePay makes no sense to it.

This is what mine looks like, with $sagepay_transaction being the saved transaction ORM record:

    use Omnipay\Common\Exception\InvalidResponseException;

    try {
        $response = $gateway->completePurchase(array(
            'transactionId' => $sagepay_transaction->transaction_id,
            'transactionReference' => $sagepay_transaction->transaction_reference,
        ))->send();
    } catch(InvalidResponseException $e) {
        // Send "INVALID" response back to SagePay.
        $request = $gateway->completePurchase([]);
        $response = new \Omnipay\SagePay\Message\ServerCompleteAuthorizeResponse($request, []);
        $response->invalid(URL::route('payment_complete'), $e->getMessage());
    }
    // Tell SagePay we accept the result (whether it was authorised or not).
    $response->confirm(URL::route('payment_complete'));

I have omitted the saving of the updated transaction with the results, logging etc. for clarity.

judgej commented 9 years ago

I have another issue open to try to remove that exception, so this can be handled a lot more cleanly.

judgej commented 9 years ago

@kayladnls I have a note in my (v3) list to suggest standardising on a callbackUrl parameter for the handful of drivers that need it.

judgej commented 9 years ago

Is it worth moving this issue to the omnipay-sagepay repository? I found this online tool that seems to be able to do it:

https://github-issue-mover.appspot.com/

It was referenced from here: https://github.com/holman/feedback/issues/413

JoSquiders commented 9 years ago

@judgej Thank you so much - you've no idea how much I've been pulling my hair out over this! I've got past the error now so hopefully I can sort it out from here, it was just knowing what to pass where and what functions to call. I couldn't find any useful code examples to work from either so you've been a great help :)

judgej commented 9 years ago

Glad it was useful. Let us know how it all works out in the end - there may be other little things that need tweaking.

JoSquiders commented 9 years ago

Thank you @judgej. I'm just tweaking some bits but I have something along these lines:

<?php 

    use Omnipay\Omnipay;

    class PaymentGateway  {

        //live details
        private $live_vendor = 'xxx';
        //test details
        private $test_vendor= 'xxx';

        //payment settings
        private $testMode = true;
        private $api_vendor = '';
        private $gateway = null;

        public function __construct()
        {
            parent::__construct();
            //setup api details for test or live
            if ($this->testMode) :
                $this->api_vendor = $this->test_vendor;
            else :
                $this->api_vendor = $this->live_vendor;
            endif;

            //initialise the payment gateway
            $this->gateway = Omnipay::create('SagePay_Server');
            $this->gateway->setVendor($this->api_vendor);
            $this->gateway->setTestMode($this->testMode);

        }

        public function initiate()
        {

            //get order details
            $orderNo = customFunctionToGetOrderNo(); //get the order number from your system however you store and retrieve it

    $params = array(
                'description'=> 'Online order',
                'currency'=> 'GBP',
                'transactionId'=> $orderNo,
                'amount'=> customFunctionToGetOrderTotal($orderNo)
            );

            $customer = customFunctionToGetCustomerDetails($orderNo);

            $params['returnUrl'] = '/payment-gateway-process/' . $orderNo .  '/'; //this is the Sagepay NotificationURL

            $params['card'] = array(
                'firstName' => $customer['billing_firstname'],
                'lastName' => $customer['billing_lastname'],
                'email' => $customer['billing_email'],
                'billingAddress1' => $customer['billing_address1'],
                'billingAddress2' => $customer['billing_address2'],
                'billingCity' => $customer['billing_town'],
                'billingPostcode' => $customer['billing_postcode'],
                'billingCountry' => $customer['billing_country'],
                'billingPhone' => $customer['billing_telephone'],
                'shippingAddress1' => $customer['delivery_address1'],
                'shippingAddress2' => $customer['delivery_address2'],
                'shippingCity' => $customer['delivery_town'],
                'shippingPostcode' => $customer['delivery_postcode'],
                'shippingCountry' => $customer['delivery_country']
            );

            try {
                $response = $this->gateway->purchase($params)->send();

                if ($response->isSuccessful()) :

                    //not using this part

                elseif ($response->isRedirect()) :

                    $reference = $response->getTransactionReference();
                    customFunctionToSaveTransactionReference($orderNo, $reference);
                    $response->redirect();

                else :
                    // do something with an error
                    echo $response->getMessage();

                endif;

            } catch (\Exception $e) {

                //do something with this if an error has occurred
                echo 'Sorry, there was an error processing your payment. Please try again later.';
            }

        }

        public function processPayment($orderNo)
        {

            $params = array(
                'description'=> 'Online order',
                'currency'=> 'GBP',
                'transactionId'=> $orderNo,
                'amount'=> customFunctionToGetOrderTotal($orderNo)
            );

            $transactionReference = customFunctionToGetTransactionReference($orderNo);

            try {
                $response = $this->gateway->completePurchase(array(
                    'transactionId' => $orderNo,
                    'transactionReference' => $transactionReference,
                ))->send();

                customFunctionToSaveStatus($orderNo, array('payment_status' => $response->getStatus()));
                customFunctionToSaveMessage($orderNo, array('gateway_response' => $response->getMessage()));

                //encrypt it to stop anyone being able to view other orders     
                $encodeOrderNo = customFunctionToEncodeOrderNo($orderNo);
                $response->confirm('/payment-gateway-response/' . $encodeOrderNo);

            } catch(InvalidResponseException $e) {
                // Send "INVALID" response back to SagePay.
                $request = $this->gateway->completePurchase(array());
                $response = new \Omnipay\SagePay\Message\ServerCompleteAuthorizeResponse($request, array());

                customFunctionToSaveStatus($orderNo, array('payment_status' => $response->getStatus()));
                customFunctionToSaveMessage($orderNo, array('gateway_response' => $response->getMessage()));

                redirect('/payment-error-response/');
            }

        }

        public function paymentResponse($encodedOrderNo)
        {
            $orderNo = customFunctionToDecode($encodedOrderNo);
            $sessionOrderNo = customFunctionToGetOrderNo(); 
            if ($orderNo != $sessionOrderNo) :
                //do something here as someone is trying to fake a successful order
            endif;

            $status = customFunctionToGetOrderStatus($orderNo);

            switch(strtolower($status)) :
                case 'ok' :
                    customFunctionToHandleSuccess($orderNo);
                break;

                case 'rejected' :
                case 'notauthed' :
                    //do something to handle failed payments
                break;

                case 'error' :
                   //do something to handle errors

                break;

                default:

                    //do something if it ever reaches here

            endswitch;

        }

   }

I haven't worked on the error handling yet though I have some code in there for it. Have cut the code down to basics to make it easy for someone else to follow.

judgej commented 9 years ago

That looks like it covers most stuff.

The $params you set up in the callback (card and payee details) are not used, and don't need to be used, so a lot could come out there.

Checking that order number is the same as in the user's session is essential. I've seen examples of sites (e.g. some WooComnmerce and Drupal gateways) where this check is not done. What happens is that people make a tiny purchase with a valid card, and catch the final page URL without going there. Then they make a massive purchase with an invalid card, but then go to the final page URL they caught before, and it marks the larger payment as successful. This stuff does happen, is a well-known technique, so you can be sure someone will try it on your site.

JoSquiders commented 9 years ago

I have added in that check and yes makes perfect sense. Have updated my code above to make that clear and to take out some of the unnecessary $params. I'd probably still be going round in circles if it wasn't for your help so hopefully this will help someone else in the future :)

judgej commented 9 years ago

Totally :-) The more we get this sorted out, together, the easier it will be to use, and the more people will use it, and so the more people there are to help maintain it and take it to levels we would not have dreamt of. It's a virtuous circle.

sterlingsolutions commented 9 years ago

I am getting the same issue as documented here, and I have re-read this thread several times hoping I had missed something but can't see what, please help!

This is the code I have so far:


public function __construct(OrderInterface $order, OrderRepositoryInterface $orderRepository, StatusRepositoryInterface $statusRepository)
    {
        // by this stage we should have captured:
        // billing and shipping address
        // Cart::items should now be Order, OrderItems, and OrderItemFiles
        $this->gateway = OmniPay::create('SagePay_Server');
        $this->gateway->setVendor(Config::get('payment.vendor'));
        $this->gateway->setTestMode(getSetting('payment_test_mode'));
        $this->order = $order;
        $this->orderRepository = $orderRepository;
        $this->statusRepository = $statusRepository;
    }

    public function store(Payment $payment)
    {
        $this->setTransactionId();

        $options = $this->getOptions($payment);
// returns
// array:7 [▼
//  "card" => CreditCard {#475 ▼
//    #parameters: ParameterBag {#565 ▼
//      #parameters: array:24 [▼
//        "billingFirstName" => "web"
//        "shippingFirstName" => "web"
//        "billingLastName" => "support"
//        "shippingLastName" => "support"
//        "number" => "4929000000006"
//        "expiryMonth" => 2
//        "expiryYear" => 2017
//        "startMonth" => 2
//        "startYear" => 2012
//        "cvv" => "123"
//        "issueNumber" => ""
//        "billingAddress1" => "88"
//        "billingAddress2" => ""
//        "billingCity" => "Billing"
//        "billingPostcode" => "412"
//        "billingState" => "Billingshire"
//        "billingCountry" => "GB"
//        "shippingAddress1" => "1 Shipping Street"
//        "shippingAddress2" => ""
//        "shippingCity" => "Shipping"
//        "shippingPostcode" => "SG78 8HJ"
//        "shippingState" => "Shippingshire"
//        "shippingCountry" => "GB"
//        "email" => "websupport@sterlingsolutions.co.uk"
//      ]
//    }
//  }
//  "amount" => "48.47"
//  "transactionId" => "VZiWZ3W3fZ8Z47xQ"
//  "returnUrl" => "http://printable.co.uk/payment/23/update"
//  "cancelUrl" => "http://printable.co.uk/payment/23/update"
//  "currency" => "GBP"
//  "description" => "Printable Order 23"
//]

        $response = $this->gateway->purchase($options)
                                  ->send();

        if ($response->isSuccessful())
        {
            // Sage Pay will never get here but just in case...
            $this->saveOrderTransactionReference($response);

            $this->orderRepository->updateStatus($this->order, 'paid', $this->statusRepository);

            return redirect()
                ->route('checkout.confirmation', [ 'payment' => $this->order->transaction_reference ])
                ->with('message', 'Payment Complete!');
        }
        elseif ($response->isRedirect())
        {
            $this->saveOrderTransactionReference($response);

            $response->redirect();
        }
        else
        {
            $this->saveOrderTransactionReference($response);

            $this->orderRepository->updateStatus($this->order, 'failed', $this->statusRepository);

            return redirect()
                ->route('payment.confirm')
                ->with('message', $response->getMessage());
        }
    }

public function update()
    {
        try
        {
            $response = $this->gateway->completePurchase([
                                                             'transactionReference' => $this->order->transaction_reference,
                                                             'transaction_id'       => $this->order->transaction_id
                                                         ])
                                      ->send();

            $this->statusRepository->firstOrCreate([ 'name' => $response->getStatus() ]);

            $this->orderRepository->updateStatus($this->order, $response->getStatus(), $this->statusRepository);

            $response->confirm(route('payment.confirm', $this->order->transaction_id));
        }
        catch (InvalidCreditCardException $e)
        {
            // Send "INVALID" response back to SagePay.
            $request = $this->gateway->completePurchase([ ]);

            $response = new ServerCompleteAuthorizeResponse($request, [ ]);

            $this->statusRepository->firstOrCreate([ 'name' => $response->getStatus() ]);

            $this->orderRepository->updateStatus($this->order, $response->getStatus(), $this->statusRepository);

            $response->invalid(route('payment.confirm', $this->order->transaction_id));
        }
    }

public function confirm()
    {
        switch (strtolower($this->order->status->name)) :
            case 'ok' :
                $this->orderRepository->updateStatus($this->order, 'paid', $this->statusRepository);
                return true;
                break;

            case 'rejected' :
                return false;
                break;
            case 'notauthed' :
                //do something to handle failed payments
                return false;
                break;

            case 'error' :
                //do something to handle errors
                return false;
                break;

            default:
                return false;
            //do something if it ever reaches here

        endswitch;

    }
judgej commented 9 years ago

I wouldn't put the transaction ID in the final confirm URL - keep that in the session where the user cannot mess with it. That is one vulnerable place where hackers can switch the IDs of small and large partial orders around, and end up getting a big purchase for a small amount, if you have not taken other steps to prevent this.

You can use notifyUrl() with the latest version, which offers a little more clarity and gets you ready for when returnUrl is removed. Does the same thing for now though. (Please ignore - also mixing up with Authorize.Net)

Is the "23" in your notify URL the transaction ID? It is also available through $_POST['VendorTxCode'] and more lately from:

$request = $this->gateway->completePurchase([...]);
$transaction_id = $request->getTransactionId();

You will need it to retrieve your order payment from the database, but you don't need to pass it into completePurchase(), as that method already knows how to get it from $_POST. Actually, ignore this - I am mixing it up with the Authorize.Net driver.

Beyond that, where is it actually failing? You can write messages to the log file in the "update" notify handler to see what is happening in there (Log::info('Here!')).

sterlingsolutions commented 9 years ago

I can't get inside the update method at all, I can get to $response->redirect() and I am redirected to SagePay and then when it tries to fire my URL I get the 5006.

I have put log calls in, but I omitted them from the code for clarity, but SagePay doesn't hit my endpoint : (

judgej commented 9 years ago

Have you checked the HTTP logs? Is it really not getting there?

sterlingsolutions commented 9 years ago

I'm running on Digital Ocean, I have checked the error and access logs for nginx and there is nothing there to suggest that SagePay has come back

judgej commented 9 years ago

Hmm, I can get to the URL, so something is up there. If you provide a complete invalid URL - just some random characters - does it get as far through the process? If SagePay did not think you had given it a URL, then it would not have reported a success at the first stage, so you should in theory not get a redirect.

I need to get home now, but I'll do a few tests tonight in case it is anything that has changed at the SagePay end.

sterlingsolutions commented 9 years ago

Same here, hopefully some fresh eyes tomorrow will help.

JoSquiders commented 9 years ago

Can't remember off the top of my head, but is Sagepay one of the merchants that requires you to add in your IP address to an allowed list? Just a thought that you might want to check that!

\ thinking about it, I think it gives you an invalid IP address error. Ignore me!

sterlingsolutions commented 9 years ago

I've done that, and it would return an Invalid IP response if it wasn't allowed, thanks though!

judgej commented 9 years ago

@JoSquiders yes it is. The IP address is the only way SagePay knows it is really your server that is sending the request. Other gateways use certificate-based hashing of that data, but SagePay does not (at the initial registration, at least).

However, it would be rejected right at the start if the IP was not right. I'm wondering, is SagePay really returning a valid "go ahead" response to that initial registration? If it is, then it is certainly happy with the details that it has been given, and happy with the IP address.

So after redirecting to SagePay, exactly what happens next? You should get a screen with a list of credit card icons. Click one of those and it takes you to the pre-filled personal details page. Only after completing that does SagePay try to contact your server. Is that the sequence that is being seen here?

JoSquiders commented 9 years ago

Had forgotten it returns the IP invalid error! Also missed that it is getting to the SagePay redirect, think I read that as it wasn't!

sterlingsolutions commented 9 years ago

@judgej I'm getting to the SagePay screen, I can select a card, and fill in the form, and then it goes to a confirmation screen; when I confirm, a progress bar appears, and then the 5006 error pops up after a while, not sure if it is timing out, I read in the SagePay docs it tries up to 10 times and then continues to retry but with a failed response instead.

I also read somewhere, that, sometimes, you need to allow access to the site via the firewall on your server, but I can't find the IP addresses I need to add anywhere. I pinged the site, but got 100% packet loss so wasn't sure if that was the right IP and I couldn't see that IP in my access log.

sterlingsolutions commented 9 years ago

@judgej Just following up to see if you were able to think of anything that could be causing this issue?

judgej commented 9 years ago

Yes, SagePay does try multiple times in rapid succession. I've seen that with a script returning a 500, in the HTTP logs. There are just a few seconds between each retry, for the first few at least. It will try again later, maybe five minutes or so, before it times out and sends a final "timed out" message to your notification URL.

So your problem: SagePay calls a URL and does not get a response it expects. It is either not calling the right URL, or your handler is not returning the correct response. Not sure if I've already asked, but is there anything in the HTTP or HTTPS access logs that show SagePay trying to access the notification URL? If not, could there be a firewall stopping it (I could access those URLs from my browser, so I would guess not). If access attempts are in those logs, then it narrows it down to what your script is actually doing.

If you are using HTTPS, then try HTTP. IIRC SagePay does not have a problem with self-signed SSL certificates (some gateways do - Authorize.Net hates them). But it is worth trying HTTP as a long-shot.

sterlingsolutions commented 9 years ago

I don't have SSL installed yet. I have tried several times whilst tailing the access log for nginx and nothing appears, we had an issue recently regarding HHVM, which we have installed on the server, have you heard of any conflicts between the OmniPay, SagePay, and HHVM? I'm wondering whether HHVM is interfering with the request at all? The firewall isn't an issue, it should accept the calls from SagePay.

judgej commented 9 years ago

No, no experience with that. SagePay is firing off notifications to the URL you give it. It is getting through to your server, or it isn't. There should be some log files somewhere on your server that tell you what is coming in over HTTP. This is what you primarily need to find out.

The SagePag test admin panel should also allow you to drill down to the response it gets. I'm sure I've seen that feature, but it's well hidden in the interface (you need to select failed transactions and there are some no-too-intuitive links through from that).

sterlingsolutions commented 9 years ago

For anyone who might read this in future, I uninstalled HHVM and this seemed to fix the problem, not sure what inside HHVM made this fail, but this is how I got round it.

sterlingsolutions commented 9 years ago

Thanks @judgej for your help!

judgej commented 9 years ago

Could there have been a layer of front-end caching? The notify is a POST message so I can't see how, but clutching at straws here. It would be nice to know the root reason for this problem, because I'm certain it won't be the last we see of it.

Thanks for letting us know.

sterlingsolutions commented 9 years ago

I don't think so, I cleared the cache a couple of times between tries to ensure javascript scripts were up to date, and the result was still the same.

I think the problem here was in the HHVM config, but I don't know enough about servers to really debug it.

thaithai21 commented 8 years ago

Hi JudJ you seems like a real expert on this subject and I was wondering if you can help me please. Thanks so much in advance.

After changing my website over to shared SSL, I have been trying to solve this 5006 error for the past few days with a developer. The error happened in the final stage after the 3D secure password. When my woocommerce site was http it seems fine, only when I made it https with a valid https certificate, I am getting this error 5006 and 500.

  I gave SagepayVPSTxld on a few failed transactions and their response is as below.   " Hi, I have investigated the all the log and each time the transaction are timing out due to the fact that we cannot connect to the following URL

http://www.mywebsite.com/?wc-api=woocommerce_sagepayserver

I would advise on investigating the URL in question to see if it is active and responsive."  

When I click on the above link I get this:   Status=INVALID StatusDetail=SagePay Server, No VendorTxCode posted. RedirectURL=https://www.mywebsite.com/wp-content/plugins/sagepay-server-gateway-for-woocommerce/includes/pages/redirect.php?page

All my plugin were fine and working, only when switch over to https. Can you help me and pinpoint the actual place where I have to put this url in? I mean should it be automatic? I never created this url.    I have done all the below too as advised by sagepay

Ensure you have opened ports 80 and 443 on your Servers to allow posts from Sage Pay. Ensure all Sage Pay IP Addresses have been added to your Server. Check any additional firewall or security measures will allow contact from the Sage Pay systems.

judgej commented 8 years ago

@thaithai21 Do you have any security plugins installed on your WP installation. Sometimes they can block URLs from some sources but not others, and that may be affected by the use of SSL.

Sage Pay is happy to respond to non-SSL URLs and self-signed URLs (other gateways are not happy to do this). Just something to bear in mind, in case the policy changes.

The status message you get back looks fine. Can you confirm the response - even with the error message - is a HTTP 200 and not a 40X? Sage Pay Server always expects a 200 response, no matter what the status.

Could you try a POST to that same notify URL. You should get the same result as with the GET. Sage Pay will send a POST. Firebug, or some online tools will be able to do that for you. I've seen security plugins treat GET and POST to the same URL differently before.

thaithai21 commented 8 years ago

Hi @judgej, Thanks for the reply. I manage to solve the problem last night. Finally got through to blue host as I have site lock on. So yes you are right it is some kind of security issue. They also re-validate my SSL certificate when I moved over to their wordpress hosting server. Not sure exactly what they have done but it seems to wok now. Thanks so much for the response anyway, the stuff that you know you should be a partner of sagepay ;-)

judgej commented 8 years ago

I'm closing this as the original issue has been resolved. The details will hang around and should probably go into documentation at some point.

CybroOdoo commented 8 years ago

Hi @judgej, Thanks for your post. I am trying to integrate SagePay with Odoo ecommerce (Odoo is an opensource erp). I got notification post to NotificationURL. The document of Sgepay says to send ReturnURL as a response to this post. But I dont know how to send response without knowing the url the post came from.

Here is my code in Python:

@http.route('/payment/sagepay/complete', type='json', auth="public", methods=['POST', 'GET'])

def sagepay_complete(self, **post):

cr, uid, context = request.cr, SUPERUSER_ID, request.context

validate_url = 'https://test.sagepay.com/gateway/service/vspserver-register.vsp'

base_url = request.httprequest.host_url

return_url = '%s' % urlparse.urljoin(base_url, '/payment/paysage')

reply_values = '''Status=OK

RedirectURL=%s

StatusDetail=0000 : The Authorisation was Successful.''' % return_url

return reply_values

@http.route('/payment/paysage', type='http', auth="none", methods=['POST', 'GET'])

def sagepay_complete_redirect(self, **post):

cr, uid, context = request.cr, SUPERUSER_ID, request.context

res = self.sagepay_validate_data(**post)

print "=sagepay_complete==res===",res

return_url = self._get_return_url(res, **post)

return werkzeug.utils.redirect(return_url)

Here first url '/payment/sagepay/complete' is the NotificationURL. I got response to this url. I returned the response with RedirectURL '/payment/paysage'. But it always gives the error 5006. Please guide me if I am missing something.

judgej commented 8 years ago

This probably isn't the place to be asking Python questions, but I'll try to answer generically. Sage have offloaded their main developer support questions to Stack Overflow (the cheapskates) so that is where you will find the best Sage Pay answers. Please move the question over to there if there are specific technical questions.

When you first set up the Sage Pay Server transaction, you give the gateway the notification URL to send the results to. You can set any URL you like, SSL or not, and that can include GET parameters and a path to give your notification handler some context.

In your notification handler you must tell Sage Pay (using the format you have already identified in your post above) where to send the user next. IMO that URL should be the same place regardless of the status of the transaction (OK, FAILED or ERROR) - don't put anything into that URL that can be used as an attack vector if the end user fiddles with it.

So what is that URL? That is up to you. In your handler you have:

This information will be enough to be able to construct a URL to send the user to.

When the user to returned to the site front end (through that URL) the first thing to look at is the session - you should have stored the state of the transaction there (your internal session ID), so on return your application knows where to pick up from. Then you can use that to fetch the transaction result that was stored in the database in the notification handler.

That, in a nutshell, is how the notification handler for Sage Pay Server is used. Sage Pay Direct (needing more PCI certification) and Sage Pay REST (a very new gateway which only came out of beta this week) do not use a notification handler.

andyiwest commented 6 years ago

Had this issue as well, my problem lied in the fact we had just installed an SSL onto the domain, and hadn't changed;

$strPost=$strPost . "&NotificationURL=https://" . $_SERVER['HTTP_HOST'] . "/sagepay";

Why not have the system detect if it's http or https itself, eliminates this kind of problem