Refactor authentication system (#2602)
Make auth configuration available via UI, remove FOSUserBundle and SAML-Bundle dependency
This commit is contained in:
195
src/Controller/Security/PasswordResetController.php
Normal file
195
src/Controller/Security/PasswordResetController.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Security;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\User;
|
||||
use App\Event\EmailEvent;
|
||||
use App\Event\EmailPasswordResetEvent;
|
||||
use App\Form\PasswordResetForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use DateTime;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/resetting")
|
||||
*/
|
||||
final class PasswordResetController extends AbstractController
|
||||
{
|
||||
private $eventDispatcher;
|
||||
private $userService;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, UserService $userService, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->userService = $userService;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request reset user password: show form.
|
||||
*
|
||||
* @Route(path="/request", name="fos_user_resetting_request", methods={"GET"})
|
||||
*/
|
||||
public function requestAction(): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/request.html.twig');
|
||||
}
|
||||
|
||||
/**
|
||||
* Request reset user password: submit form and send email.
|
||||
*
|
||||
* @Route(path="/send-email", name="fos_user_resetting_send_email", methods={"POST"})
|
||||
*/
|
||||
public function sendEmailAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$username = $request->request->get('username');
|
||||
$user = $this->userService->findUserByUsernameOrEmail($username);
|
||||
|
||||
if (null !== $user && !$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
if (!$user->isInternalUser()) {
|
||||
throw $this->createAccessDeniedException(
|
||||
sprintf('The user "%s" tried to reset the password, but it is registered as "%s" auth-type.', $user->getUsername(), $user->getAuth())
|
||||
);
|
||||
}
|
||||
|
||||
if (null === $user->getConfirmationToken()) {
|
||||
$user->setConfirmationToken($this->userService->generateSecurityToken());
|
||||
}
|
||||
|
||||
$mail = $this->generateResettingEmailMessage($user);
|
||||
$event = new EmailPasswordResetEvent($user, $mail);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
// this will finally send the email
|
||||
$this->eventDispatcher->dispatch(new EmailEvent($event->getEmail()));
|
||||
|
||||
$user->setPasswordRequestedAt(new DateTime());
|
||||
$this->userService->updateUser($user);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('fos_user_resetting_check_email', ['username' => $username]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the user to check his email provider.
|
||||
*
|
||||
* @Route(path="/check-email", name="fos_user_resetting_check_email", methods={"GET"})
|
||||
*/
|
||||
public function checkEmailAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$username = $request->query->get('username');
|
||||
|
||||
if (empty($username)) {
|
||||
// the user does not come from the sendEmail action
|
||||
return $this->redirectToRoute('fos_user_resetting_request');
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/check_email.html.twig', [
|
||||
'tokenLifetime' => ceil($this->configuration->getPasswordResetRetryLifetime() / 3600),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset user password.
|
||||
*
|
||||
* @Route(path="/reset/{token}", name="fos_user_resetting_reset", methods={"GET", "POST"})
|
||||
*/
|
||||
public function resetAction(Request $request, LoginManager $loginManager, ?string $token): Response
|
||||
{
|
||||
if (!$this->configuration->isPasswordResetActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->userService->findUserByConfirmationToken($token);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
}
|
||||
|
||||
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetTokenLifetime())) {
|
||||
return $this->redirectToRoute('fos_user_resetting_request');
|
||||
}
|
||||
|
||||
$form = $this->createResetForm();
|
||||
$form->setData($user);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->setConfirmationToken(null);
|
||||
$user->setPasswordRequestedAt(null);
|
||||
$user->setEnabled(true);
|
||||
|
||||
$this->userService->updateUser($user);
|
||||
|
||||
$response = $this->redirectToRoute('my_profile');
|
||||
$loginManager->logInUser($user, $response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->render('security/password-reset/reset.html.twig', [
|
||||
'token' => $token,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createResetForm(): FormInterface
|
||||
{
|
||||
$options = ['validation_groups' => ['ResetPassword', 'Default']];
|
||||
|
||||
return $this->createFormBuilder()->create('fos_user_resetting_form', PasswordResetForm::class, $options)->getForm();
|
||||
}
|
||||
|
||||
private function generateResettingEmailMessage(User $user): Email
|
||||
{
|
||||
$username = $user->getDisplayName();
|
||||
$language = $user->getLanguage();
|
||||
|
||||
$url = $this->generateUrl('fos_user_resetting_reset', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to(new Address($user->getEmail()))
|
||||
->subject(
|
||||
$this->getTranslator()->trans('reset.subject', ['%username%' => $username], 'email', $language)
|
||||
)
|
||||
->htmlTemplate('emails/password-reset.html.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'username' => $username,
|
||||
'confirmationUrl' => $url,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
84
src/Controller/Security/SecurityController.php
Normal file
84
src/Controller/Security/SecurityController.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Security;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
|
||||
|
||||
final class SecurityController extends AbstractController
|
||||
{
|
||||
private $tokenManager;
|
||||
|
||||
public function __construct(CsrfTokenManagerInterface $tokenManager)
|
||||
{
|
||||
$this->tokenManager = $tokenManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/login", name="fos_user_security_login", methods={"GET", "POST"})
|
||||
*/
|
||||
public function loginAction(Request $request): Response
|
||||
{
|
||||
/** @var SessionInterface $session */
|
||||
$session = $request->getSession();
|
||||
|
||||
$authErrorKey = Security::AUTHENTICATION_ERROR;
|
||||
$lastUsernameKey = Security::LAST_USERNAME;
|
||||
|
||||
// get the error if any (works with forward and redirect -- see below)
|
||||
if ($request->attributes->has($authErrorKey)) {
|
||||
$error = $request->attributes->get($authErrorKey);
|
||||
} elseif (null !== $session && $session->has($authErrorKey)) {
|
||||
$error = $session->get($authErrorKey);
|
||||
$session->remove($authErrorKey);
|
||||
} else {
|
||||
$error = null;
|
||||
}
|
||||
|
||||
if (!$error instanceof AuthenticationException) {
|
||||
$error = null; // The value does not come from the security component.
|
||||
}
|
||||
|
||||
$lastUsername = '';
|
||||
if ($request->hasSession()) {
|
||||
$lastUsername = $session->get($lastUsernameKey);
|
||||
}
|
||||
|
||||
$csrfToken = $this->tokenManager->getToken('authenticate')->getValue();
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
'csrf_token' => $csrfToken,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/login_check", name="fos_user_security_check", methods={"POST"})
|
||||
*/
|
||||
public function checkAction()
|
||||
{
|
||||
throw new \RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/logout", name="fos_user_security_logout", methods={"GET", "POST"})
|
||||
*/
|
||||
public function logoutAction()
|
||||
{
|
||||
throw new \RuntimeException('You must activate the logout in your security firewall configuration.');
|
||||
}
|
||||
}
|
||||
212
src/Controller/Security/SelfRegistrationController.php
Normal file
212
src/Controller/Security/SelfRegistrationController.php
Normal file
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller\Security;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Controller\AbstractController;
|
||||
use App\Entity\User;
|
||||
use App\Event\EmailEvent;
|
||||
use App\Event\EmailSelfRegistrationEvent;
|
||||
use App\Form\SelfRegistrationForm;
|
||||
use App\User\LoginManager;
|
||||
use App\User\UserService;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @Route(path="/register")
|
||||
*/
|
||||
class SelfRegistrationController extends AbstractController
|
||||
{
|
||||
private $eventDispatcher;
|
||||
private $userService;
|
||||
private $tokenStorage;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher, UserService $userService, TokenStorageInterface $tokenStorage, SystemConfiguration $configuration)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->userService = $userService;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/", name="fos_user_registration_register", methods={"GET", "POST"})
|
||||
*/
|
||||
public function registerAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->userService->createNewUser();
|
||||
$user->setLanguage($request->getLocale());
|
||||
|
||||
$form = $this->createSelfRegistrationForm();
|
||||
$form->setData($user);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$user->setEnabled(false);
|
||||
$user->setConfirmationToken($this->userService->generateSecurityToken());
|
||||
|
||||
$mail = $this->generateConfirmationEmail($user);
|
||||
$event = new EmailSelfRegistrationEvent($user, $mail);
|
||||
$this->eventDispatcher->dispatch($event);
|
||||
|
||||
// this will finally send the email
|
||||
$this->eventDispatcher->dispatch(new EmailEvent($event->getEmail()));
|
||||
|
||||
$request->getSession()->set('fos_user_send_confirmation_email/email', $user->getEmail());
|
||||
|
||||
$this->userService->saveNewUser($user);
|
||||
|
||||
return $this->redirectToRoute('fos_user_registration_check_email');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/register.html.twig', [
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the user to check their email provider.
|
||||
*
|
||||
* @Route(path="/check-email", name="fos_user_registration_check_email", methods={"GET"})
|
||||
*/
|
||||
public function checkEmailAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$email = $request->getSession()->get('fos_user_send_confirmation_email/email');
|
||||
|
||||
if (empty($email)) {
|
||||
return $this->redirectToRoute('fos_user_registration_register');
|
||||
}
|
||||
|
||||
$request->getSession()->remove('fos_user_send_confirmation_email/email');
|
||||
$user = $this->userService->findUserByEmail($email);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/check_email.html.twig', [
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive the confirmation token from user email provider, login the user.
|
||||
*
|
||||
* @Route(path="/confirm/{token}", name="fos_user_registration_confirm", methods={"GET"})
|
||||
*/
|
||||
public function confirmAction(LoginManager $loginManager, ?string $token): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->userService->findUserByConfirmationToken($token);
|
||||
|
||||
if (null === $user) {
|
||||
return $this->redirectToRoute('fos_user_security_login');
|
||||
}
|
||||
|
||||
$user->setConfirmationToken(null);
|
||||
$user->setEnabled(true);
|
||||
|
||||
$this->userService->updateUser($user);
|
||||
|
||||
$response = $this->redirectToRoute('fos_user_registration_confirmed');
|
||||
$loginManager->logInUser($user, $response);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the user his account is now confirmed.
|
||||
*
|
||||
* @Route(path="/confirmed", name="fos_user_registration_confirmed", methods={"GET"})
|
||||
*/
|
||||
public function confirmedAction(Request $request): Response
|
||||
{
|
||||
if (!$this->configuration->isSelfRegistrationActive()) {
|
||||
throw $this->createNotFoundException();
|
||||
}
|
||||
|
||||
$user = $this->getUser();
|
||||
if ($user === null) {
|
||||
throw $this->createAccessDeniedException('This user does not have access to this section.');
|
||||
}
|
||||
|
||||
return $this->render('security/self-registration/confirmed.html.twig', [
|
||||
'user' => $user,
|
||||
'targetUrl' => $this->getTargetUrlFromSession($request->getSession()),
|
||||
]);
|
||||
}
|
||||
|
||||
private function createSelfRegistrationForm(): FormInterface
|
||||
{
|
||||
$options = ['validation_groups' => ['Registration', 'Default']];
|
||||
|
||||
return $this->createFormBuilder()->create('fos_user_registration_form', SelfRegistrationForm::class, $options)->getForm();
|
||||
}
|
||||
|
||||
private function getTargetUrlFromSession(SessionInterface $session): ?string
|
||||
{
|
||||
$token = $this->tokenStorage->getToken();
|
||||
if (!method_exists($token, 'getProviderKey')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$key = sprintf('_security.%s.target_path', $token->getProviderKey());
|
||||
|
||||
if ($session->has($key)) {
|
||||
return $session->get($key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function generateConfirmationEmail(User $user): Email
|
||||
{
|
||||
$username = $user->getDisplayName();
|
||||
$language = $user->getLanguage();
|
||||
|
||||
$url = $this->generateUrl('fos_user_registration_confirm', ['token' => $user->getConfirmationToken()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
return (new TemplatedEmail())
|
||||
->to(new Address($user->getEmail()))
|
||||
->subject(
|
||||
$this->getTranslator()->trans('registration.subject', ['%username%' => $username], 'email', $language)
|
||||
)
|
||||
->htmlTemplate('emails/confirmation.html.twig')
|
||||
->context([
|
||||
'user' => $user,
|
||||
'username' => $username,
|
||||
'confirmationUrl' => $url,
|
||||
])
|
||||
;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user