Description
Summary
I've read many of the SameSite issues and possible solutions / workarounds on the repo issues, however so far the only real way of making this work is setting SameSite to None.
My question is about how to make it work with Lax mode and how Paypal integration works.
I've been checking the paypal module and it seems to preserve the session for the logged in user, it uses this plugin
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\Paypal\Plugin;
use Magento\Framework\App\Request\Http;
use Magento\Framework\Session\SessionStartChecker;
/**
* Intended to preserve session cookie after submitting POST form from PayPal to Magento controller.
*/
class TransparentSessionChecker
{
/**
* @var string[]
*/
private $disableSessionUrls = [
'paypal/transparent/redirect',
'paypal/payflowadvanced/returnUrl',
'paypal/payflow/returnUrl',
'paypal/hostedpro/return',
];
/**
* @var Http
*/
private $request;
/**
* @param Http $request
*/
public function __construct(
Http $request
) {
$this->request = $request;
}
/**
* Prevents session starting while instantiating PayPal transparent redirect controller.
*
* @param SessionStartChecker $subject
* @param bool $result
* @return bool
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function afterCheck(SessionStartChecker $subject, bool $result): bool
{
if ($result === false) {
return false;
}
foreach ($this->disableSessionUrls as $url) {
if (strpos((string)$this->request->getPathInfo(), $url) !== false) {
return false;
}
}
return true;
}
}
This effectively makes the session not be destroyed and the user stays logged in.
However on the initial controller which handles the response in my case
cardlink_checkout/payment/response
When fetching the CheckoutSession via Factory or via Proxy or Plain with the class through DI, it's an empty session.
I've read further into the paypal integration and saw that it uses an intermediate redirect by intercepting the initial POST request
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\Paypal\Controller\Transparent;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\Action\HttpPostActionInterface;
use Magento\Framework\App\CsrfAwareActionInterface;
use Magento\Framework\App\Request\InvalidRequestException;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\View\Result\LayoutFactory;
use Magento\Payment\Model\Method\Logger;
use Magento\Paypal\Model\Payflow\Transparent;
/**
* Class for redirecting the Paypal response result to Magento controller.
*/
class Redirect extends Action implements CsrfAwareActionInterface, HttpPostActionInterface
{
/**
* @var LayoutFactory
*/
private $resultLayoutFactory;
/**
* @var Transparent
*/
private $transparent;
/**
* @var Logger
*/
private $logger;
/**
* @param Context $context
* @param LayoutFactory $resultLayoutFactory
* @param Transparent $transparent
* @param Logger $logger
*/
public function __construct(
Context $context,
LayoutFactory $resultLayoutFactory,
Transparent $transparent,
Logger $logger
) {
$this->resultLayoutFactory = $resultLayoutFactory;
$this->transparent = $transparent;
$this->logger = $logger;
parent::__construct($context);
}
/**
* @inheritdoc
*/
public function createCsrfValidationException(
RequestInterface $request
): ?InvalidRequestException {
return null;
}
/**
* @inheritdoc
*/
public function validateForCsrf(RequestInterface $request): ?bool
{
return true;
}
/**
* Saves the payment in quote
*
* @return ResultInterface
* @throws LocalizedException
*/
public function execute()
{
$gatewayResponse = (array)$this->getRequest()->getPostValue();
$this->logger->debug(
['PayPal PayflowPro redirect:' => $gatewayResponse],
$this->transparent->getDebugReplacePrivateDataKeys(),
$this->transparent->getDebugFlag()
);
$resultLayout = $this->resultLayoutFactory->create();
$resultLayout->addDefaultHandle();
$resultLayout->getLayout()->getUpdate()->load(['transparent_payment_redirect']);
return $resultLayout;
}
}
Which effectively renders a form which submits again the same data but from the same domain
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
/** @var \Magento\Payment\Block\Transparent\Redirect $block */
$params = $block->getPostParams();
$redirectUrl = $block->getRedirectUrl();
?>
<html>
<head></head>
<body onload="document.forms['proxy_form'].submit()">
<form id="proxy_form" action="<?= $block->escapeUrl($redirectUrl) ?>"
method="POST" hidden enctype="application/x-www-form-urlencoded" class="no-display">
<?php foreach ($params as $name => $value):?>
<input value="<?= $block->escapeHtmlAttr($value) ?>" name="<?= $block->escapeHtmlAttr($name) ?>" type="hidden"/>
<?php endforeach?>
</form>
</body>
</html>
the URL is declared in di.xml
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/layout_generic.xsd">
<container name="root" label="Root">
<block class="Magento\Payment\Block\Transparent\Redirect" name="transparent_redirect" template="Magento_Payment::transparent/redirect.phtml">
<arguments>
<argument name="route_path" xsi:type="string">paypal/transparent/response</argument>
</arguments>
</block>
</container>
</layout>
However when reaching the response controller equivalent in the module I'm trying to patch, I still can't get the CheckoutSession properly and it's entirely empty.
Furthermore, when setting the data to it for the LastSuccessQuoteId and redirecting to checkout/onepage/success
controller, I notice that there, the checkoutSession is restored entirely to the previous state, before my modification. Thus because the LastSuccessQuoteId is not there in that state, I'm redirected to cart instead.
So I'm assuming here that something extra is being done in the paypal gateway to retain the actual CheckoutSession and modify that one instead of creating a new empty one.
However I'm unable to find any type of documentation regarding this. In the official docs, there's no mention of how to tackle SameSite and also it is left in the responsibility of each payment gateway to fix it. However this is a problem for Magento itself and us developers trying to fix the gateway modules without nuking the settings to make the cookies set Samesite to "None"
As far as I can see in the network tab, the PHPSESSID is there in all requests, but the cookie isn't.
On redirecting to payment provider
On redirecting to the response handler which creates the "proxy" form as expected nothing is there
On the actual processor everything is there except the PHPSESSID cookie for some reason in the response and in the request the PHPSESS ID is actually there
And on the checkout/onepage/success
controller which can also see the original CheckoutSession I can see everything there as well.
So my question is what's the missing step to actually fetch the proper CheckoutSession after the "proxy" form redirects to the actual processor.
Assuming of course what paypal module shows in transparent/redirect
classes / phtml files is the correct approach.
If not, what's the correct approach to achieve this while still using SameSite = Lax
.
Examples
here's an example module provided by Cardlink a major payment gateway in Greece.
Their proposal was to change SameSite to "None" as well.
I'm particularly curious as to why the information for the session is transferred properly when redirecting from the responseProcessor
to checkout/onepage/success
controller on redirect 2 while not transferred properly when redirecting from the response
controller to the responseProcessor
on redirect 1 because if the information is there in redirect 2, why wouldn't it be available in redirect 1?
Any help is appreciated
Proposed solution
No response
Release note
No response
Triage and priority
- Severity: S0 - Affects critical data or functionality and leaves users without workaround.
- Severity: S1 - Affects critical data or functionality and forces users to employ a workaround.
- Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
- Severity: S3 - Affects non-critical data or functionality and does not force users to employ a workaround.
- Severity: S4 - Affects aesthetics, professional look and feel, “quality” or “usability”.