vendor/pimcore/portal-engine/src/Controller/AuthController.php line 41

  1. <?php
  2. /**
  3. * This source file is available under the terms of the
  4. * Pimcore Open Core License (POCL)
  5. * Full copyright and license information is available in
  6. * LICENSE.md which is distributed with this source code.
  7. *
  8. * @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
  9. * @license Pimcore Open Core License (POCL)
  10. */
  11. namespace Pimcore\Bundle\PortalEngineBundle\Controller;
  12. use Exception;
  13. use GuzzleHttp\ClientInterface;
  14. use InvalidArgumentException;
  15. use Pimcore\Bundle\OpenIdConnectBundle\Client\Provider\OpenIdConnectProvider;
  16. use Pimcore\Bundle\OpenIdConnectBundle\EventListener\OpenIdSessionBagListener;
  17. use Pimcore\Bundle\PortalEngineBundle\Exception\OutputErrorException;
  18. use Pimcore\Bundle\PortalEngineBundle\Form\LoginForm;
  19. use Pimcore\Bundle\PortalEngineBundle\Form\RecoverPasswordForm;
  20. use Pimcore\Bundle\PortalEngineBundle\Model\View\Notification;
  21. use Pimcore\Bundle\PortalEngineBundle\Service\Content\HeadTitleService;
  22. use Pimcore\Bundle\PortalEngineBundle\Service\Frontend\FrontendNotificationService;
  23. use Pimcore\Bundle\PortalEngineBundle\Service\Frontend\FrontendVariantService;
  24. use Pimcore\Bundle\PortalEngineBundle\Service\PortalConfig\PortalConfigService;
  25. use Pimcore\Bundle\PortalEngineBundle\Service\Security\Authentication\User\PasswordChangeableService;
  26. use Pimcore\Bundle\PortalEngineBundle\Service\Security\Authentication\User\RecoverPasswordService;
  27. use Pimcore\Bundle\PortalEngineBundle\Service\Security\Authenticator\OpenIdConnectAuthenticator;
  28. use Pimcore\Tool;
  29. use Pimcore\Tool\Session;
  30. use Symfony\Component\Form\FormFactoryInterface;
  31. use Symfony\Component\Form\FormInterface;
  32. use Symfony\Component\HttpFoundation\RedirectResponse;
  33. use Symfony\Component\HttpFoundation\Request;
  34. use Symfony\Component\HttpFoundation\Response;
  35. use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
  36. use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface;
  37. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  38. use Symfony\Component\Routing\Attribute\Route;
  39. use Symfony\Contracts\Translation\LocaleAwareInterface;
  40. use Symfony\Contracts\Translation\TranslatorInterface;
  41. /**
  42. * @internal
  43. */
  44. #[Route(path: '/auth', condition: "request.attributes.get('isPortalEngineSite')")]
  45. class AuthController extends AbstractSiteController
  46. {
  47. public function __construct(protected ClientInterface $httpClient)
  48. {
  49. }
  50. /**
  51. * @return Response
  52. */
  53. #[Route(path: '/login', name: 'pimcore_portalengine_auth_login')]
  54. public function loginAction(Request $request, FormFactoryInterface $formFactory, HeadTitleService $headTitleService, TranslatorInterface $translator, PortalConfigService $portalConfigService, PasswordChangeableService $passwordChangeableService, FrontendVariantService $frontendVariantService)
  55. {
  56. /** @var string $locale */
  57. $locale = $request->getPreferredLanguage(Tool::getValidLanguages());
  58. if ($translator instanceof LocaleAwareInterface) {
  59. $translator->setLocale($locale);
  60. }
  61. $request->setLocale($locale);
  62. $portalName = $portalConfigService->getPortalName();
  63. $oidcProviders = $portalConfigService->getCurrentPortalConfig()->getOidcConfigs();
  64. $guestUserActivated = !empty($portalConfigService->getCurrentPortalConfig()->getGuestUser());
  65. $headTitleService->setTitle($translator->trans('portal-engine.content.title.auth-login', ['%name%' => $portalName], 'portal_engine_frontend'));
  66. $loginForm = $formFactory->create(LoginForm::class);
  67. // The new studio login is served only to portals on the new frontend; legacy portals keep
  68. // their original Bootstrap login (auth/login.html.twig).
  69. $template = $frontendVariantService->isNew()
  70. ? '@PimcorePortalEngine/auth/login-studio.html.twig'
  71. : '@PimcorePortalEngine/auth/login.html.twig';
  72. return $this->renderTemplate($template, [
  73. 'form' => $loginForm->createView(),
  74. 'loginFailed' => (bool)$request->query->get('loginFailed'),
  75. 'portalName' => $portalName,
  76. 'oidcProviderNames' => array_keys($oidcProviders),
  77. 'guestUserActivated' => $guestUserActivated,
  78. 'showRecoverPassword' => $passwordChangeableService->isPasswordChangeable()
  79. ]);
  80. }
  81. /**
  82. * @return RedirectResponse
  83. */
  84. #[Route(path: '/logout', name: 'pimcore_portalengine_auth_logout')]
  85. public function logoutAction(Request $request)
  86. {
  87. return $this->redirectToRoute('pimcore_portalengine_auth_login');
  88. }
  89. /**
  90. * @return Response
  91. */
  92. #[Route(path: '/recover-password', name: 'pimcore_portalengine_auth_recover_password')]
  93. public function recoverPasswordAction(Request $request, TranslatorInterface $translator, FormFactoryInterface $formFactory, FrontendNotificationService $frontendNotificationService, RecoverPasswordService $recoverPasswordService, PasswordChangeableService $passwordChangeableService, HeadTitleService $headTitleService, FrontendVariantService $frontendVariantService): Response
  94. {
  95. if (!$passwordChangeableService->isPasswordChangeable()) {
  96. throw new NotFoundHttpException('password not changeable');
  97. }
  98. /** @var string $locale */
  99. $locale = $request->getPreferredLanguage(Tool::getValidLanguages());
  100. if ($translator instanceof LocaleAwareInterface) {
  101. $translator->setLocale($locale);
  102. }
  103. $request->setLocale($locale);
  104. $headTitleService->setTitle($translator->trans('portal-engine.content.title.auth-recover-password', [], 'portal_engine_frontend'));
  105. /** @var FormInterface $recoverPasswordForm */
  106. $recoverPasswordForm = $formFactory
  107. ->create(RecoverPasswordForm::class)
  108. ->handleRequest($request);
  109. if ($request->isMethod(Request::METHOD_POST) && $recoverPasswordForm->isSubmitted() && $recoverPasswordForm->isValid()) {
  110. try {
  111. /** @var string $userIdentifier */
  112. $userIdentifier = (string)$recoverPasswordForm->get('userIdentifier')->getData();
  113. if (empty($userIdentifier)) {
  114. throw new OutputErrorException($translator->trans('portal-engine.auth.user-not-found', [], 'portal_engine_frontend'));
  115. }
  116. $recoverPasswordService->recoverPassword($userIdentifier);
  117. $frontendNotificationService
  118. ->addNotification($translator->trans('portal-engine.auth.password-recover-email', [], 'portal_engine_frontend'), Notification::HTML_CLASS_SUCCESS);
  119. } catch (Exception $e) {
  120. if ($e instanceof OutputErrorException) {
  121. $frontendNotificationService->addNotification($e->getMessage(), Notification::HTML_CLASS_DANGER);
  122. }
  123. }
  124. }
  125. // Same coexistence rule as loginAction(): the studio recover page is served only to
  126. // portals on the new frontend; legacy portals keep the original Bootstrap page. Without
  127. // this the "forgot password" link dropped out of the new design and back into it at the
  128. // /user/data step, which is already migrated.
  129. $template = $frontendVariantService->isNew()
  130. ? '@PimcorePortalEngine/auth/recover_password-studio.html.twig'
  131. : '@PimcorePortalEngine/auth/recover_password.html.twig';
  132. return $this->renderTemplate($template, [
  133. 'recoverPasswordForm' => $recoverPasswordForm->createView(),
  134. 'notification' => $frontendNotificationService->getNotification()
  135. ]);
  136. }
  137. /**
  138. *
  139. * @return RedirectResponse|Response
  140. *
  141. * @throws Exception
  142. */
  143. #[Route(path: '/oidc/endpoint', name: 'pimcore_portalengine_auth_oidc')]
  144. public function oidcAction(Request $request, PortalConfigService $portalConfigService)
  145. {
  146. if (($request->query->get('code') || $request->query->get('error')) && !$request->query->get('redirect')) {
  147. $url = $request->getSchemeAndHttpHost() . $request->getRequestUri() . '&redirect=1';
  148. $content = <<<RESPONSE
  149. <html>
  150. <head>
  151. <meta http-equiv="refresh" content="0;URL=' {$url}'"/>
  152. </head>
  153. <body><p>Redirecting to <a href="{$url}">{$url}</a>.</p></body>
  154. </html>
  155. RESPONSE;
  156. return new Response($content);
  157. }
  158. if (!class_exists('Pimcore\\Bundle\\OpenIdConnectBundle\\PimcoreOpenIdConnectBundle')) {
  159. throw new Exception('OpenID Connect Bundle not available');
  160. }
  161. if ($request->query->get('error')) {
  162. throw new Exception(strip_tags($request->query->getString('error')) . ': ' . strip_tags($request->query->getString('error_description')));
  163. }
  164. $portalConfig = $portalConfigService->getCurrentPortalConfig();
  165. $session = $request->getSession();
  166. /** @var AttributeBag $sessionBag */
  167. $sessionBag = $session->getBag(OpenIdSessionBagListener::SESSION_BAG_NAME);
  168. // if coming back from provider, use provider from session
  169. if ($request->query->get('code')) {
  170. $providerName = $sessionBag->get(OpenIdConnectAuthenticator::SESSION_KEY_PROVIDER);
  171. } else {
  172. $providerName = $request->query->get('provider');
  173. Session::useBag($request->getSession(), function (AttributeBagInterface $sessionBag) use ($providerName) {
  174. $sessionBag->set(OpenIdConnectAuthenticator::SESSION_KEY_PROVIDER, $providerName);
  175. }, OpenIdSessionBagListener::SESSION_BAG_NAME);
  176. }
  177. $oidcConfig = $portalConfig?->getOidcConfig($providerName);
  178. if ($oidcConfig === null) {
  179. throw new InvalidArgumentException(sprintf('Provider `%s` unknown.', strip_tags($request->query->getString('provider'))));
  180. }
  181. $provider = new OpenIdConnectProvider([
  182. 'clientId' => $oidcConfig->getClientId(),
  183. 'clientSecret' => $oidcConfig->getClientSecret(),
  184. 'urlDiscovery' => $oidcConfig->getDiscoveryUrl(),
  185. 'redirectUri' => $request->getSchemeAndHttpHost() . $request->getPathInfo(),
  186. 'scopes' => $oidcConfig->getScopes()
  187. ]);
  188. $provider->setHttpClient($this->httpClient);
  189. if (!$request->query->get('code')) {
  190. // If we don't have an authorization code then get one
  191. $authUrl = $provider->getAuthorizationUrl();
  192. Session::useBag($request->getSession(), function (AttributeBagInterface $sessionBag) use ($provider) {
  193. $sessionBag->set(OpenIdConnectAuthenticator::SESSION_KEY_STATE, $provider->getState());
  194. }, OpenIdSessionBagListener::SESSION_BAG_NAME);
  195. return $this->redirect($authUrl);
  196. } elseif (!$request->query->get('state') || ($request->query->get('state') !== $sessionBag->get('oauth2state'))) {
  197. // Check given state against previously stored one to mitigate CSRF attack
  198. $sessionBag->remove(OpenIdConnectAuthenticator::SESSION_KEY_STATE);
  199. throw new Exception('Invalid state');
  200. }
  201. return $this->redirectToRoute('pimcore_portalengine_auth_login');
  202. }
  203. }