custom/plugins/CioSso/src/Controller/SsoController.php line 145

Open in your IDE?
  1. <?php
  2. namespace CioSso\Controller;
  3. use CioSso\Service\Sso;
  4. use Psr\Log\LoggerInterface;
  5. use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
  6. use Shopware\Core\Checkout\Customer\CustomerEntity;
  7. use Shopware\Core\Framework\Api\Context\AdminApiSource;
  8. use Shopware\Core\Framework\Context;
  9. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  12. use Shopware\Core\Framework\Routing\Annotation\RouteScope;
  13. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  14. use Shopware\Core\System\User\UserEntity;
  15. use Shopware\Storefront\Controller\StorefrontController;
  16. use Symfony\Component\HttpFoundation\RedirectResponse;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\Routing\Annotation\Route;
  20. class SsoController extends StorefrontController
  21. {
  22.     private Sso $sso;
  23.     private ?LoggerInterface $logger;
  24.     private EntityRepositoryInterface $userRepository;
  25.     public function __construct(
  26.         Sso $sso,
  27.         EntityRepositoryInterface $userRepository
  28.     ) {
  29.         $this->sso $sso;
  30.         $this->userRepository $userRepository;
  31.     }
  32.     /**
  33.      * @RouteScope(scopes={"storefront"})
  34.      * @Route("/PraeTbSso/signOn", name="frontend.sso.signon")
  35.      */
  36.     public function signon(Request $requestSalesChannelContext $context): RedirectResponse
  37.     {
  38.         $userId $request->get('custExtenrId');
  39.         $timestamp $request->get('timestamp');
  40.         $hash $request->get('hash');
  41.         $newHash $this->sso->generateHash($userId$timestamp);
  42.         if ($this->sso->validateHash($hash$newHash)) {
  43.             if ($this->sso->validateTimestamp($timestamp)) {
  44.                 if ($customer $this->sso->getCustomer($userId$context)) {
  45.                     $cart $this->sso->login($customer$context);
  46.                     $this->addCartErrors($cart);
  47.                     if ($this->sso->validateCustomerEmail($customer)) {
  48.                         return $this->redirectToRoute('frontend.home.page');
  49.                     } else {
  50.                         return $this->redirectToRoute('frontend.loginpage.sso.email');
  51.                     }
  52.                 }
  53.             }
  54.         }
  55.         return $this->redirectToRoute('frontend.account.login.page');
  56.     }
  57.     /**
  58.      * @RouteScope(scopes={"storefront"})
  59.      * @Route("/loginpage/sso/email", name="frontend.loginpage.sso.email")
  60.      */
  61.     public function loginpage(Request $requestSalesChannelContext $context)
  62.     {
  63.         if (!$context->getCustomer() instanceof CustomerEntity) {
  64.             throw new CustomerNotLoggedInException();
  65.         }
  66.         if (strtoupper($request->getMethod()) === 'POST') {
  67.             $tmpCustomer = new CustomerEntity();
  68.             $tmpCustomer->setEmail(
  69.                 $request->request->get('email')
  70.             );
  71.             $tmpCustomer->setCustomerNumber(
  72.                 $context->getCustomer()->getCustomerNumber()
  73.             );
  74.             if ($request->request->get('email') && $this->sso->validateCustomerEmail($tmpCustomer)) {
  75.                 /** @var EntityRepositoryInterface $customerRepository */
  76.                 $customerRepository $this->container->get('customer.repository');
  77.                 $checkEmailCustomerCriteria = new Criteria();
  78.                 $checkEmailCustomerCriteria->addFilter(new EqualsFilter('email'$request->request->get('email')));
  79.                 $checkEmailCustomer $customerRepository->search($checkEmailCustomerCriteria$context->getContext())->first();
  80.                 if ($checkEmailCustomer instanceof CustomerEntity) {
  81.                     return $this->renderStorefront('@CioSso/storefront/page/sso/missingemail.html.twig', ['uniqueMailError' => true'oldMail' => $request->request->get('email')]);
  82.                 }
  83.                 $customerRepository->update([
  84.                     [
  85.                         'id' => $context->getCustomer()->getId(),
  86.                         'email' => $request->request->get('email')
  87.                     ]
  88.                 ], $context->getContext());
  89.                 return $this->redirectToRoute('frontend.home.page');
  90.             }
  91.         }
  92.         if ($this->sso->validateCustomerEmail($context->getCustomer())) {
  93.             return $this->redirectToRoute('frontend.home.page');
  94.         }
  95.         return $this->renderStorefront('@CioSso/storefront/page/sso/missingemail.html.twig');
  96.     }
  97.     /**
  98.      * @RouteScope(scopes={"storefront"})
  99.      * @Route("/sso/media/fallback", name="frontend.media.fallback")
  100.      */
  101.     public function media(Request $requestSalesChannelContext $context): Response
  102.     {
  103.         $path $request->query->get('path');
  104.         // Prüfen ob ein Kunde eingeloggt ist
  105.         if (!is_null($context->getCustomer()) && !empty($path)) {
  106.             return $this->serveMediaFile($path);
  107.         }
  108.         // Wenn kein Kunde eingeloggt ist, zur Admin-Route weiterleiten
  109.         // um dort zu prüfen ob ein Admin eingeloggt ist
  110.         if (!empty($path)) {
  111.             return $this->redirectToRoute('admin.media.fallback', ['path' => $path]);
  112.         }
  113.         return $this->redirectToRoute('frontend.account.login.page');
  114.     }
  115.     /**
  116.      * @RouteScope(scopes={"administration"})
  117.      * @Route("/admin/sso/media/fallback", name="admin.media.fallback", defaults={"auth_required"=false})
  118.      */
  119.     public function adminMedia(Request $request): Response
  120.     {
  121.         $path $request->query->get('path');
  122.         // Admin Bearer Token validieren
  123.         if (!$this->validateAdminBearerToken($request)) {
  124.             // Bei ungültigem Token zum Customer Login weiterleiten
  125.             return $this->redirectToRoute('frontend.account.login.page');
  126.         }
  127.         // Bei gültigem Admin Token die Media-Datei ausliefern
  128.         if (!empty($path)) {
  129.             return $this->serveMediaFile($path);
  130.         }
  131.         return new Response('Path parameter required'Response::HTTP_BAD_REQUEST);
  132.     }
  133.     /**
  134.      * Validiert den Admin Bearer Token mit Shopware Services
  135.      *
  136.      * @param Request $request HTTP Request
  137.      * @return bool True wenn Token gültig ist, false sonst
  138.      */
  139.     private function validateAdminBearerToken(Request $request): bool
  140.     {
  141.         try {
  142.             // 1. Authorization Header extrahieren (Shopware Standard)
  143.             $authHeader $request->headers->get('Authorization');
  144.             if (!$authHeader) {
  145.                 // Fallback auf Cookie (spezielle Anforderung)
  146.                 $authHeader $request->cookies->get('bearerAuth');
  147.             }
  148.             if (!$authHeader) {
  149.                 return false;
  150.             }
  151.             // 2. Bearer Token extrahieren (Shopware Standard Regex)
  152.             $jwt trim(preg_replace('/^(?:\s+)?Bearer\s/'''$authHeader) ?? '');
  153.             if (empty($jwt)) {
  154.                 return false;
  155.             }
  156.             // 3. Token mit Shopware Services validieren
  157.             return $this->validateTokenWithShopwareServices($jwt);
  158.         } catch (\Exception $e) {
  159.             return false;
  160.         }
  161.     }
  162.     /**
  163.      * Validiert JWT Token mit Shopware Services
  164.      *
  165.      * @param string $jwt JWT Token
  166.      * @return bool True wenn Token gültig ist
  167.      */
  168.     private function validateTokenWithShopwareServices(string $jwt): bool
  169.     {
  170.         try {
  171.             // JWT Payload dekodieren um User ID zu extrahieren
  172.             $payload $this->decodeJwtPayload($jwt);
  173.             if (!$payload || !isset($payload['sub'])) {
  174.                 return false;
  175.             }
  176.             // Token Ablaufzeit prüfen
  177.             if (isset($payload['exp']) && $payload['exp'] < time()) {
  178.                 return false;
  179.             }
  180.             // AdminApiSource mit User ID erstellen
  181.             $source = new AdminApiSource($payload['sub']);
  182.             $context = new Context($source);
  183.             // Admin User validieren
  184.             return $this->validateAdminUser($payload['sub'], $context);
  185.         } catch (\Exception $e) {
  186.             return false;
  187.         }
  188.     }
  189.     /**
  190.      * Dekodiert JWT Payload ohne Signatur-Validierung
  191.      *
  192.      * @param string $jwt JWT Token
  193.      * @return array|null Dekodiertes Payload oder null bei Fehler
  194.      */
  195.     private function decodeJwtPayload(string $jwt): ?array
  196.     {
  197.         try {
  198.             $parts explode('.'$jwt);
  199.             if (count($parts) !== 3) {
  200.                 return null;
  201.             }
  202.             // Base64 URL-Safe Dekodierung
  203.             $payload base64_decode(str_replace(['-''_'], ['+''/'], $parts[1]));
  204.             if (!$payload) {
  205.                 return null;
  206.             }
  207.             $decoded json_decode($payloadtrue);
  208.             return is_array($decoded) ? $decoded null;
  209.         } catch (\Exception $e) {
  210.             return null;
  211.         }
  212.     }
  213.     /**
  214.      * Validiert Admin User mit Shopware User Repository
  215.      *
  216.      * @param string $userId User ID aus JWT Token
  217.      * @param Context $context Shopware Context
  218.      * @return bool True wenn User gültig und aktiv ist
  219.      */
  220.     private function validateAdminUser(string $userIdContext $context): bool
  221.     {
  222.         try {
  223.             $criteria = new Criteria([$userId]);
  224.             $user $this->userRepository->search($criteria$context)->first();
  225.             if (!$user instanceof UserEntity) {
  226.                 return false;
  227.             }
  228.             // User muss aktiv sein
  229.             if (!$user->getActive()) {
  230.                 return false;
  231.             }
  232.             return true;
  233.         } catch (\Exception $e) {
  234.             return false;
  235.         }
  236.     }
  237.     /**
  238.      * Sichere Auslieferung von Media-Dateien aus dem public/media Verzeichnis
  239.      *
  240.      * @param string $path Relativer Pfad zur Media-Datei
  241.      * @return Response HTTP Response mit der Datei oder 404 wenn nicht gefunden
  242.      */
  243.     private function serveMediaFile(string $path): Response
  244.     {
  245.         // Sicherheitsprüfung: Pfad muss innerhalb von public/media liegen
  246.         $mediaBasePath realpath('public/media');
  247.         $requestedPath 'public/media/' ltrim($path'/');
  248.         $resolvedPath realpath($requestedPath);
  249.         // Prüfen ob der aufgelöste Pfad innerhalb des erlaubten Verzeichnisses liegt
  250.         if ($resolvedPath === false || strpos($resolvedPath$mediaBasePath) !== 0) {
  251.             return new Response('File not found'Response::HTTP_NOT_FOUND);
  252.         }
  253.         // Prüfen ob die Datei existiert
  254.         if (!file_exists($resolvedPath)) {
  255.             return new Response('File not found'Response::HTTP_NOT_FOUND);
  256.         }
  257.         // Datei laden und ausliefern
  258.         $fileContent file_get_contents($resolvedPath);
  259.         $fileName basename($resolvedPath);
  260.         return new Response($fileContentResponse::HTTP_OK, [
  261.             'Content-Type' => mime_content_type($resolvedPath),
  262.             'Content-Disposition' => 'filename=' $fileName
  263.         ]);
  264.     }
  265. }