vendor/nbgrp/onelogin-saml-bundle/src/Security/Http/Authenticator/SamlAuthenticator.php line 40

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. namespace Nbgrp\OneloginSamlBundle\Security\Http\Authenticator;
  4. use Nbgrp\OneloginSamlBundle\Event\UserCreatedEvent;
  5. use Nbgrp\OneloginSamlBundle\Event\UserModifiedEvent;
  6. use Nbgrp\OneloginSamlBundle\Idp\IdpResolverInterface;
  7. use Nbgrp\OneloginSamlBundle\Onelogin\AuthRegistryInterface;
  8. use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge\DeferredEventBadge;
  9. use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Passport\Badge\SamlAttributesBadge;
  10. use Nbgrp\OneloginSamlBundle\Security\Http\Authenticator\Token\SamlToken;
  11. use Nbgrp\OneloginSamlBundle\Security\User\SamlUserFactoryInterface;
  12. use Nbgrp\OneloginSamlBundle\Security\User\SamlUserInterface;
  13. use OneLogin\Saml2\Auth;
  14. use OneLogin\Saml2\Utils;
  15. use Psr\Log\LoggerInterface;
  16. use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
  17. use Symfony\Component\HttpFoundation\RedirectResponse;
  18. use Symfony\Component\HttpFoundation\Request;
  19. use Symfony\Component\HttpFoundation\Response;
  20. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  21. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  22. use Symfony\Component\Security\Core\Exception\AuthenticationException;
  23. use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
  24. use Symfony\Component\Security\Core\Exception\LogicException;
  25. use Symfony\Component\Security\Core\Exception\SessionUnavailableException;
  26. use Symfony\Component\Security\Core\Exception\UserNotFoundException;
  27. use Symfony\Component\Security\Core\User\UserProviderInterface;
  28. use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
  29. use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
  30. use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
  31. use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
  32. use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
  33. use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
  34. use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
  35. use Symfony\Component\Security\Http\HttpUtils;
  36. #[AutoconfigureTag('monolog.logger', ['channel' => 'security'])]
  37. class SamlAuthenticator implements AuthenticatorInterfaceAuthenticationEntryPointInterface
  38. {
  39.     public const SESSION_INDEX_ATTRIBUTE '_saml_session_index';
  40.     public const LAST_REQUEST_ID '_saml_last_request_id';
  41.     public function __construct(
  42.         private HttpUtils $httpUtils,
  43.         private UserProviderInterface $userProvider,
  44.         private IdpResolverInterface $idpResolver,
  45.         private AuthRegistryInterface $authRegistry,
  46.         private AuthenticationSuccessHandlerInterface $successHandler,
  47.         private AuthenticationFailureHandlerInterface $failureHandler,
  48.         private array $options,
  49.         private ?SamlUserFactoryInterface $userFactory,
  50.         private ?LoggerInterface $logger,
  51.         private string $idpParameterName,
  52.         private bool $useProxyVars,
  53.     ) {}
  54.     public function supports(Request $request): ?bool
  55.     {
  56.         return $request->isMethod('POST')
  57.             && $this->httpUtils->checkRequestPath($request, (string) $this->options['check_path']);
  58.     }
  59.     public function start(Request $request, ?AuthenticationException $authException null): Response
  60.     {
  61.         $uri $this->httpUtils->generateUri($request, (string) $this->options['login_path']);
  62.         $idp $this->idpResolver->resolve($request);
  63.         if ($idp) {
  64.             $uri .= '?'.$this->idpParameterName.'='.$idp;
  65.         }
  66.         return new RedirectResponse($uri);
  67.     }
  68.     public function authenticate(Request $request): Passport
  69.     {
  70.         if (!$request->hasSession()) {
  71.             throw new SessionUnavailableException('This authentication method requires a session.');
  72.         }
  73.         if ($this->options['require_previous_session'] && !$request->hasPreviousSession()) {
  74.             throw new SessionUnavailableException('Your session has timed out, or you have disabled cookies.');
  75.         }
  76.         $oneLoginAuth $this->getOneLoginAuth($request);
  77.         Utils::setProxyVars($this->useProxyVars);
  78.         $this->processResponse($oneLoginAuth$request->getSession());
  79.         if ($oneLoginAuth->getErrors()) {
  80.             $errorReason $oneLoginAuth->getLastErrorReason() ?? 'Undefined OneLogin auth error.';
  81.             $this->logger?->error($errorReason);
  82.             throw new AuthenticationException($errorReason);
  83.         }
  84.         return $this->createPassport($oneLoginAuth);
  85.     }
  86.     public function createToken(Passport $passportstring $firewallName): TokenInterface
  87.     {
  88.         if (!$passport->hasBadge(SamlAttributesBadge::class)) {
  89.             throw new LogicException(sprintf('Passport should contains a "%s" badge.'SamlAttributesBadge::class));
  90.         }
  91.         $badge $passport->getBadge(SamlAttributesBadge::class);
  92.         $attributes = [];
  93.         if ($badge instanceof SamlAttributesBadge) {
  94.             $attributes $badge->getAttributes();
  95.         }
  96.         return new SamlToken($passport->getUser(), $firewallName$passport->getUser()->getRoles(), $attributes);
  97.     }
  98.     public function onAuthenticationSuccess(Request $requestTokenInterface $tokenstring $firewallName): ?Response
  99.     {
  100.         return $this->successHandler->onAuthenticationSuccess($request$token);
  101.     }
  102.     public function onAuthenticationFailure(Request $requestAuthenticationException $exception): ?Response
  103.     {
  104.         return $this->failureHandler->onAuthenticationFailure($request$exception);
  105.     }
  106.     protected function processResponse(Auth $oneLoginAuthSessionInterface $session): void
  107.     {
  108.         $requestId null;
  109.         $security $oneLoginAuth->getSettings()->getSecurityData();
  110.         if ($security['rejectUnsolicitedResponsesWithInResponseTo'] ?? false) {
  111.             /** @var string $requestId */
  112.             $requestId $session->get(self::LAST_REQUEST_ID);
  113.         }
  114.         $oneLoginAuth->processResponse($requestId);
  115.     }
  116.     protected function createPassport(Auth $oneLoginAuth): Passport
  117.     {
  118.         $attributes $this->extractAttributes($oneLoginAuth);
  119.         $this->logger?->debug('SAML attributes extracted'$attributes);
  120.         $deferredEventBadge = new DeferredEventBadge();
  121.         $userBadge = new UserBadge(
  122.             $this->extractIdentifier($oneLoginAuth$attributes),
  123.             function (string $identifier) use ($deferredEventBadge$attributes) {
  124.                 try {
  125.                     try {
  126.                         $user $this->userProvider->loadUserByIdentifier($identifier);
  127.                         if ($user instanceof SamlUserInterface) {
  128.                             $user->setSamlAttributes($attributes);
  129.                             $deferredEventBadge->setEvent(new UserModifiedEvent($user));
  130.                         }
  131.                     } catch (UserNotFoundException $exception) {
  132.                         if (!$this->userFactory instanceof SamlUserFactoryInterface) {
  133.                             throw $exception;
  134.                         }
  135.                         $user $this->userFactory->createUser($identifier$attributes);
  136.                         $deferredEventBadge->setEvent(new UserCreatedEvent($user));
  137.                     }
  138.                 } catch (\Throwable $exception) {
  139.                     if ($exception instanceof UserNotFoundException) {
  140.                         throw $exception;
  141.                     }
  142.                     throw new AuthenticationException('The authentication failed.'0$exception);
  143.                 }
  144.                 return $user;
  145.             },
  146.         );
  147.         return new SelfValidatingPassport($userBadge, [
  148.             new SamlAttributesBadge($attributes),
  149.             $deferredEventBadge,
  150.         ]);
  151.     }
  152.     protected function extractAttributes(Auth $oneLoginAuth): array
  153.     {
  154.         $attributes $this->options['use_attribute_friendly_name'] ?? false
  155.             $oneLoginAuth->getAttributesWithFriendlyName()
  156.             : $oneLoginAuth->getAttributes();
  157.         $attributes[self::SESSION_INDEX_ATTRIBUTE] = $oneLoginAuth->getSessionIndex();
  158.         return $attributes;
  159.     }
  160.     protected function extractIdentifier(Auth $oneLoginAuth, array $attributes): string
  161.     {
  162.         if (empty($this->options['identifier_attribute'])) {
  163.             return $oneLoginAuth->getNameId();
  164.         }
  165.         $identifierAttribute = (string) $this->options['identifier_attribute'];
  166.         if (!\array_key_exists($identifierAttribute$attributes)) {
  167.             throw new \RuntimeException('Attribute "'.$identifierAttribute.'" not found in SAML data.');
  168.         }
  169.         $identifier $attributes[$identifierAttribute];
  170.         if (\is_array($identifier)) {
  171.             /** @var mixed $identifier */
  172.             $identifier reset($identifier);
  173.         }
  174.         if (!\is_string($identifier)) {
  175.             throw new \RuntimeException('Attribute "'.$identifierAttribute.'" does not contain valid user identifier.');
  176.         }
  177.         return $identifier;
  178.     }
  179.     private function getOneLoginAuth(Request $request): Auth
  180.     {
  181.         try {
  182.             $idp $this->idpResolver->resolve($request);
  183.             $authService $idp
  184.                 $this->authRegistry->getService($idp)
  185.                 : $this->authRegistry->getDefaultService();
  186.         } catch (\RuntimeException $exception) {
  187.             $this->logger?->error($exception->getMessage());
  188.             throw new AuthenticationServiceException($exception->getMessage());
  189.         }
  190.         return $authService;
  191.     }
  192. }