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.
Related
My remember function for my symfony application seems to be not working appropriately. I've followed the resource provided by Symfony itself here.
Anyway, here is part of my security.yaml file:
security:
enable_authenticator_manager: true
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
form_login:
login_path: /p/login
entry_point: form_login
# login_throttling:
# limiter: app.custom.limiter
lazy: true
provider: app_user_provider
# https://symfony.com/doc/current/security/impersonating_user.html
switch_user: true
custom_authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: logout
# where to redirect after logout
# target: app_any_route
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
token_provider:
doctrine: true
My LoginFormAuthenticator.php file:
<?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\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\RateLimiter\RateLimiterFactory;
class LoginFormAuthenticator extends AbstractAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'login';
private $entityManager;
private $urlGenerator;
private $csrfTokenManager;
private $passwordEncoder;
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordHasherInterface $passwordEncoder)
{
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
}
public function supports(Request $request): ?bool
{
return self::LOGIN_ROUTE === $request->attributes->get('_route')
&& $request->isMethod('POST');
}
public function authenticate(Request $request): Passport
{
$email = $request->request->get('email');
$password = $request->request->get('password');
$csrfToken = $request->request->get('_csrf_token');
return new Passport(
new UserBadge($email),
new PasswordCredentials($password),
[
new CsrfTokenBadge('authenticate', $csrfToken),
new RememberMeBadge()
],
);
}
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(['email' => $credentials['email']]);
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Invalid credentials.');
}
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): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('feed', ['page_num' => 1]));
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse($this->urlGenerator->generate('login'));
}
protected function getLoginUrl()
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
The database schema that was created by using the doctrine as the token provider:
The user gets logged in but then logs out automatically after an hour (I believe) which has gotten annoying. Even though 1 week has been specified in the security.yaml file.
UPDATE
After reading threads via github (here), it seems this approach isn't viable. What alternative should we use to allow our users to stay logged in within the specified duration that we note in our security.yaml file?
Consider that if we use cookies or regular sessions for user log ins, every user will be logged out every time we deploy because of the cache:clear function.
Goto user entity and find getUserIdentyfier() function. Make sure this returns email.
Property: email<
Symfony 5.3
security.yaml
security:
...
erase_credentials: false
LoginListener.php
<?php
namespace App\EventListener;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
class LoginListener
{
private $passwordHasherFactory;
private $em;
public function __construct(PasswordHasherFactoryInterface $passwordHasherFactory, EntityManagerInterface $em)
{
$this->passwordHasherFactory = $passwordHasherFactory;
$this->em = $em;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
{
$user = $event->getAuthenticationToken()->getUser();
$token = $event->getAuthenticationToken();
// Migrate the user to the new hashing algorithm if is using the legacy one
if ($user->hasLegacyPassword()) {
// Credentials can be retrieved thanks to the false value of
// the erase_credentials parameter in security.yml
$plainPassword = $token->getCredentials();
file_put_contents('darius.txt', 'test'.$plainPassword, FILE_APPEND); // why null?
}
$token->eraseCredentials();
}
}
https://symfony.com/doc/current/reference/configuration/security.html#erase-credentials
If true, the eraseCredentials() method of the user object is called
after authentication.
So probably if false it should not erase? Why it is erasing?
Password is received because login works. I just dissapears at some point.
Update
Question is why credentials are null before calling
$token->eraseCredentials();
Managed to install xdebug and found that when creating token the credentials are not set:
JsonLoginAuthenticator:
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
return new UsernamePasswordToken($passport->getUser(), null, $firewallName, $passport->getUser()->getRoles());
}
UsernamePasswordToken:
public function __construct($user, $credentials, string $firewallName, array $roles = [])
{
parent::__construct($roles);
if ('' === $firewallName) {
throw new \InvalidArgumentException('$firewallName must not be empty.');
}
$this->setUser($user);
$this->credentials = $credentials;
$this->firewallName = $firewallName;
parent::setAuthenticated(\count($roles) > 0);
}
So I guess that is the problem - under security - firewall there is such setup:
main:
lazy: true
provider: app_user_provider
logout:
path: logout
target: after_logout
json_login:
check_path: /login
entry_point: App\Security\AuthenticationEntryPoint
So probably I have answered my question why there is no credentials. I am just missing now how do create new password hash for password on login, but that is probably for different question.
I'm using Symfony 4 "Custom Authentication System with Guard (API Token Example)"Custom Authentication System with Guard (API Token Example)
I want to generate api token when user register from other app(i.e Advance Rest Client) and then want to use this token to access other api's like get articles list.
There is no option to generate token or may be I can't find out.
Here is my code:
config/packages/security.yaml
security:
encoders:
App\Entity\User:
algorithm: bcrypt
# 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: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api/
stateless: true
anonymous: false
guard:
authenticators:
- App\Security\TokenAuthenticator
main:
pattern: ^/
anonymous: true
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
# activate different ways to authenticate
# http_basic: true
# https://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate
# form_login: true
# https://symfony.com/doc/current/security/form_login_setup.html
# 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: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
Controller/RegistrationController.php
class RegistrationController extends AbstractController{
/**
* #Route("/register/api", name="app_register_api")
*/
public function registerApi(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, TokenAuthenticator $authenticator){
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// do anything else you need here, like send an email
$data = $guardHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$authenticator,
'api' // firewall name in security.yaml
);
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
}
}
Security/TokenAuthenticator.php
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning false will cause this authenticator
* to be skipped.
*/
public function supports(Request $request)
{
return $request->headers->has('X-AUTH-TOKEN');
}
/**
* Called on every request. Return whatever credentials you want to
* be passed to getUser() as $credentials.
*/
public function getCredentials(Request $request)
{
return [
'token' => $request->headers->get('X-AUTH-TOKEN'),
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$apiToken = $credentials['token'];
if (null === $apiToken) {
return;
}
// if a User object, checkCredentials() is called
return $this->em->getRepository(User::class)
->findOneBy(['apiToken' => $apiToken]);
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
// no credential check is needed in this case
// return true to cause authentication success
return true;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = [
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
// you might translate this message
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supportsRememberMe()
{
return false;
}
}
Next problem is how to get user detail from token in articles api?
<?php
namespace App\Controller\Rest;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use FOS\RestBundle\Controller\FOSRestController;
use FOS\RestBundle\Controller\Annotations as Rest;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use App\Entity\Article;
use App\Form\ArticleType;
use App\Service\ArticleService;
class ArticleController extends FOSRestController
{
/**
* #var ArticleService
*/
private $articleService;
/**
* ArticleController constructor.
* #param ArticleService $articleService
*/
public function __construct(ArticleService $articleService)
{
$this->articleService = $articleService;
}
/**
* Lists all Articles.
* http://sf4.local/api/articles
* #Rest\Get("/articles")
*
* #return Response
*/
public function getArticles()
{
$items = $this->articlesService->getAllArticles();
return $this->handleView($this->view($items));
}
}
I would like to write a basic login form, which authenticates users by sending a request to an external REST API. The external API receives the login/password and return 200 (ok) if the credentials are correct.
However, I can't implement it via the UserProviderInterface, because the external REST API give me the password in the reply. (I can't fill the user password in the loadUserByUsername method).
I found a valid solution here, but it uses classes that have been removed in Symfony 3 : Symfony2 custom connection by web service
I made a test with a custom Authenticator, which only checks that the password is "toto", but I get a redirection loop and my dummy UserProvider is still called :
<?php
namespace AppBundle\Security\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimpleFormAuthenticatorInterface;
class WebserviceAuthenticator implements SimpleFormAuthenticatorInterface
{
private $encoder;
public function __construct(UserPasswordEncoderInterface $encoder)
{
$this->encoder = $encoder;
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
$user = new WebserviceUser($token->getUsername(), $token->getCredentials(), null, ['ROLE_ADMIN']);
// HERE : call the external REST API
if ($token->getCredentials() === 'toto') {
$token = new UsernamePasswordToken(
$user,
$user->getPassword(),
'main',
$user->getRoles()
);
return $token;
}
throw new CustomUserMessageAuthenticationException('Invalid username or password');
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UsernamePasswordToken
&& $token->getProviderKey() === $providerKey;
}
public function createToken(Request $request, $username, $password, $providerKey)
{
return new UsernamePasswordToken($username, $password, $providerKey);
}
}
I got it working with that implementation :
security.yml
providers:
webservice:
id: AppBundle\Security\User\WebserviceUserProvider
encoders:
AppBundle\Entity\WebserviceUser: plaintext
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: ~
provider: webservice
pattern: ^/
form_login:
check_path: login
login_path: login
use_forward: true
logout: ~
guard:
authenticators:
- app.webservice_authenticator
login:
pattern: ^/login$
anonymous: ~
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/cache, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
role_hierarchy:
ROLE_ADMIN: ROLE_USER
services.yml
services:
app.webservice_authenticator:
class: AppBundle\Security\User\WebserviceAuthenticator
User Provider
namespace AppBundle\Security\User;
use AppBundle\Entity\WebserviceUser;
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 WebserviceUserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
return new WebserviceUser($username, null, null, ['ROLE_USER']);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $user;
}
public function supportsClass($class)
{
return WebserviceUser::class === $class;
}
}
Authenticator
<?php
namespace AppBundle\Security\User;
use AppBundle\Service\RestClient;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
class WebserviceAuthenticator extends AbstractFormLoginAuthenticator
{
private $container;
private $restClient;
public function __construct(ContainerInterface $container, RestClient $restClient)
{
$this->container = $container;
$this->restClient = $restClient;
}
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login' || $request->getMethod() != 'POST') {
return;
}
$username = $request->request->get('_username');
$request->getSession()->set(Security::LAST_USERNAME, $username);
$password = $request->request->get('_password');
return array(
'username' => $username,
'password' => $password
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
//dump($credentials); die();
if (array_key_exists('username', $credentials) == false) {
return null;
}
$username = $credentials['username'];
$password = strtoupper($credentials['password']);
if ($username == '') {
return null;
}
// Here the business code, provide your own implementtion
if ($this->restClient->IsValidLogin($username, $password)) {
return new WebserviceUser($username, $password, null, ['ROLE_USER']);
} else {
throw new CustomUserMessageAuthenticationException('Invalid credentials');
}
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// AJAX! Return some JSON
if ($request->isXmlHttpRequest()) {
return new JsonResponse(array('message' => $exception->getMessageKey()), 403);
}
// for non-AJAX requests, return the normal redirect
return parent::onAuthenticationFailure($request, $exception);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// AJAX! Return some JSON
if ($request->isXmlHttpRequest()) {
return new JsonResponse(array('userId' => $token->getUser()->getId()));
}
// for non-AJAX requests, return the normal redirect
return parent::onAuthenticationSuccess($request, $token, $providerKey);
}
protected function getLoginUrl()
{
return $this->container->get('router')
->generate('login');
}
protected function getDefaultSuccessRedirectUrl()
{
return $this->container->get('router')
->generate('homepage');
}
}
The trick seems to be :
to implement password validation in the getUser method of the authenticator, and have checkCredentials method always return true.
to disable the refreshUser method of UserProvider
This question already has an answer here:
Symfony & Guard: "The security token was removed due to an AccountStatusException"
(1 answer)
Closed 5 years ago.
I try to make a form login authentication with guard (symfony 3.2) but it doesn't work.
The authentication is working, but when I'm redirected to the home page (accueil), I'm redirected to the login page without anthentication.
If I put in the controler of my home page
$user = $this->get('security.token_storage')->getToken();
dump($user); die;
I can see my user, the role but he is not authenticated.
DashboardController.php on line 23:
PostAuthenticationGuardToken {#133 ▼
-providerKey: "main"
-user: User {#457 ▶}
-roles: array:1 [▼
0 => Role {#120 ▼
-role: "ROLE_SUPERADMIN"
}
]
-authenticated: false
-attributes: []
}
What I've missed ?
Security.ym
security:
encoders:
EntBundle\Entity\User\User:
algorithm: bcrypt
providers:
database:
entity:
class: EntBundle:User\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
logout: ~
guard:
authenticators:
- ent.login_authenticator
TestAuthenticator.php
namespace EntBundle\Security;
use Doctrine\ORM\EntityManager;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
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\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class TestAuthenticator extends AbstractGuardAuthenticator
{
private $em;
private $router;
public function __construct(EntityManager $em, RouterInterface $router)
{
$this->em = $em;
$this->router = $router;
}
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) {
return;
}
return [
'username' => $request->request->get('_username'),
'password' => $request->request->get('_password'),
];
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
return $this->em->getRepository('EntBundle:User\User')->findOneBy(['username' => $username]);
}
public function checkCredentials($credentials, UserInterface $user)
{
// this is just for test
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$url = $this->router->generate('accueil');
return new RedirectResponse($url);
}
public function start(Request $request, AuthenticationException $authException = null)
{
$url = $this->router->generate('login');
return new RedirectResponse($url);
}
public function supportsRememberMe()
{
return false;
}
}
DashboardController.php
namespace EntBundle\Controller\Dashboard;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DashboardController extends Controller
{
/**
* #Route("/accueil", name="accueil")
*/
public function indexAction()
{
$user = $this->get('security.token_storage')->getToken();
dump($user); die;
return $this->render('EntBundle:dashboard:dashboard_structure.html.twig');
}
/**
* #Route("/login", name="login")
*/
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('EntBundle::login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
/**
* #Route("/logout", name="logout")
*/
public function logoutAction()
{
}
}
EDIT:
Thanks leo_ap for your help but the problem doesnt come from there.
The config session is like this :
session:
handler_id: session.handler.native_file
save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%"
and if I check in the save path folder I have session file created but not authenticated.
_sf2_attributes|a:1:{s:26:"_security.main.target_path";s:29:"http://localhost:8000/accueil";}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1488245179;s:1:"c";i:1488244922;s:1:"l";s:1:"0";}
If I try the normal login_form with security.yml it's working fine...
I've try with handler_id and save_path at null with no success.
EDIT2:
I've found why I'm always redirected to the login page, because I'm logged out!
[2017-02-28 09:16:34] security.INFO: The security token was removed due to an AccountStatusException. {"exception":"[object] (Symfony\\Component\\Security\\Core\\Exception\\AuthenticationExpiredException(code: 0): at /home/philippe/Documents/symfony/vendor/symfony/symfony/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php:86)"}
and in GuardAuthenticationProvider.php (86)
The listener *only* passes PreAuthenticationGuardToken instances.
This means that an authenticated token (e.g.PostAuthenticationGuardToken)
is being passed here, which happens if that token becomes "not authenticated" (e.g. happens if the user changes between requests).
In this case, the user should be logged out, so we will return an AnonymousToken to accomplish that.
But Why ???
May be your Session that isn't persisting the token. Check your Session configuration, inside: config.yml. in the framework option, there is session. See how the handler_id and save_path are configured. It may be that your php instalation is unable to handle the sessions on the configured path. Try to put null to handler_id and save_path to force php use its own build in configurations to handle sessions.
config.yml file:
framework:
{ .. Other configurations ..}
session:
handler_id: null
save_path: null
{ .. More configurations ..}