I always get message: "Bad credentials" when I try to login in symfony2. I am doing this based to http://symfony.com/doc/current/cookbook/security/custom_provider.html. Please help me to figure out, where the problem is? Thanks in advance.
security.yml looks like this
security:
encoders:
Zags\UserBundle\Security\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
webservice:
id: zags_user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login_firewall:
pattern: ^/login$
anonymous: ~
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: /login
check_path: /login_check
#anonymous: ~
#http_basic:
# realm: "Secured Demo Area"
access_control:
- { path: ^/gender_type, roles: ROLE_USER }
#- { path: ^/_internal/secure, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
I have added these lines to routing.yml
login:
pattern: /login
defaults: { _controller: ZagsUserBundle:Security:login }
login_check:
pattern: /login_check
User.php class looks like this
<?php
namespace Zags\UserBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
private $username;
private $password;
private $salt;
private $roles;
public function __construct($username, $password, $salt, array $roles)
{
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->roles = $roles;
}
public function getRoles()
{
return $this->roles;
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
return $this->salt;
}
public function getUsername()
{
return $this->username;
}
public function eraseCredentials()
{
}
public function equals(UserInterface $user)
{
return $user->getUsername() === $this->username;
}
}
?>
So this is my UserProvider.php class
<?php
namespace Zags\UserBundle\Security\User;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class UserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
// make a call to your webservice here
$userData = array("username" => "latysh", "password" => "123", "salt" => "123", "roles" => array('ROLE_USER'));
// pretend it returns an array on success, false if there is no user
if ($userData) {
$username = $userData['username'];
$password = $userData['password'];
$salt = $userData['salt'];
$roles = $userData['roles'];
// ...
return new User($username, $password, $salt, $roles);
}
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'Zags\UserBundle\Security\User\User';
}
}
?>
and services.yml looks like this
parameters:
zags_user_provider.class: Zags\UserBundle\Security\User\UserProvider
services:
zags_user_provider:
class: "%zags_user_provider.class%"
SecurityController.php
<?php
namespace Zags\UserBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;
class SecurityController extends Controller
{
public function loginAction()
{
$request = $this->getRequest();
$session = $request->getSession();
// get the login error if there is one
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
} else {
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
$session->remove(SecurityContext::AUTHENTICATION_ERROR);
}
return $this->render('ZagsUserBundle:Security:login.html.twig', array(
// last username entered by the user
'last_username' => $session->get(SecurityContext::LAST_USERNAME),
'error' => $error,
));
}
}
?>
and login.html.twig
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form action="{{ path('login_check') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
{#
If you want to control the URL the user is redirected to on success (more details below)
<input type="hidden" name="_target_path" value="/account" />
#}
<button type="submit">login</button>
</form>
Has found answer to my question. Thanks to machour for responce. The problem was with SALT. So I updated the User.php class to
public function getSalt()
{
return '';
}
Then it logs in correctly or I should have encoded password with salt to successfully login. If one know how to do it, please write it as answer, YAHOO ))
$factory = $this->get('security.encoder_factory');
$encoder = $factory->getEncoder($user);
$pass = $encoder->encodePassword($user->getPassword(), $user->getSalt());
Related
Evening everyone , I've been stack with this problem since few days and whatever I change it remains .. basically on login when I put an invalid email it says email not found , when I put invalid password it says wrong password( I edited it of course ) but when they're both correct it shows Invalid credentials for some reason , I m really disparate .. help me please I can link any page code but Im 90% sure the code is correct ,thanks in advance
SecurityController.php
<?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('/index');
}
// 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.');
}
}
security.yaml
security:
encoders:
App\Entity\User5:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
app_user_provider:
entity:
class: 'App\Entity\User5'
property: 'email'
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
guard:
authenticators:
- App\Security\LoginFormAuthenticator
entry_point: App\Security\LoginFormAuthenticator
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
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
#access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
# - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
login.html.twig
{% extends 'home.html.twig' %}
{% block title %}Log in!{% endblock %}
{% block body %}
<form method="post" action="{{ path('app_login')}}">
{% 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 }}, Logout
</div>
{% endif %}
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label for="inputEmail">Email</label>
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" 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 %}
LoginFormAuthenticator
<?php
namespace App\Security;
use App\Entity\User5;
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\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\Guard\PasswordAuthenticatedInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
{
use TargetPathTrait;
private 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 = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
);
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(User5::class)->findOneBy(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Email could not be found.');
}
$user = $this->entityManager->getRepository(User5::class)->findOneBy(['password' => $credentials['password']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Wrong password.');
}
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, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
// For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
return new RedirectResponse($this->urlGenerator->generate('index'));
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate('app_login');
}
}
User5.php
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity()
* #UniqueEntity("email")
*/
/**
* #ORM\Entity(repositoryClass="App\Repository\User5Repository")
* #UniqueEntity(
* fields={"email"},
* message="That Email is already taken , try another "
* )
*/
class User5 implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(unique=true, type="string", nullable=false)
*/
private $email;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Role", mappedBy="Users")
*/
private $userRoles;
public function __construct()
{
$this->userRoles = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->email;
}
/**
* #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
{
$this->password = $password;
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;
}
/**
* #return Collection|Role[]
*/
public function getUserRoles(): Collection
{
return $this->userRoles;
}
public function addUserRole(Role $userRole): self
{
if (!$this->userRoles->contains($userRole)) {
$this->userRoles[] = $userRole;
$userRole->addUser($this);
}
return $this;
}
public function removeUserRole(Role $userRole): self
{
if ($this->userRoles->contains($userRole)) {
$this->userRoles->removeElement($userRole);
$userRole->removeUser($this);
}
return $this;
}
}
You should remove this lines, the password is encrypted in the database so you can't retrieve a user by his password, Symfony will do this job (comparing the two passwords).
$user = $this->entityManager->getRepository(User5::class)->findOneBy(['password' => $credentials['password']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Wrong password.');
}
I solved it my self , apparently you cant use guard and form login in the same time you gotta pick one of them because both do the same job one is easy and one is customized also I forgot something very important in the register function , forgot to encode the password , now it works just fine
heres the new register function
public function register(Request $request , EntityManagerInterface $entityManager, UserPasswordEncoderInterface $encoder)
{
$user = new User5();
$form = $this->createForm(Form::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$hashed = $encoder->encodePassword($user, $user->getPassword());
$user->setPassword($hashed);
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash("success", "Welcome to our application");
return $this->redirectToRoute("app_login");
}
return $this->render('account/registration.html.twig',[
'form' => $form->createView()
]);
}
new security.yaml
security:
encoders:
App\Entity\User5:
algorithm: bcrypt
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
app_user_provider:
entity:
class: 'App\Entity\User5'
property: 'email'
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
provider: app_user_provider
form_login:
login_path: app_login
check_path: app_login
# default_target_path: dashboard
logout:
path: logout_user
target: index
# target: app_any_route
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
#access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
# - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
# - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
I’m currently discovering symfony.I am trying to code some user account. There are two role : ADMIN_ROLE or USER_ROLE. When I try to connect an account, I have this message « Authentication request could not be processed due to a system problem. » even when I enter a wrong username/password !
I have been looking for a solution for days (in docs and on internet).
I can fill my database with my user thanks to the fixture (I can see it in SQLyog). I tried to erase ma database to create it again.
This is my security.yaml :
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
form_login:
login_path: login
check_path: login
logout:
path: app_logout
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: ROLE_USER }
This is my UtilisateurFixtures (to create false data)
<?php
namespace App\DataFixtures;
use App\Entity\Utilisateur;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\User\User;
//use Symfony\Bridge\Doctrine\Tests\Fixtures\User;
class UtilisateurFixtures extends Fixture
{
/**
* #var UserPasswordEncoderInterface
*/
private $encoder;
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
public function load(ObjectManager $manager)
{
$user = new Utilisateur();
$user->setUsername('demo3');
$user->setPassword($this->encoder->encodePassword($user,'demo3'));
$user->setRoles(['ROLE_USER', 'ROLE_ADMIN']);
$manager->persist($user);
$manager->flush();
}
}
This is my SecurityController :
<?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="login")
*/
public function login(AuthenticationUtils $authenticationUtils): Response
{
// 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", methods={"GET"})
*/
public function logout()
{
// controller can be blank: it will never be executed!
throw new \Exception('Don\'t forget to activate logout in security.yaml');
}
}
This is my Utilisateur.php :
<?php
namespace App\DataFixtures;
use App\Entity\Utilisateur;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\User\User;
//use Symfony\Bridge\Doctrine\Tests\Fixtures\User;
class UtilisateurFixtures extends Fixture
{
/**
* #var UserPasswordEncoderInterface
*/
private $encoder;
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
public function load(ObjectManager $manager)
{
$user = new Utilisateur();
$user->setUsername('demo3');
$user->setPassword($this->encoder->encodePassword($user,'demo3'));
$user->setRoles(['ROLE_USER', 'ROLE_ADMIN']);
$manager->persist($user);
$manager->flush();
}
}
This is my login.htlml.twig (where there is my form) :
{% extends 'base.html.twig' %}
{% block title %}Log in!{% endblock %}
{% block body %}{% if error %}
{{ error.messageKey|trans(error.messageData, 'security') }}
{% endif %}
<form action="{{ path('login') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
<input type="hidden" name="_target_path" value="/" />
{#
If you want to control the URL the user
is redirected to on success (more details below)
<input type="hidden" name="_target_path" value="/account" />
#}
<button type="submit">login</button>
</form>
{% endblock %}
This is UtilisateurRepository :
<?php
namespace App\Repository;
use App\Entity\Utilisateur;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
use Symfony\Bridge\Doctrine\RegistryInterface;
/**
• #method Utilisateur|null find($id, $lockMode = null, $lockVersion = null)
• #method Utilisateur|null findOneBy(array $criteria, array $orderBy = null)
• #method Utilisateur[] findAll()
• #method Utilisateur[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class UtilisateurRepository extends ServiceEntityRepository
{
public function __construct(RegistryInterface $registry)
{
parent::__construct($registry, Utilisateur::class);
}
// /**
// * #return Utilisateur[] Returns an array of Utilisateur objects
// /
/
public function findByExampleField($value)
{
return $this->createQueryBuilder('u')
->andWhere('u.exampleField = :val')
->setParameter('val', $value)
->orderBy('u.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Utilisateur
{
return $this->createQueryBuilder('u')
->andWhere('u.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}
Thank you for your help ! Don't hesitate if you need anything.
I'm building a custom GuardAuthenticator to login with a token on a specific route. According to the documentation if supportsRememberMe() returns true and remember_me is activated in the firewall, the remember me cookie should be set, but it's not (although it is set if I use a form login authentication on another route).
The route:
/**
* #Route("/login/token/{id}/{token}/{force}", defaults={"force"=0}, name="login_token")
*/
public function loginToken()
{
}
The GuardAuthenticator:
<?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\HttpKernel\Exception\HttpException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class TokenLoginAuthenticator extends AbstractGuardAuthenticator
{
use TargetPathTrait;
private $em;
private $force;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function supports(Request $request)
{
return 'login_token' === $request->attributes->get('_route') && $request->isMethod('GET');
}
public function getCredentials(Request $request)
{
$credentials = [
'id' => $request->attributes->get('id'),
'token' => $request->attributes->get('token')
];
$this->force = $request->attributes->get('force');
return $credentials;
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = $this->em->getRepository(User::class)->find($credentials['id']);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('No user found.');
}
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($user->getToken() === $credentials['token']) {
return true;
}
throw new HttpException(403, "Forbidden");
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
throw new HttpException(403, "Forbidden");
}
public function start(Request $request, AuthenticationException $authException = null)
{
}
public function supportsRememberMe()
{
return true;
}
}
The security config:
security:
encoders:
App\Entity\User:
id: 'App\Security\PasswordEncoder'
providers:
in_memory: { memory: ~ }
orm:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
form_login:
login_path: login
check_path: login
provider: orm
csrf_token_generator: security.csrf.token_manager
default_target_path: homepage
logout:
path: /logout
target: /
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
# by default, the feature is enablered by checking a
# checkbox in the login form (see below), uncomment the
# following line to always enable it.
# always_remember_me: true
guard:
provider: orm
authenticators:
- App\Security\TokenLoginAuthenticator
In Symfony 5.2.6 dont forget to add new RememberMeBadge() to the authenticate function:
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
public function authenticate(Request $request): PassportInterface
{
$email = $request->request->get('email', '');
$request->getSession()->set(Security::LAST_USERNAME, $email);
return new Passport(
new UserBadge($email),
new PasswordCredentials($request->request->get('password', '')),
[
new CsrfTokenBadge('authenticate', $request->get('_csrf_token')),
new RememberMeBadge(),
]
);
}
}
Remember me cookie will be set if all of the following are met:
The supportsRememberMe() method returns true.
The remember_me key in the firewall is configured.
The (default) _remember_me parameter is sent in the request. This is usually done by having a _remember_me checkbox in a login form (but it can be sent as url param (?_remember_me=1), or we can configure the firewall remember_me key to always_remember_me.
The onAuthenticationSuccess() method returns a Response object.
If a page needs authentication and no User is found Symfony simply redirects or shows the login page. So simple enough I got that working.
Next, I would like to send a custom message (or html) if the User makes an Ajax call inside a page that requires authentication, but the session has died for instance (the User is not authenticated anymore).
security.yml
security:
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
providers:
db_provider:
entity:
class: AppBundle:User
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
pattern: ^/
form_login:
login_path: security_login
check_path: security_login
use_forward: false
failure_handler: AppBundle\Security\AuthenticationHandler
logout:
path: /logout
target: /
access_denied_handler: AppBundle\Security\AccessDeniedHandler
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: ROLE_ADMIN }
I have tried to intercept an event error by using access_denied_handler or failure_handler.
AppBundle\Security\AccessDeniedHandler.php
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
class AccessDeniedHandler implements AccessDeniedHandlerInterface {
public function handle(Request $request, AccessDeniedException $exception) {
return new JsonResponse([
'success' => 0,
'error' => 1,
'message' => $exception -> getMessage(),
'from' => 'AccessDeniedHandler'
]);
}
}
AppBundle\Security\AuthenticationHandler.php
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class AuthenticationHandler implements AuthenticationFailureHandlerInterface {
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) {
return new JsonResponse(['error' => 1, 'from' => 'AuthenticationHandler']);
}
}
None of those classes are accessed. What am I missing?
Notes
Created for a Symfony 3.4 project, should be compatible with Symfony 4, but I haven't tested;
All services are auto-wired, so there's nothing to add in services.yml
I'm not using FOSUserBundle;
I'm not following Symfony coding standars;
I've made notes, here and there; also I've put some comments in the code itself;
The important part is at the end (LoginFormAuthenticator), I'm posting the whole code, hopefully someone will have an easier time than me.
Source of inspiration:
https://symfony.com/doc/3.4/security.html
https://symfonycasts.com/screencast/symfony3-security
https://www.sitepoint.com/easier-authentication-with-guard-in-symfony-3/
Wall of code
security.yml
Security configuration
For the "memory" user the username and password is "admin"
security:
encoders:
Symfony\Component\Security\Core\User\User:
algorithm: bcrypt
AppBundle\Entity\User:
algorithm: bcrypt
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
chain_provider:
chain:
providers: [memory_provider, db_provider]
memory_provider:
memory:
users:
admin:
password: '$2y$13$21gXkzksqlR68HhAYB2WLOqcQvJZzgIrSH/KRq1aEzkkOnjI7lR9e'
roles: 'ROLE_SUPER_ADMIN'
db_provider:
entity:
class: AppBundle:User
property: email
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
pattern: ^/
logout:
path: /logout
target: /
guard:
authenticators:
- AppBundle\Security\LoginFormAuthenticator
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: ROLE_USER }
Template app/Resources/views/Security/_content.login.html.twig
{% set form_action = path('security_login') %}
<form action="{{ form_action }}" method="post" autocomplete="off" id="login_f">
{% if error %}
<div class="login_form_error">{{ error.messageKey }}</div>
{% endif %}
<div class="closed">
<input type="hidden" name="_csrf_token" value="{{ csrf_token(login_csrf_token) }}" />
</div>
<div class="login_field login_field_0">
<label for="login_username" class="login_l">
<i class="fas fa-user"></i>
</label>
<input type="text" class="login_i" id="login_username" name="_username" placeholder="Username" />
</div>
<div class="login_field login_field_1">
<label for="login_password" class="login_l">
<i class="fas fa-key"></i>
</label>
<input type="password" class="login_i" id="login_password" name="_password" placeholder="Password" />
</div>
<div>
<input type="submit" class="login_bttn" id="_submit" value="Login" />
</div>
</form>
Template app/Resources/views/Security/login.html.twig
No need for base.html.twig
{% extends 'base.html.twig' %}
{% block content %}
<div id="login_c">
{% include 'Security/_content.login.html.twig' %}
</div>
{% endblock %}
The service
Renders the login page or the login content
Replace the CSRF_TOKEN constant value with your own
namespace AppBundle\Services\User;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class LoginFormService {
private $templatingEngine;
private $authenticationUtils;
const CSRF_TOKEN = 'login:token:w4kSzA3v5VJyb4aWLbV7stAY92cNwgL77J6QrXpU!';
function __construct(
EngineInterface $templatingEngine,
AuthenticationUtils $authenticationUtils) {
$this -> templatingEngine = $templatingEngine;
$this -> authenticationUtils = $authenticationUtils;
}
function getHtml($contentOnly = False) {
// last username entered by the user
$lastUsername = $this -> authenticationUtils -> getLastUsername();
// get the login error if there is one
$error = $this -> authenticationUtils -> getLastAuthenticationError();
$html_vars = array(
'lastUsername' => $lastUsername,
'error' => $error,
'login_csrf_token' => self::CSRF_TOKEN,
);
$html_template = 'Security/login.html.twig';
if ( $contentOnly ) {
$html_template = 'Security/_content.login.html.twig';
}
$html = $this -> templatingEngine -> render($html_template, $html_vars);
return $html;
}
}
The login controller
A simple buffer controller to render the login page if the user will access /login
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use AppBundle\Services\User\LoginFormService;
class SecurityController extends Controller {
/**
#Route("/login", name="security_login")
*/
public function loginAction(LoginFormService $loginFormService, Request $request) {
return new Response($loginFormService -> getHtml());
}
/**
#Route("/logout", name="security_logout")
*/
public function logoutAction() {}
}
The Guard authenticator
Instead of "project_homepage_route" use whatever route you want
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Routing\RouterInterface;
use AppBundle\Services\User\LoginFormService;
class LoginFormAuthenticator extends AbstractGuardAuthenticator {
private $router;
private $templatingEngine;
private $passwordEncoder;
private $csrfTokenManager;
private $loginService;
protected $auth_error_csrf = 'Invalid CSRF token!!!';
protected $auth_error_message = 'Invalid credentials!!!';
function __construct(
RouterInterface $router,
UserPasswordEncoderInterface $passwordEncoder,
CsrfTokenManagerInterface $csrfTokenManager,
LoginFormService $loginService) {
$this -> router = $router;
$this -> passwordEncoder = $passwordEncoder;
$this -> csrfTokenManager = $csrfTokenManager;
$this -> loginService = $loginService;
}
/* Methods */
protected function loginResponse(Request $request, $forbidden = False) {
// The javascript library must set the 'X-Requested-With' header
// xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
if ( $request -> isXmlHttpRequest() ) {
$response = new JsonResponse([
'error' => 1,
'html' => $this -> loginService -> getHtml(True)
]);
} else {
$html = $this -> loginService -> getHtml();
$response = new Response($html);
}
if ($forbidden) {
$response -> setStatusCode(Response::HTTP_FORBIDDEN);
}
return $response;
}
/* AbstractGuardAuthenticator methods */
public function supports(Request $request) {
return $request -> attributes -> get('_route') === 'security_login' && $request -> isMethod('POST');
}
public function getCredentials(Request $request) {
// Add csrf protection
$csrfData = $request -> request -> get('_csrf_token');
$csrfToken = new CsrfToken(LoginFormService::CSRF_TOKEN, $csrfData);
if ( !$this -> csrfTokenManager -> isTokenValid($csrfToken) ) {
throw new InvalidCsrfTokenException( $this -> auth_error_csrf );
}
return array(
'username' => $request -> request -> get('_username'),
'password' => $request -> request -> get('_password'),
);
}
public function getUser($credentials, UserProviderInterface $userProvider) {
$username = $credentials['username'];
try {
return $userProvider -> loadUserByUsername($username);
} catch (UsernameNotFoundException $e) {
throw new CustomUserMessageAuthenticationException( $this -> auth_error_message );
}
return null;
}
public function checkCredentials($credentials, UserInterface $user) {
$is_valid_password = $this -> passwordEncoder -> isPasswordValid($user, $credentials['password']);
if ( !$is_valid_password ) {
throw new CustomUserMessageAuthenticationException( $this -> auth_error_message );
return;
}
return True;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $authException) {
$session = $request -> getSession();
$session -> set(Security::AUTHENTICATION_ERROR, $authException);
$session -> set(Security::LAST_USERNAME, $request -> request -> get('_username'));
// Shows the login form instead of the page content
return $this -> loginResponse($request, True);
// If you want redirect make sure the line below is used
// return new RedirectResponse($this -> router -> generate('security_login'));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) {
if ( $request -> isXmlHttpRequest() ) {
return new JsonResponse([
'success' => 1,
'message' => 'Authentication success!'
]);
}
return new RedirectResponse($this -> router -> generate('project_homepage_route'));
}
public function start(Request $request, AuthenticationException $authException = null) {
// Shows the login form instead of the page content
return $this -> loginResponse($request);
// If you want redirect make sure the line below is used
// return new RedirectResponse($this -> router -> generate('security_login'));
}
public function supportsRememberMe() {
return false;
}
}
I am implementing a login system with guard in my symfony application.
I already setup the system, but i did one mistake, therefore i cant login...
The essentials:
My User Entity implements "AdvancedUserInterface, \Serializable" and it provides email property instead of username property..moreover i changed "getUsername"-function to:
public function getUsername()
{
return $this->email;
}
security.yml:
# app/config/security.yml
security:
providers:
main_db_provider:
entity:
class: AppBundle:User
property: email
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
logout:
path: /logout
target: /
guard:
authenticators:
- form_authenticator
provider: main_db_provider
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_ADMIN }
services.yml:
services:
form_authenticator:
class: AppBundle\Security\FormAuthenticator
arguments: ["#service_container"]
login.html.twig:
<form action="{{ path('login') }}" method="POST">
<input type="text" name="email">
<input type="text" name="password">
<input type="submit" name="submit" value="Submit">
</form>
LoginController(part of it):
/**
* #Route("/login", name="login")
*/
public function loginAction(Request $request) {
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render(
'AppBundle:login:index.html.twig',
[
'error' => $error ? $error->getMessage() : NULL,
'last_username' => $lastUsername
]
);
}
And last my FormAuthentificator:
class FormAuthenticator extends AbstractGuardAuthenticator
{
private $container;
/**
* Default message for authentication failure.
*
* #var string
*/
private $failMessage = 'Invalid credentials';
/**
* Creates a new instance of FormAuthenticator
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* {#inheritdoc}
*/
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) {
return;
}
return array(
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
);
}
/**
* {#inheritdoc}
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
$email = $credentials['email'];
return $userProvider->loadUserByUsername($email);
}
/**
* {#inheritdoc}
*/
public function checkCredentials($credentials, UserInterface $user)
{
$plainPassword = $credentials['password'];
$encoder = $this->container->get('security.password_encoder');
if (!$encoder->isPasswordValid($user, $plainPassword)) {
throw new CustomUserMessageAuthenticationException($this->failMessage);
}
return true;
}
/**
* {#inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->container->get('router')->generate('backend');
return new RedirectResponse($url);
}
/**
* {#inheritdoc}
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$url = $this->container->get('router')->generate('login');
return new RedirectResponse($url);
}
/**
* {#inheritdoc}
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->container->get('router')->generate('login');
return new RedirectResponse($url);
}
/**
* {#inheritdoc}
*/
public function supportsRememberMe()
{
return false;
}
}
When i enter my valid credentials, i get:
Invalid credentials:
I also tried with other credentials, but always same error.
Anybody could help me to solve this issue?
Thanks and Greetings!