From e7de1931fc38050565709d1170e683d8bacdf700 Mon Sep 17 00:00:00 2001 From: tracer <tracer@24unix.net> Date: Sun, 30 May 2021 17:44:10 +0200 Subject: [PATCH] added authenticator --- config/packages/security.yaml | 7 ++ src/Controller/SecurityController.php | 36 +++++++++ src/Security/AppAuthenticator.php | 106 ++++++++++++++++++++++++++ templates/security/login.html.twig | 42 ++++++++++ 4 files changed, 191 insertions(+) create mode 100644 src/Controller/SecurityController.php create mode 100644 src/Security/AppAuthenticator.php create mode 100644 templates/security/login.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 31bce8a..f8bd720 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -18,6 +18,13 @@ security: anonymous: true lazy: true provider: app_user_provider + guard: + authenticators: + - App\Security\AppAuthenticator + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route # activate different ways to authenticate # https://symfony.com/doc/current/security.html#firewalls-authentication diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..ed5a595 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,36 @@ +<?php + +namespace App\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; + +class SecurityController extends AbstractController +{ + /** + * @Route("/login", name="app_login") + */ + public function login(AuthenticationUtils $authenticationUtils): Response + { + // if ($this->getUser()) { + // return $this->redirectToRoute('target_path'); + // } + + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + /** + * @Route("/logout", name="app_logout") + */ + public function logout() + { + throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} diff --git a/src/Security/AppAuthenticator.php b/src/Security/AppAuthenticator.php new file mode 100644 index 0000000..bb5b251 --- /dev/null +++ b/src/Security/AppAuthenticator.php @@ -0,0 +1,106 @@ +<?php + +namespace App\Security; + +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; +use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Security; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Csrf\CsrfToken; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; +use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; +use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; +use Symfony\Component\Security\Http\Util\TargetPathTrait; + +class AppAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface +{ + use TargetPathTrait; + + public const LOGIN_ROUTE = 'app_login'; + + private $entityManager; + private $urlGenerator; + private $csrfTokenManager; + private $passwordEncoder; + + public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) + { + $this->entityManager = $entityManager; + $this->urlGenerator = $urlGenerator; + $this->csrfTokenManager = $csrfTokenManager; + $this->passwordEncoder = $passwordEncoder; + } + + public function supports(Request $request) + { + return self::LOGIN_ROUTE === $request->attributes->get('_route') + && $request->isMethod('POST'); + } + + public function getCredentials(Request $request) + { + $credentials = [ + 'username' => $request->request->get('username'), + 'password' => $request->request->get('password'), + 'csrf_token' => $request->request->get('_csrf_token'), + ]; + $request->getSession()->set( + Security::LAST_USERNAME, + $credentials['username'] + ); + + return $credentials; + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + $token = new CsrfToken('authenticate', $credentials['csrf_token']); + if (!$this->csrfTokenManager->isTokenValid($token)) { + throw new InvalidCsrfTokenException(); + } + + $user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => $credentials['username']]); + + if (!$user) { + throw new UsernameNotFoundException('Username could not be found.'); + } + + return $user; + } + + public function checkCredentials($credentials, UserInterface $user) + { + return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function getPassword($credentials): ?string + { + return $credentials['password']; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) + { + if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { + return new RedirectResponse($targetPath); + } + + // For example : return new RedirectResponse($this->urlGenerator->generate('some_route')); + throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); + } + + protected function getLoginUrl() + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } +} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..fbaba11 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,42 @@ +{% extends 'base.html.twig' %} + +{% block title %}Log in!{% endblock %} + +{% block body %} +<form method="post"> + {% if error %} + <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> + {% endif %} + + {% if app.user %} + <div class="mb-3"> + You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a> + </div> + {% endif %} + + <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> + <label for="inputUsername">Username</label> + <input type="text" value="{{ last_username }}" name="username" id="inputUsername" class="form-control" autocomplete="username" required autofocus> + <label for="inputPassword">Password</label> + <input type="password" name="password" id="inputPassword" class="form-control" autocomplete="current-password" required> + + <input type="hidden" name="_csrf_token" + value="{{ csrf_token('authenticate') }}" + > + + {# + Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. + See https://symfony.com/doc/current/security/remember_me.html + + <div class="checkbox mb-3"> + <label> + <input type="checkbox" name="_remember_me"> Remember me + </label> + </div> + #} + + <button class="btn btn-lg btn-primary" type="submit"> + Sign in + </button> +</form> +{% endblock %}