<?php
namespace App\Controller\Main;
use App\Security\LoginFormAuthenticator;
use App\Security\UserProvider;
use App\Services\Admin\UserService;
use App\Services\Main\SecurityService;
use OneLogin\Saml2\Auth;
use OneLogin\Saml2\Settings;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
/**
* Class SecurityController.
*/
class SecurityController extends AbstractController
{
private const ERROR_UNKNOW_TOKEN = 'unknown_privateToken';
/**
* @var SecurityService
*/
private SecurityService $securityService;
/**
* @var UserService
*/
private UserService $userService;
/**
* @var LoginFormAuthenticator
*/
private LoginFormAuthenticator $loginFormAuthenticator;
/**
* SecurityController constructor.
*
* @param SecurityService $securityService
* @param UserService $userService
* @param LoginFormAuthenticator $loginFormAuthenticator
*/
public function __construct(
SecurityService $securityService,
UserService $userService,
LoginFormAuthenticator $loginFormAuthenticator
) {
$this->securityService = $securityService;
$this->userService = $userService;
$this->loginFormAuthenticator = $loginFormAuthenticator;
}
/**
* @Route("/", name="app.login")
*
* @param AuthenticationUtils $authenticationUtils
*
* @return Response
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('dashboard.index');
}
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
$options = [
'twoAuthRequired' => false,
];
$messageData = [];
$errorMessage = null;
if (null !== $error) {
$errorMessage = $error->getMessage();
if (empty($errorMessage)) {
$errorMessage = $error->getMessageKey();
}
if (str_contains($errorMessage, 'security.twoAuth.required')) {
$messageData = $error->getMessageData();
$options['twoAuthRequired'] = true;
if (null !== $messageData['twoAuthToken']) {
$this->addFlash(
'danger',
('expiredToken' === $messageData['mailError'])
? 'security.twoAuth.expiredToken'
: 'security.twoAuth.wrongToken'
);
}
$this->addFlash('info', $errorMessage);
} else {
$this->addFlash('danger', $errorMessage);
}
}
$loginForm = $this->createForm('App\Form\Main\LoginType', null, $options);
$lastUsername = $authenticationUtils->getLastUsername();
if (!empty($lastUsername)) {
$loginForm->get('login')->setData($lastUsername);
}
return $this->render('security/login.html.twig', [
'loginForm' => $loginForm->createView(),
'last_username' => $lastUsername,
'last_password' => $messageData['password'] ?? null,
'isTwoAuthVerification' => $options['twoAuthRequired'],
'isTwoAuthMail' => ($options['twoAuthRequired'] && 'security.twoAuth.required.mail' === $errorMessage),
]);
}
/**
* @Route("/login/forgot-password", name="app.forgot")
*
* @param Request $request
*
* @throws DecodingExceptionInterface
*
* @return Response
*/
public function forgotPassword(Request $request): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('dashboard.index');
}
$recoverPasswordForm = $this->createForm('App\Form\Main\RecoverPasswordType');
$recoverPasswordForm->handleRequest($request);
if ($recoverPasswordForm->isSubmitted()) {
if ($recoverPasswordForm->isValid()) {
$login = $recoverPasswordForm->get('login')->getData();
$responseArray = $this->securityService->requestPasswordRecovery($login);
if (!$responseArray['error']) {
$this->addFlash('info', 'security.password.forgot.confirmation');
} else {
$this->addFlash('danger', $responseArray['message']);
}
} else {
$this->addFlash('danger', 'security.password.forgot.error.invalid');
}
}
return $this->render('security/forgotPassword.html.twig', [
'recoverPasswordForm' => $recoverPasswordForm->createView(),
'passwordRecovery' => $passwordRecovery ?? null,
]);
}
/**
* @Route("/login/reset-password/{privateToken}", name="app.password.reset")
*
* @param $privateToken
* @param Request $request
*
* @throws DecodingExceptionInterface
*
* @return Response
*/
public function resetPassword($privateToken, Request $request): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('dashboard.index');
}
$responseArray = $this->securityService->getPasswordRecoveryByKey($privateToken);
if ($responseArray['error']) {
if (self::ERROR_UNKNOW_TOKEN === $responseArray['message']) {
$this->addFlash('danger', 'security.password.reset.error.notfound');
} else {
$this->addFlash('danger', 'global.errors.undefined');
}
return $this->redirectToRoute('root');
}
return $this->forward('App\Controller\Main\SecurityController::definePassword', [], $responseArray['content']);
}
/**
* @Route("/login/define-password", name="app.definePassword")
*
* @throws DecodingExceptionInterface
*
* @return Response
*/
public function definePassword(Request $request): Response
{
if ($this->getUser()) {
$this->addFlash('danger', 'security.password.expired.label');
$this->logout();
}
$definePasswordForm = $this->createForm('App\Form\Main\DefinePasswordType', [], [
'action' => $this->generateUrl('app.definePassword'),
]);
$definePasswordForm->handleRequest($request);
if ($definePasswordForm->isSubmitted()) {
if ($definePasswordForm->isValid()) {
$resetRequest = $this->securityService->handleDefinePassword(
$definePasswordForm->get('login')->getData(),
$definePasswordForm->get('password')->getData()
);
$responseArray = $resetRequest->getArrayResponse();
if ($responseArray['error']) {
$this->addFlash('danger', 'security.firstLogin.error');
} else {
return $this->redirectToRoute('app.login');
}
} else {
$this->addFlash('danger', 'security.firstLogin.error');
}
}
$rgpdOptions = $request->query->get('rgpdOptions');
if (empty($rgpdOptions)) {
return $this->redirectToRoute('app.login');
}
return $this->render('security/definePassword.html.twig', [
'definePasswordForm' => $definePasswordForm->createView(),
'rgpdOptions' => $rgpdOptions,
'login' => $request->query->get('login'),
]);
}
/**
* @Route("/login/first-login", name="app.firstLogin")
*
* @param Request $request
*
* @throws DecodingExceptionInterface
*
* @return Response
*/
public function firstLogin(Request $request): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('dashboard.index');
}
$firstLoginForm = $this->createForm('App\Form\Main\FirstLoginType');
$firstLoginForm->handleRequest($request);
if ($firstLoginForm->isSubmitted()) {
if ($firstLoginForm->isValid()) {
$resetRequest = $this->securityService->handleFirstLogin(
$firstLoginForm->get('email')->getData(),
$firstLoginForm->get('login')->getData()
);
if (!$resetRequest->getArrayResponse()['error']) {
return $this->forward(
'App\Controller\Main\SecurityController::definePassword',
[],
$resetRequest->getContent()
);
}
$this->addFlash('danger', 'security.firstLogin.error');
} else {
$this->addFlash('danger', 'security.firstLogin.error');
}
}
return $this->render('security/firstLogin.html.twig', [
'firstLoginForm' => $firstLoginForm->createView(),
]);
}
/**
* @Route("/logout", name="app.logout")
*/
public function logout(): void
{
}
/**
* @Route("/classic-logout", name="app.logout.classic")
*/
public function classicLogout()
{
//TODO: call API to delete refresh_token
$response = $this->redirectToRoute('app.login');
$response->headers->clearCookie('PHPSESSID');
return $response;
}
/**
* @Route("/saml-login/{custId}", name="app.samlLogin", methods={"POST"})
*
* @param string $custId
*/
public function samlLogin(string $custId, Request $request)
{
if ('' === $custId) {
return $this->redirectToRoute('root');
}
$settings = $this->getParameter('hslavich_onelogin_saml');
$customerSettings = $this->securityService->updateSSOParameters($settings, $custId);
if (false !== $customerSettings) {
$auth = new Auth($customerSettings);
$auth->processResponse(null);
$errors = $auth->getErrors();
if (!$auth->isAuthenticated()) {
return $this->redirectToRoute('root');
}
$samlUserdata = $auth->getAttributes();
$_SESSION['samlUserdata'] = $samlUserdata;
if (isset($samlUserdata['token'])) {
$credential = [
'ssoToken' => $samlUserdata['token'][0],
];
$user = $this->loginFormAuthenticator->getUser($credential, new UserProvider($this->securityService));
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->get('security.token_storage')->setToken($token);
$this->get('session')->set('_security_main', serialize($token));
}
}
return $this->redirectToRoute('root');
}
/**
* @Route("/get-metadata", name="app.getMetadata")
*/
public function getMetadata()
{
$settings = $this->getParameter('hslavich_onelogin_saml');
try {
$settings = new Settings($settings, true);
$content = $settings->getSPMetadata();
$errors = $settings->validateMetadata($content);
if (empty($errors)) {
$response = new Response($content);
$response->headers->set('Content-Type', 'text/xml');
return $response;
}
throw new OneLogin_Saml2_Error('Invalid SP metadata: '.implode(', ', $errors), OneLogin_Saml2_Error::METADATA_SP_INVALID);
} catch (Exception $e) {
$content = $e->getMessage();
}
return new Response(
$content
);
}
/**
* @Route("/login/define-two-auth", name="app.defineTwoAuth")
*
* @param RequestStack $requestStack
*
* @throws DecodingExceptionInterface
*
* @return Response
*/
public function defineTwoAuth(RequestStack $requestStack): Response
{
$session = $requestStack->getSession();
$loginInformations = $session->get('login-informations');
$qrCode = $this->userService->getMyTotpQrCode();
$user = $this->getUser();
//Logout user and clear this sentive information from session
$this->classicLogout();
return $this->render('security/defineTwoAuth.html.twig', [
'qrCode' => $qrCode,
'login' => $loginInformations['login'],
'password' => $loginInformations['password'],
'user' => $user,
]);
}
/**
* @Route("/oauth/{customerId}/{authType}", name="oauth")
*
* @param RequestStack $requestStack
*
* @throws DecodingExceptionInterface
*
* @return Response
*/
public function oAuthResponse(Request $request, string $customerId, string $authType): Response
{
$parameters = [];
switch ($authType) {
case 'azure':
$parameters = ['customerId' => $customerId, 'code' => $request->query->get('code')];
break;
default:
//TODO: add error message
return $this->redirectToRoute('root');
}
$tokens = $this->securityService->authWithOauth($authType, $parameters);
if (empty($tokens['token']) || empty($tokens['refresh_token'])) {
return $this->redirectToRoute('app.login');
}
$user = $this->loginFormAuthenticator->getUser([], new UserProvider($this->securityService), $tokens);
$token = new UsernamePasswordToken($user, null, 'main', $user->getRoles());
$this->get('security.token_storage')->setToken($token);
$this->get('session')->set('_security_main', serialize($token));
return $this->redirectToRoute('root');
}
}