login stuff

This commit is contained in:
overflowerror 2021-01-05 19:24:38 +01:00
parent da3bbbe08a
commit bfb864998e
11 changed files with 286 additions and 47 deletions

View file

@ -50,7 +50,7 @@
"symfony/maker-bundle": "^1.0", "symfony/maker-bundle": "^1.0",
"symfony/phpunit-bridge": "^5.2", "symfony/phpunit-bridge": "^5.2",
"symfony/stopwatch": "^5.2", "symfony/stopwatch": "^5.2",
"symfony/var-dumper": "^5.2", "symfony/var-dumper": "5.2.*",
"symfony/web-profiler-bundle": "^5.2" "symfony/web-profiler-bundle": "^5.2"
}, },
"config": { "config": {

2
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c58039be4de71978e7105749e19393f5", "content-hash": "90b92968037c42cf1b71fcc5e6772761",
"packages": [ "packages": [
{ {
"name": "composer/package-versions-deprecated", "name": "composer/package-versions-deprecated",

View file

@ -1,7 +1,15 @@
security: security:
encoders:
App\Entity\User:
algorithm: auto
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers: providers:
users_in_memory: { memory: null } # used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: name
firewalls: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
@ -9,7 +17,14 @@ security:
main: main:
anonymous: true anonymous: true
lazy: true lazy: true
provider: users_in_memory provider: app_user_provider
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
# where to redirect after logout
# target: app_any_route
# activate different ways to authenticate # activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication # https://symfony.com/doc/current/security.html#firewalls-authentication

View file

@ -0,0 +1,37 @@
<?php
namespace App\Controller;
use LogicException;
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.');
}
}

View file

@ -1,29 +0,0 @@
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class UserController extends AbstractController
{
/**
* @Route("/login", name="user_login")
*/
public function login(): Response
{
$form = $this->createFormBuilder()
->add("username", TextType::class)
->add("password", PasswordType::class)
->add("save", SubmitType::class)
->getForm();
return $this->render("user/login.html.twig", [
"form" => $form->createView()
]);
}
}

View file

@ -5,9 +5,17 @@ namespace App\DataFixtures;
use App\Entity\User; use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager; use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserFixtures extends Fixture class UserFixtures extends Fixture
{ {
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
public function load(ObjectManager $manager) public function load(ObjectManager $manager)
{ {
// $product = new Product(); // $product = new Product();
@ -15,7 +23,7 @@ class UserFixtures extends Fixture
$admin = new User(); $admin = new User();
$admin->setName("admin"); $admin->setName("admin");
$admin->setPassword("password"); $admin->setPassword($this->passwordEncoder->encodePassword($admin, "password"));
$manager->flush(); $manager->flush();
} }

View file

@ -4,11 +4,12 @@ namespace App\Entity;
use App\Repository\UserRepository; use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/** /**
* @ORM\Entity(repositoryClass=UserRepository::class) * @ORM\Entity(repositoryClass=UserRepository::class)
*/ */
class User class User implements UserInterface
{ {
/** /**
* @ORM\Id * @ORM\Id
@ -18,12 +19,18 @@ class User
private $id; private $id;
/** /**
* @ORM\Column(type="string", length=255) * @ORM\Column(type="string", length=180, unique=true)
*/ */
private $name; private $name;
/** /**
* @ORM\Column(type="string", length=255) * @ORM\Column(type="json")
*/
private $roles = [];
/**
* @var string The hashed password
* @ORM\Column(type="string")
*/ */
private $password; private $password;
@ -44,9 +51,41 @@ class User
return $this; return $this;
} }
public function getPassword(): ?string /**
* A visual identifier that represents this user.
*
* @see UserInterface
*/
public function getUsername(): string
{ {
return $this->password; return (string)$this->name;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return (string)$this->password;
} }
public function setPassword(string $password): self public function setPassword(string $password): self
@ -55,4 +94,21 @@ class User
return $this; return $this;
} }
/**
* @see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
} }

View file

@ -5,6 +5,10 @@ namespace App\Repository;
use App\Entity\User; use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry; use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use function get_class;
/** /**
* @method User|null find($id, $lockMode = null, $lockVersion = null) * @method User|null find($id, $lockMode = null, $lockVersion = null)
@ -12,13 +16,27 @@ use Doctrine\Persistence\ManagerRegistry;
* @method User[] findAll() * @method User[] findAll()
* @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/ */
class UserRepository extends ServiceEntityRepository class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
{ {
public function __construct(ManagerRegistry $registry) public function __construct(ManagerRegistry $registry)
{ {
parent::__construct($registry, User::class); parent::__construct($registry, User::class);
} }
/**
* Used to upgrade (rehash) the user's password automatically over time.
*/
public function upgradePassword(UserInterface $user, string $newEncodedPassword): void
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
$user->setPassword($newEncodedPassword);
$this->_em->persist($user);
$this->_em->flush();
}
// /** // /**
// * @return User[] Returns an array of User objects // * @return User[] Returns an array of User objects
// */ // */

View file

@ -0,0 +1,98 @@
<?php
namespace App\Security;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
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\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
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\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
}
public function supports(Request $request)
{
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function getCredentials(Request $request)
{
$credentials = [
'name' => $request->request->get('name'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['name']
);
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(['name' => $credentials['name']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Name could not be found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
// Check the user's password or other credentials and return true or false
// If there are no credentials to check, you can just return true
throw new Exception('TODO: check the credentials inside ' . __FILE__);
}
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);
}
}

View file

@ -0,0 +1,43 @@
{% 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="inputName">Name</label>
<input type="text" value="{{ last_username }}" name="name" id="inputName" class="form-control" required
autofocus>
<label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" 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 %}

View file

@ -1,7 +0,0 @@
{% extends "base.html.twig" %}
{% block title %}Login{% endblock %}
{% block body %}
{{ form(form) }}
{% endblock %}