<?php
namespace CioSso\Controller;
use CioSso\Service\Sso;
use Psr\Log\LoggerInterface;
use Shopware\Core\Checkout\Cart\Exception\CustomerNotLoggedInException;
use Shopware\Core\Checkout\Customer\CustomerEntity;
use Shopware\Core\Framework\Api\Context\AdminApiSource;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\User\UserEntity;
use Shopware\Storefront\Controller\StorefrontController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SsoController extends StorefrontController
{
private Sso $sso;
private ?LoggerInterface $logger;
private EntityRepositoryInterface $userRepository;
public function __construct(
Sso $sso,
EntityRepositoryInterface $userRepository
) {
$this->sso = $sso;
$this->userRepository = $userRepository;
}
/**
* @RouteScope(scopes={"storefront"})
* @Route("/PraeTbSso/signOn", name="frontend.sso.signon")
*/
public function signon(Request $request, SalesChannelContext $context): RedirectResponse
{
$userId = $request->get('custExtenrId');
$timestamp = $request->get('timestamp');
$hash = $request->get('hash');
$newHash = $this->sso->generateHash($userId, $timestamp);
if ($this->sso->validateHash($hash, $newHash)) {
if ($this->sso->validateTimestamp($timestamp)) {
if ($customer = $this->sso->getCustomer($userId, $context)) {
$cart = $this->sso->login($customer, $context);
$this->addCartErrors($cart);
if ($this->sso->validateCustomerEmail($customer)) {
return $this->redirectToRoute('frontend.home.page');
} else {
return $this->redirectToRoute('frontend.loginpage.sso.email');
}
}
}
}
return $this->redirectToRoute('frontend.account.login.page');
}
/**
* @RouteScope(scopes={"storefront"})
* @Route("/loginpage/sso/email", name="frontend.loginpage.sso.email")
*/
public function loginpage(Request $request, SalesChannelContext $context)
{
if (!$context->getCustomer() instanceof CustomerEntity) {
throw new CustomerNotLoggedInException();
}
if (strtoupper($request->getMethod()) === 'POST') {
$tmpCustomer = new CustomerEntity();
$tmpCustomer->setEmail(
$request->request->get('email')
);
$tmpCustomer->setCustomerNumber(
$context->getCustomer()->getCustomerNumber()
);
if ($request->request->get('email') && $this->sso->validateCustomerEmail($tmpCustomer)) {
/** @var EntityRepositoryInterface $customerRepository */
$customerRepository = $this->container->get('customer.repository');
$checkEmailCustomerCriteria = new Criteria();
$checkEmailCustomerCriteria->addFilter(new EqualsFilter('email', $request->request->get('email')));
$checkEmailCustomer = $customerRepository->search($checkEmailCustomerCriteria, $context->getContext())->first();
if ($checkEmailCustomer instanceof CustomerEntity) {
return $this->renderStorefront('@CioSso/storefront/page/sso/missingemail.html.twig', ['uniqueMailError' => true, 'oldMail' => $request->request->get('email')]);
}
$customerRepository->update([
[
'id' => $context->getCustomer()->getId(),
'email' => $request->request->get('email')
]
], $context->getContext());
return $this->redirectToRoute('frontend.home.page');
}
}
if ($this->sso->validateCustomerEmail($context->getCustomer())) {
return $this->redirectToRoute('frontend.home.page');
}
return $this->renderStorefront('@CioSso/storefront/page/sso/missingemail.html.twig');
}
/**
* @RouteScope(scopes={"storefront"})
* @Route("/sso/media/fallback", name="frontend.media.fallback")
*/
public function media(Request $request, SalesChannelContext $context): Response
{
$path = $request->query->get('path');
// Prüfen ob ein Kunde eingeloggt ist
if (!is_null($context->getCustomer()) && !empty($path)) {
return $this->serveMediaFile($path);
}
// Wenn kein Kunde eingeloggt ist, zur Admin-Route weiterleiten
// um dort zu prüfen ob ein Admin eingeloggt ist
if (!empty($path)) {
return $this->redirectToRoute('admin.media.fallback', ['path' => $path]);
}
return $this->redirectToRoute('frontend.account.login.page');
}
/**
* @RouteScope(scopes={"administration"})
* @Route("/admin/sso/media/fallback", name="admin.media.fallback", defaults={"auth_required"=false})
*/
public function adminMedia(Request $request): Response
{
$path = $request->query->get('path');
// Admin Bearer Token validieren
if (!$this->validateAdminBearerToken($request)) {
// Bei ungültigem Token zum Customer Login weiterleiten
return $this->redirectToRoute('frontend.account.login.page');
}
// Bei gültigem Admin Token die Media-Datei ausliefern
if (!empty($path)) {
return $this->serveMediaFile($path);
}
return new Response('Path parameter required', Response::HTTP_BAD_REQUEST);
}
/**
* Validiert den Admin Bearer Token mit Shopware Services
*
* @param Request $request HTTP Request
* @return bool True wenn Token gültig ist, false sonst
*/
private function validateAdminBearerToken(Request $request): bool
{
try {
// 1. Authorization Header extrahieren (Shopware Standard)
$authHeader = $request->headers->get('Authorization');
if (!$authHeader) {
// Fallback auf Cookie (spezielle Anforderung)
$authHeader = $request->cookies->get('bearerAuth');
}
if (!$authHeader) {
return false;
}
// 2. Bearer Token extrahieren (Shopware Standard Regex)
$jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $authHeader) ?? '');
if (empty($jwt)) {
return false;
}
// 3. Token mit Shopware Services validieren
return $this->validateTokenWithShopwareServices($jwt);
} catch (\Exception $e) {
return false;
}
}
/**
* Validiert JWT Token mit Shopware Services
*
* @param string $jwt JWT Token
* @return bool True wenn Token gültig ist
*/
private function validateTokenWithShopwareServices(string $jwt): bool
{
try {
// JWT Payload dekodieren um User ID zu extrahieren
$payload = $this->decodeJwtPayload($jwt);
if (!$payload || !isset($payload['sub'])) {
return false;
}
// Token Ablaufzeit prüfen
if (isset($payload['exp']) && $payload['exp'] < time()) {
return false;
}
// AdminApiSource mit User ID erstellen
$source = new AdminApiSource($payload['sub']);
$context = new Context($source);
// Admin User validieren
return $this->validateAdminUser($payload['sub'], $context);
} catch (\Exception $e) {
return false;
}
}
/**
* Dekodiert JWT Payload ohne Signatur-Validierung
*
* @param string $jwt JWT Token
* @return array|null Dekodiertes Payload oder null bei Fehler
*/
private function decodeJwtPayload(string $jwt): ?array
{
try {
$parts = explode('.', $jwt);
if (count($parts) !== 3) {
return null;
}
// Base64 URL-Safe Dekodierung
$payload = base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1]));
if (!$payload) {
return null;
}
$decoded = json_decode($payload, true);
return is_array($decoded) ? $decoded : null;
} catch (\Exception $e) {
return null;
}
}
/**
* Validiert Admin User mit Shopware User Repository
*
* @param string $userId User ID aus JWT Token
* @param Context $context Shopware Context
* @return bool True wenn User gültig und aktiv ist
*/
private function validateAdminUser(string $userId, Context $context): bool
{
try {
$criteria = new Criteria([$userId]);
$user = $this->userRepository->search($criteria, $context)->first();
if (!$user instanceof UserEntity) {
return false;
}
// User muss aktiv sein
if (!$user->getActive()) {
return false;
}
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* Sichere Auslieferung von Media-Dateien aus dem public/media Verzeichnis
*
* @param string $path Relativer Pfad zur Media-Datei
* @return Response HTTP Response mit der Datei oder 404 wenn nicht gefunden
*/
private function serveMediaFile(string $path): Response
{
// Sicherheitsprüfung: Pfad muss innerhalb von public/media liegen
$mediaBasePath = realpath('public/media');
$requestedPath = 'public/media/' . ltrim($path, '/');
$resolvedPath = realpath($requestedPath);
// Prüfen ob der aufgelöste Pfad innerhalb des erlaubten Verzeichnisses liegt
if ($resolvedPath === false || strpos($resolvedPath, $mediaBasePath) !== 0) {
return new Response('File not found', Response::HTTP_NOT_FOUND);
}
// Prüfen ob die Datei existiert
if (!file_exists($resolvedPath)) {
return new Response('File not found', Response::HTTP_NOT_FOUND);
}
// Datei laden und ausliefern
$fileContent = file_get_contents($resolvedPath);
$fileName = basename($resolvedPath);
return new Response($fileContent, Response::HTTP_OK, [
'Content-Type' => mime_content_type($resolvedPath),
'Content-Disposition' => 'filename=' . $fileName
]);
}
}