Kunden nach Custom-Login als eingeloggt zur Startseite weiterleiten

Hallo ihr Lieben,

da die meisten Beiträge zu dem Thema schon älter sind oder leider unbeantwortet, wollte ich hier noch einmal nachfragen, da ich da aktuell echt stuck bin.

Problem: Wir haben einen Kunden, der seinen Kunden einen Login auf einer Seite außerhalb seines Shopware-Shops anbietet. Die geben dort ihre E-Mail-Adresse und ihr Passwort ein und werden - sofern erfolgreich - zum Shop weitergeleitet und sind dort eingeloggt. Zumindest in SW5.

In Shopware 6 kriege ich das ganze nicht ans Laufen.

$client = new Client();
            $response = $client->request('POST', $this->STORE_API . '/account/login', [
                'headers' => [
                    'Content-Type' => 'application/json',
                    'Accept' => 'application/json',
                    'sw-access-key' => $this->SALES_CHANNEL_TOKEN
                ],
                'json' => [
                    'username' => $username,
                    'password' => $password,
                ]
            ]);

Nutzername und Passwort werden verschlüsselt als URL-Queries mit gegeben. Die habe ich davor schon ausgelesen und mache anschließend einen API-Call an /account/login. Die Response sieht auch richtig aus, ich kriege im Header das Context-Token und im Body folgendes:

"body": "{\"apiAlias\":\"array_struct\",\"redirectUrl\":null}"

Erst dachte ich, alles klar dann returne ich eine RedirectResponse und setze im Header das Context-Token.

            if ($response->getStatusCode() == 200) {
                return new RedirectResponse('/', 302, [
                    'sw-context-token' => $response->getHeader('sw-context-token')[0],
                ]);
            }

Das leitet mich zwar zum Shop weiter, allerdings bin ich dort nicht eingeloggt.

In der API-Dokumentation steht:

Returns the context token. Use that as your sw-context-token header for subsequent requests. Redirect if getRedirectUrl is set.

Aber ich konnte bisher nirgendwo finden, was mit „if getRedirectUrl is set“ gemeint ist. Wo wird das gesetzt, während der Request?

Wäre mega nice, wenn da jemand einen Denkanstoß für mich hätte, weil ich hier seit 3 Tagen nicht weiterkomme und nicht mehr weiß, was ich noch ausprobieren kann.

Liebe Grüße

Hallo,

bitte mal so probieren:

if ($response->getStatusCode() == 200) {

        $url = "/"; // alternativ APP_URL Umgebungsvariable einfügen

        $response->setContent(
            sprintf('<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=\'%1$s\'" />

        <title>Redirecting to %1$s</title>
    </head>
    <body>
        Redirecting to <a href="%1$s">%1$s</a>.
    </body>
</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8')));

        $response->headers->set('Location', $url);
        $response->headers->set('Content-Type', 'text/html; charset=utf-8');
}

Hey, vielen Dank schon mal für die Hilfe.

Darf ich fragen, welche Klassen du für den Client und die Response verwendest?
Ich hatte vorher GuzzleHTTP für die Request benutzt. Die Response unterstützt zwar die von dir verwendeten Methoden nicht, ich hatte dann aber eine neue Symfony\Component\HttpFoundation\Response erstellt.

$url = '/';
$htmlContent = sprintf('<!DOCTYPE html>

<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url=\'%1$s\'" />
        <title>Redirecting to %1$s</title>
    </head>
    <body>
        Redirecting to <a href="%1$s">%1$s</a>.
    </body>
</html>', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8'));

return new Response($htmlContent, 302, array_merge(
    $response->getHeaders(),
    [
        'Location' => $url,
        'Content-Type' => 'text/html; charset=utf-8'
    ]
));

Außerdem hat ich gerade einmal Symfony\Component\HttpClient\HttpClient anstatt Guzzle verwendet und bei beiden versucht, die Originalheader an die neue Response weiterzugeben. Die Weiterleitung funktioniert zwar, allerdings ohne den eingeloggten Status.

Die Originalresponse von Shopware enthält ein Set-Cookie: sw-states=logged-in; welches nach dem Redirect auf deleted gesetzt wird.

Ich verwende den in Shopware verwendeten use Symfony\Component\HttpFoundation\Response

Aber ich mache das nicht über XHR Request, vllt liegt da der Hase begraben?

Um den Response anzuzapfen nutze ich einen Subscriber.

<?php declare(strict_types=1);

class KernelEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => 'onResponse'
        ];
    }

    public function onResponse(ResponseEvent $event): void
    {
    }
}

Mir ist auch noch aufgefallen, dass bei manchen Shopware Instanzen der sw_states Cookie komplett wegfällt. Aber der Session Cookie sollte auf jeden Fall gesetzt sein.

1 „Gefällt mir“

Schon mal vielen Dank für die Infos, ich werde das mal testen und dann berichten.

Ok, sorry aber ich stehe da echt auf dem Schlauch. Wie genau handled dein Subscriber den Loginvorgang?

Ich dachte jetzt, okay ich schaue, ob die Response eine von meiner Route ist und sorge dann dafür, dass der Loginstate richtig gesetzt wird:

class RequestSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::RESPONSE => 'onResponse',
        ];
    }

    public function onResponse(ResponseEvent $event): void
    {
        $response = $event->getResponse();
        $request = $event->getRequest();

        if ($response->isRedirection() && $request->attributes->get('_route') === 'frontend.custom_login') {
            $response->headers->setCookie(
                new Cookie('sw-states', 'logged-in', strtotime('+1 hour'), '/', null, true, true)
            );
            $response->headers->setCookie(new Cookie('test', 'test'));
        }
    }
}

Aber der sw-states Cookie wird nach wie vor in der finalen Response deleted.

Oder meintest du, ich schau direkt im Subscriber ob eine Request Logindaten enthält und handle den Login und finalen Redirect dann direkt im Subscriber ohne eine XHR-Request an die store-api zu senden, was mein Controller momentan tut?

Hast du da ein funktionierendes Beispiel für mich?

Hier mal ein Beispiel, wie ich ein Cookie erstelle.

    private function createCookie(Request $request, string $customerSessionToken, int $days = 7): Cookie
    {
        $cookie = Cookie::create(MoorlCustomerSession::COOKIE_TOKEN_NAME, $customerSessionToken);
        $cookie->setSecureDefault($request->isSecure());
        $cookie = $cookie->withExpires(time() + 60 * 60 * 24 * $days);
        return $cookie->withHttpOnly();
    }

Aber der State-Cookie reicht nicht! Bzw ist auch garnicht notwendig.

Hier ein Auszug, wie ein Login aussehen sollte/könnte

            $contextToken = $this->accountService->loginById($customerSession->getCustomerId(), $this->salesChannelContext);

            if (!$this->systemConfigService->getBool('MoorlCustomerSession.config.disableRedirect', $salesChannelId)) {
                /* timeout */
                $redirectTimeout = $this->systemConfigService->getInt('MoorlCustomerSession.config.redirectTimeout', $salesChannelId);
                sleep($redirectTimeout);

                /* get request url */
                $url = $request->attributes->get(RequestTransformer::STOREFRONT_URL) . $request->attributes->get(RequestTransformer::SALES_CHANNEL_RESOLVED_URI);

                /* convert to redirect response */
                $redirectResponse = new RedirectResponse($url);
                $response->setContent($redirectResponse->getContent());
                $response->setStatusCode($redirectResponse->getStatusCode());
                $response->headers = $redirectResponse->headers;
            }

            $response->headers->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $contextToken);
1 „Gefällt mir“

Yess, danke sehr.

Das sw-access-token aus der API-Anfrage an /store-api/account/login hat nicht funktioniert, aber wenn ich das Token nehme, das mir die AccountService-Klasse mit der loginByCredentials-Methode zurückgibt, funktioniert es.

Funktionierende Lösung:

Subscriber:

public function onResponse(ResponseEvent $event): void
    {
        $response = $event->getResponse();
        $request = $event->getRequest();

        if ($request->attributes->get('_route') === 'frontend.custom_login') {
            
            $url = $request->attributes->get(RequestTransformer::STOREFRONT_URL . RequestTransformer::SALES_CHANNEL_RESOLVED_URI) ?: "/";
            $salesChannelContext = $request->attributes->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT);
            $contextToken = $this->getContextToken($request, $salesChannelContext);

            $redirectResponse = new RedirectResponse($url);
            $response->setContent($redirectResponse->getContent());
            $response->setStatusCode($redirectResponse->getStatusCode());
            $response->headers = $redirectResponse->headers;
            $response->headers->set(PlatformRequest::HEADER_CONTEXT_TOKEN, $contextToken);
        }
    }

ContextToken beziehen:

private function getContextToken($request, $salesChannelContext) 
   {
        $username = $this->getDecryptedParam($request->query->get('id'));
        $password = $this->getDecryptedParam($request->query->get('p'));

        $contextToken = $this->accountService->loginByCredentials($username, $password, $salesChannelContext);

        return $contextToken;
    }

Und der Controller, der im Prinzip nichts macht, aber dessen Route wir im Subscriber überprüfen, um dort dann die entsprechende Logik auszuführen:

#[Route(defaults: ['csrf_protected' => false, '_routeScope' => ['storefront']])]
class CustomLoginController extends StorefrontController
{
    #[Route(path: '/CustomLogin', name: 'frontend.custom_login', defaults: ['csrf_protected' => false, '_routeScope' => ['storefront']], methods: ['GET'])]
    public function index(Request $request): void
    {
        
    }
}
1 „Gefällt mir“