vendor/pimcore/pimcore/bundles/AdminBundle/Controller/Admin/LoginController.php line 55

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. namespace Pimcore\Bundle\AdminBundle\Controller\Admin;
  15. use Pimcore\Bundle\AdminBundle\Controller\AdminAbstractController;
  16. use Pimcore\Bundle\AdminBundle\Controller\BruteforceProtectedControllerInterface;
  17. use Pimcore\Bundle\AdminBundle\Security\Authenticator\AdminLoginAuthenticator;
  18. use Pimcore\Bundle\AdminBundle\Security\BruteforceProtectionHandler;
  19. use Pimcore\Bundle\AdminBundle\Security\CsrfProtectionHandler;
  20. use Pimcore\Config;
  21. use Pimcore\Controller\KernelControllerEventInterface;
  22. use Pimcore\Controller\KernelResponseEventInterface;
  23. use Pimcore\Event\Admin\Login\LoginRedirectEvent;
  24. use Pimcore\Event\Admin\Login\LostPasswordEvent;
  25. use Pimcore\Event\AdminEvents;
  26. use Pimcore\Extension\Bundle\PimcoreBundleManager;
  27. use Pimcore\Http\ResponseHelper;
  28. use Pimcore\Logger;
  29. use Pimcore\Model\User;
  30. use Pimcore\Security\SecurityHelper;
  31. use Pimcore\Tool;
  32. use Pimcore\Tool\Authentication;
  33. use Symfony\Component\HttpFoundation\RedirectResponse;
  34. use Symfony\Component\HttpFoundation\Request;
  35. use Symfony\Component\HttpFoundation\Response;
  36. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  37. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  38. use Symfony\Component\RateLimiter\RateLimiterFactory;
  39. use Symfony\Component\Routing\Annotation\Route;
  40. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  41. use Symfony\Component\Routing\RouterInterface;
  42. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  43. use Symfony\Component\Security\Core\Security;
  44. use Symfony\Component\Security\Core\User\UserInterface;
  45. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  46. use Symfony\Contracts\Translation\LocaleAwareInterface;
  47. use Symfony\Contracts\Translation\TranslatorInterface;
  48. /**
  49.  * @internal
  50.  */
  51. class LoginController extends AdminAbstractController implements BruteforceProtectedControllerInterfaceKernelControllerEventInterfaceKernelResponseEventInterface
  52. {
  53.     public function __construct(
  54.         protected ResponseHelper $responseHelper,
  55.         protected TranslatorInterface $translator,
  56.         protected PimcoreBundleManager $bundleManager,
  57.         protected EventDispatcherInterface $eventDispatcher,
  58.     ) {
  59.     }
  60.     /**
  61.      * @param ControllerEvent $event
  62.      */
  63.     public function onKernelControllerEvent(ControllerEvent $event)
  64.     {
  65.         // use browser language for login page if possible
  66.         $locale 'en';
  67.         $availableLocales Tool\Admin::getLanguages();
  68.         foreach ($event->getRequest()->getLanguages() as $userLocale) {
  69.             if (in_array($userLocale$availableLocales)) {
  70.                 $locale $userLocale;
  71.                 break;
  72.             }
  73.         }
  74.         if ($this->translator instanceof LocaleAwareInterface) {
  75.             $this->translator->setLocale($locale);
  76.         }
  77.     }
  78.     /**
  79.      * {@inheritdoc}
  80.      */
  81.     public function onKernelResponseEvent(ResponseEvent $event)
  82.     {
  83.         $response $event->getResponse();
  84.         $response->headers->set('X-Frame-Options''deny'true);
  85.         $this->responseHelper->disableCache($responsetrue);
  86.     }
  87.     /**
  88.      * @Route("/login", name="pimcore_admin_login")
  89.      * @Route("/login/", name="pimcore_admin_login_fallback")
  90.      */
  91.     public function loginAction(Request $requestCsrfProtectionHandler $csrfProtectionConfig $config)
  92.     {
  93.         $queryParams $request->query->all();
  94.         if ($request->get('_route') === 'pimcore_admin_login_fallback') {
  95.             return $this->redirectToRoute('pimcore_admin_login'$queryParamsResponse::HTTP_MOVED_PERMANENTLY);
  96.         }
  97.         $redirectUrl $this->dispatchLoginRedirect($queryParams);
  98.         if ($this->generateUrl('pimcore_admin_login'$queryParams) != $redirectUrl) {
  99.             return new RedirectResponse($redirectUrl);
  100.         }
  101.         $csrfProtection->regenerateCsrfToken();
  102.         $user $this->getAdminUser();
  103.         if ($user instanceof UserInterface) {
  104.             return $this->redirectToRoute('pimcore_admin_index');
  105.         }
  106.         $params $this->buildLoginPageViewParams($config);
  107.         $session_gc_maxlifetime ini_get('session.gc_maxlifetime');
  108.         if (empty($session_gc_maxlifetime)) {
  109.             $session_gc_maxlifetime 120;
  110.         }
  111.         $params['csrfTokenRefreshInterval'] = ((int)$session_gc_maxlifetime 60) * 1000;
  112.         if ($request->get('too_many_attempts')) {
  113.             $params['error'] = SecurityHelper::convertHtmlSpecialChars($request->get('too_many_attempts'));
  114.         }
  115.         if ($request->get('auth_failed')) {
  116.             $params['error'] = 'error_auth_failed';
  117.         }
  118.         if ($request->get('session_expired')) {
  119.             $params['error'] = 'error_session_expired';
  120.         }
  121.         if ($request->get('deeplink')) {
  122.             $params['deeplink'] = true;
  123.         }
  124.         $params['browserSupported'] = $this->detectBrowser();
  125.         $params['debug'] = \Pimcore::inDebugMode();
  126.         return $this->render('@PimcoreAdmin/Admin/Login/login.html.twig'$params);
  127.     }
  128.     /**
  129.      * @Route("/login/csrf-token", name="pimcore_admin_login_csrf_token")
  130.      */
  131.     public function csrfTokenAction(Request $requestCsrfProtectionHandler $csrfProtection)
  132.     {
  133.         if (!$this->getAdminUser()) {
  134.             $csrfProtection->regenerateCsrfToken();
  135.         }
  136.         return $this->json([
  137.            'csrfToken' => $csrfProtection->getCsrfToken(),
  138.         ]);
  139.     }
  140.     /**
  141.      * @Route("/logout", name="pimcore_admin_logout" , methods={"POST"})
  142.      */
  143.     public function logoutAction()
  144.     {
  145.         // this route will never be matched, but will be handled by the logout handler
  146.     }
  147.     /**
  148.      * Dummy route used to check authentication
  149.      *
  150.      * @Route("/login/login", name="pimcore_admin_login_check")
  151.      *
  152.      * @see AdminLoginAuthenticator for the security implementation
  153.      * @see AdminAuthenticator for the security implementation (Authenticator Based Security)
  154.      */
  155.     public function loginCheckAction()
  156.     {
  157.         // just in case the authenticator didn't redirect
  158.         return new RedirectResponse($this->generateUrl('pimcore_admin_login'));
  159.     }
  160.     /**
  161.      * @Route("/login/lostpassword", name="pimcore_admin_login_lostpassword")
  162.      */
  163.     public function lostpasswordAction(Request $request, ?BruteforceProtectionHandler $bruteforceProtectionHandlerCsrfProtectionHandler $csrfProtectionConfig $configRateLimiterFactory $resetPasswordLimiterRouterInterface $router)
  164.     {
  165.         $params $this->buildLoginPageViewParams($config);
  166.         $error null;
  167.         if ($request->getMethod() === 'POST' && $username $request->get('username')) {
  168.             $user User::getByName($username);
  169.             if (!$user instanceof User) {
  170.                 $error 'user_unknown';
  171.             }
  172.             // TODO Pimcore 11: remove this BC layer, only the RateLimiter would be valid
  173.             if ($bruteforceProtectionHandler) {
  174.                 try {
  175.                     $bruteforceProtectionHandler->checkProtection($username$request);
  176.                 } catch (\Exception $e) {
  177.                     $error 'user_reset_password_too_many_attempts';
  178.                 }
  179.             } else {
  180.                 $limiter $resetPasswordLimiter->create($request->getClientIp());
  181.                 if (false === $limiter->consume(1)->isAccepted()) {
  182.                     $error 'user_reset_password_too_many_attempts';
  183.                 }
  184.             }
  185.             if (!$error) {
  186.                 if (!$user->isActive()) {
  187.                     $error 'user_inactive';
  188.                 }
  189.                 if (!$user->getEmail()) {
  190.                     $error 'user_no_email_address';
  191.                 }
  192.                 if (!$user->getPassword()) {
  193.                     $error 'user_no_password';
  194.                 }
  195.             }
  196.             if (!$error) {
  197.                 $token Authentication::generateToken($user->getName());
  198.                 $domain $config['general']['domain'];
  199.                 if (!$domain) {
  200.                     throw new \Exception('No main domain set in system settings, unable to generate reset password link');
  201.                 }
  202.                 $context $router->getContext();
  203.                 $context->setHost($domain);
  204.                 $loginUrl $this->generateUrl('pimcore_admin_login_check', [
  205.                     'token' => $token,
  206.                     'reset' => 'true',
  207.                 ], UrlGeneratorInterface::ABSOLUTE_URL);
  208.                 try {
  209.                     $event = new LostPasswordEvent($user$loginUrl);
  210.                     $this->eventDispatcher->dispatch($eventAdminEvents::LOGIN_LOSTPASSWORD);
  211.                     // only send mail if it wasn't prevented in event
  212.                     if ($event->getSendMail()) {
  213.                         $mail Tool::getMail([$user->getEmail()], 'Pimcore lost password service');
  214.                         $mail->setIgnoreDebugMode(true);
  215.                         $mail->text("Login to pimcore and change your password using the following link. This temporary login link will expire in 24 hours: \r\n\r\n" $loginUrl);
  216.                         $mail->send();
  217.                     }
  218.                     // directly return event response
  219.                     if ($event->hasResponse()) {
  220.                         return $event->getResponse();
  221.                     }
  222.                 } catch (\Exception $e) {
  223.                     Logger::error('Error sending password recovery email: ' $e->getMessage());
  224.                     $error 'lost_password_email_error';
  225.                 }
  226.             }
  227.             if ($error) {
  228.                 Logger::error('Lost password service: ' $error);
  229.                 $bruteforceProtectionHandler?->addEntry($request->get('username'), $request);
  230.             }
  231.         }
  232.         $csrfProtection->regenerateCsrfToken();
  233.         if ($error) {
  234.             $params['reset_error'] = 'Please make sure you are entering a correct input.';
  235.             if ($error === 'user_reset_password_too_many_attempts') {
  236.                 $params['reset_error'] = 'Too many attempts. Please retry later.';
  237.             }
  238.         }
  239.         return $this->render('@PimcoreAdmin/Admin/Login/lostpassword.html.twig'$params);
  240.     }
  241.     /**
  242.      * @Route("/login/deeplink", name="pimcore_admin_login_deeplink")
  243.      */
  244.     public function deeplinkAction(Request $request)
  245.     {
  246.         // check for deeplink
  247.         $queryString $_SERVER['QUERY_STRING'];
  248.         if (preg_match('/(document|asset|object)_([0-9]+)_([a-z]+)/'$queryString$deeplink)) {
  249.             $deeplink $deeplink[0];
  250.             $perspective strip_tags($request->get('perspective'''));
  251.             if (strpos($queryString'token')) {
  252.                 $url $this->dispatchLoginRedirect([
  253.                     'deeplink' => $deeplink,
  254.                     'perspective' => $perspective,
  255.                 ]);
  256.                 $url .= '&' $queryString;
  257.                 return $this->redirect($url);
  258.             } elseif ($queryString) {
  259.                 $url $this->dispatchLoginRedirect([
  260.                     'deeplink' => 'true',
  261.                     'perspective' => $perspective,
  262.                 ]);
  263.                 return $this->render('@PimcoreAdmin/Admin/Login/deeplink.html.twig', [
  264.                     'tab' => $deeplink,
  265.                     'redirect' => $url,
  266.                 ]);
  267.             }
  268.         }
  269.     }
  270.     protected function buildLoginPageViewParams(Config $config): array
  271.     {
  272.         return [
  273.             'config' => $config,
  274.             'pluginCssPaths' => $this->bundleManager->getCssPaths(),
  275.         ];
  276.     }
  277.     /**
  278.      * @Route("/login/2fa", name="pimcore_admin_2fa")
  279.      */
  280.     public function twoFactorAuthenticationAction(Request $request, ?BruteforceProtectionHandler $bruteforceProtectionHandlerConfig $config)
  281.     {
  282.         $params $this->buildLoginPageViewParams($config);
  283.         if ($request->hasSession()) {
  284.             // we have to call the check here manually, because BruteforceProtectionListener uses the 'username' from the request
  285.             $bruteforceProtectionHandler?->checkProtection($this->getAdminUser()->getName(), $request);
  286.             $session $request->getSession();
  287.             $authException $session->get(Security::AUTHENTICATION_ERROR);
  288.             if ($authException instanceof AuthenticationException) {
  289.                 $session->remove(Security::AUTHENTICATION_ERROR);
  290.                 $params['error'] = $authException->getMessage();
  291.                 $bruteforceProtectionHandler?->addEntry($this->getAdminUser()->getName(), $request);
  292.             }
  293.         } else {
  294.             $params['error'] = 'No session available, it either timed out or cookies are not enabled.';
  295.         }
  296.         return $this->render('@PimcoreAdmin/Admin/Login/twoFactorAuthentication.html.twig'$params);
  297.     }
  298.     /**
  299.      * @Route("/login/2fa-verify", name="pimcore_admin_2fa-verify")
  300.      *
  301.      * @param Request $request
  302.      */
  303.     public function twoFactorAuthenticationVerifyAction(Request $request)
  304.     {
  305.     }
  306.     /**
  307.      * @return bool
  308.      */
  309.     public function detectBrowser()
  310.     {
  311.         $supported false;
  312.         $browser = new \Browser();
  313.         $browserVersion = (int)$browser->getVersion();
  314.         if ($browser->getBrowser() == \Browser::BROWSER_FIREFOX && $browserVersion >= 72) {
  315.             $supported true;
  316.         }
  317.         if ($browser->getBrowser() == \Browser::BROWSER_CHROME && $browserVersion >= 84) {
  318.             $supported true;
  319.         }
  320.         if ($browser->getBrowser() == \Browser::BROWSER_SAFARI && $browserVersion >= 13.1) {
  321.             $supported true;
  322.         }
  323.         if ($browser->getBrowser() == \Browser::BROWSER_EDGE && $browserVersion >= 90) {
  324.             $supported true;
  325.         }
  326.         return $supported;
  327.     }
  328.     private function dispatchLoginRedirect(array $routeParams = []): string
  329.     {
  330.         $event = new LoginRedirectEvent('pimcore_admin_login'$routeParams);
  331.         $this->eventDispatcher->dispatch($eventAdminEvents::LOGIN_REDIRECT);
  332.         return $this->generateUrl($event->getRouteName(), $event->getRouteParams());
  333.     }
  334. }