src/Controller/SecurityController.php line 182

  1. <?php
  2. namespace App\Controller;
  3. use App\DTO\AzureApplicationInterface;
  4. use App\Enum\LoginErrorEnum;
  5. use App\Event\Admin\AdminLoginFailureEvent;
  6. use App\Exception\EmailDomainNotAuthorizedForSsoAdminException;
  7. use App\Exception\UnsupportedSsoAdminEntityException;
  8. use App\Utility\SsoAdminFactory;
  9. use Composer\CaBundle\CaBundle;
  10. use GuzzleHttp\Client;
  11. use GuzzleHttp\RequestOptions;
  12. use League\OAuth2\Client\Token\AccessToken;
  13. use LogicException;
  14. use Psr\Log\LoggerInterface;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16. use Symfony\Component\Filesystem\Path;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\Routing\Annotation\Route;
  20. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  21. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  22. use TheNetworg\OAuth2\Client\Provider\Azure;
  23. use TheNetworg\OAuth2\Client\Provider\AzureResourceOwner;
  24. class SecurityController extends AbstractController
  25. {
  26.     public function __construct(
  27.         private readonly EventDispatcherInterface $dispatcher,
  28.         private readonly SsoAdminFactory $ssoAdminFactory,
  29.         private readonly LoggerInterface $logger,
  30.     ) {
  31.     }
  32.     private function getProvider(string $email): Azure
  33.     {
  34.         $this->logger->debug('Looking for provider by email', ['email' => $email]);
  35.         $foundSsoAdminApplication $this->ssoAdminFactory->getSsoAdminApplication($email);
  36.         if (!$foundSsoAdminApplication) {
  37.             $this->logger->error('Could not find an SSO admin application with the supplied email address', ['email' => $email]);
  38.             throw new EmailDomainNotAuthorizedForSsoAdminException();
  39.         }
  40.         if (!$foundSsoAdminApplication instanceof AzureApplicationInterface) {
  41.             $this->logger->error('The system only supports Azure applications current', ['email' => $email'application' => $foundSsoAdminApplication]);
  42.             throw new UnsupportedSsoAdminEntityException();
  43.         }
  44.         $this->logger->debug('Found a provider', ['email' => $email]);
  45.         $httpClient = new Client([
  46.             'headers' => [
  47.                 'User-Agent' => 'AANA.com Admin SSO',
  48.             ],
  49.             RequestOptions::VERIFY => Path::canonicalize(CaBundle::getSystemCaRootBundlePath()),
  50.         ]);
  51.         return new Azure(
  52.             [
  53.                 'clientId' => $foundSsoAdminApplication->getClientId(),
  54.                 'clientSecret' => $foundSsoAdminApplication->getClientSecret(),
  55.                 'redirectUri' => $this->generateUrl('app_login_azure_redirect'referenceTypeUrlGeneratorInterface::ABSOLUTE_URL),
  56.                 'tenant' => $foundSsoAdminApplication->getTenantId(),
  57.                 'scopes' => ['openid'],
  58.                 'defaultEndPointVersion' => '2.0',
  59.             ],
  60.             [
  61.                 'httpClient' => $httpClient,
  62.             ]
  63.         );
  64.     }
  65.     private function tryGetLoginPostEmail(Request $request): ?string
  66.     {
  67.         if (!$request->isMethod('POST')) {
  68.             return null;
  69.         }
  70.         $email mb_strtolower(trim($request->get('email''')));
  71.         if (!$email) {
  72.             $this->addFlash('error''Please enter a username');
  73.             return null;
  74.         }
  75.         $emailParts explode('@'$email);
  76.         if (!== count($emailParts)) {
  77.             $this->addFlash('error''Invalid username');
  78.             $this->logger->info('Invalid username', ['email' => $email]);
  79.             $this->dispatcher->dispatch(new AdminLoginFailureEvent($email));
  80.             return null;
  81.         }
  82.         return $email;
  83.     }
  84.     #[Route(path'/login_azure_redirect'name'app_login_azure_redirect')]
  85.     public function login_azure_redirect(Request $request): Response
  86.     {
  87.         $email $request->getSession()->get('aana.email');
  88.         $this->logger->debug('Azure SSO redirect', ['email' => $email]);
  89.         try {
  90.             $provider $this->getProvider($email);
  91.         } catch (EmailDomainNotAuthorizedForSsoAdminException|UnsupportedSsoAdminEntityException $e) {
  92.             $this->addFlash('error''The supplied credentials are not valid on this site');
  93.             $this->logger->warning('Supplied email does not have have an admin SSO provider', ['email' => $email]);
  94.             return $this->redirectToRoute('app_login');
  95.         }
  96.         $request->getSession()->remove('OAuth2.state');
  97.         $accessToken $provider->getAccessToken('authorization_code', [
  98.             'code' => $_GET['code'],
  99.         ]);
  100.         $this->logger->debug('SSO token received from provider', ['token' => $accessToken]);
  101.         if (!$accessToken instanceof AccessToken) {
  102.             $this->logger->error('Invalid SSO token');
  103.             throw new \RuntimeException('Invalid token type');
  104.         }
  105.         /** @var AzureResourceOwner $resourceOwner */
  106.         $resourceOwner $provider->getResourceOwner($accessToken);
  107.         $this->logger->debug('Found resource owner', ['resourceOwner' => $resourceOwner->toArray()]);
  108.         $request->getSession()->set('azure'$resourceOwner);
  109. //        $this->dispatcher->dispatch(new AdminLoginSuccessEvent($resourceOwner->claim('email')));
  110.         return $this->redirectToRoute('admin_dashboard');
  111.     }
  112.     #[Route(path'/login_azure'name'app_login_azure')]
  113.     public function login_azure(Request $request): Response
  114.     {
  115.         $knownEmailAddress $this->tryGetLoginPostEmail($request);
  116.         try {
  117.             $provider $this->getProvider($knownEmailAddress);
  118.         } catch (EmailDomainNotAuthorizedForSsoAdminException|UnsupportedSsoAdminEntityException $e) {
  119.             $this->addFlash('error''The supplied credentials are not valid on this site');
  120.             return $this->redirectToRoute('app_login');
  121.         }
  122.         // Set to use v2 API, skip the line or set the value to Azure::ENDPOINT_VERSION_1_0 if willing to use v1 API
  123.         $provider->defaultEndPointVersion Azure::ENDPOINT_VERSION_2_0;
  124.         $baseGraphUri $provider->getRootMicrosoftGraphUri(null);
  125.         $provider->scope 'openid profile email offline_access '.$baseGraphUri.'/User.Read';
  126.         $authorizationUrl $provider->getAuthorizationUrl(['scope' => $provider->scope'login_hint' => $knownEmailAddress]);
  127.         $request->getSession()->set('aana.email'$knownEmailAddress);
  128.         $request->getSession()->set('OAuth2.state'$provider->getState());
  129.         return $this->redirect($authorizationUrl);
  130.     }
  131.     #[Route(path'/login'name'app_login')]
  132.     public function login(): Response
  133.     {
  134.         return $this->render(
  135.             'security/login.html.twig',
  136.             [
  137.                 'formAction' => 'app_login_azure',
  138.             ]
  139.         );
  140.     }
  141.     #[Route(path'/login-error/{id}'name'app_login_error')]
  142.     public function login_error(int $id): Response
  143.     {
  144.         $message = match (LoginErrorEnum::tryFrom($id)) {
  145.             LoginErrorEnum::NotLocal => 'The supplied user does not have permission to access this site',
  146.             default => 'An unknown error occurred while attempting to sign the user on',
  147.         };
  148.         $this->addFlash('error'$message);
  149.         return $this->redirectToRoute('app_login');
  150.     }
  151.     #[Route(path'/logout'name'app_logout')]
  152.     public function logout(): void
  153.     {
  154.         throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
  155.     }
  156. }