How can i block a user account in symfony 6? - php

Im using symfony 6 and easyadmin 4.
Im trying to figure out how to block a user account on my website but
i can't find a solution.
I tried to create a role named: ROLE_BLOCKED and then use a function like IsDenied in the controllers to block the access but it seems like they are no such function in symfony 6.
update:
here is my LoginAuthenticator
class LoginAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
public function __construct(private UrlGeneratorInterface $urlGenerator)
{
}
public function authenticate(Request $request): Passport
{
$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->request->get('_csrf_token')),
]
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
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(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}
and my security.yaml:
# 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
# used to reload user from session & other features (e.g. switch_user)
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\RegisterAuthenticator
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#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_ARTIST: ROLE_USER
# 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 }
when#test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
FINAL UPDATE: I solved the problem by using a userChecker CLass

use Symfony\Component\Security\Core\Security to get user details and add condition based on the status. or you can directly use isEnabled method
public function isEnabled() {
return $this->getIsActive();
}

To achieve what you want, you need to:
Store users status (able to connect or not)
Prevent user from logging in
Be able to disable an user with EasyAdmin
To enable/disable users, you could just add a new $isEnabled property:
/**
* #ORM\Column(type="boolean", options={"default":true})
*/
private bool $isEnabled = true;
public function isEnabled(): ?bool
{
return $this->isEnabled;
}
public function setIsEnabled(bool $isEnabled): self
{
$this->isEnabled = $isEnabled;
return $this;
}
Don't forget to update your schema (with a new migration)
To prevent your user from logging in, if you are using the new authenticator system (5.3+) you could just update your getUser method and add something like:
if (!$user->isEnabled()) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Account is disabled.');
}
And finally just add your new isEnabled boolean to your crud controller:
public function configureFields(string $pageName): iterable
{
//...
yield BooleanField::new('isEnabled');
}

Related

Symfony Password validation regex (Call to member function null)

Pretty new to the Symfony community but trying my best and I'm having a hard time on "how to validate password" topic.
When my user is connecting for the first time, he's redirected on a new page to change it to a stronger one. I check if it's strong enough with a regex and it's working but only if the user match the strong password policies on the first try. Otherwise he is disconnected and I get a "Call to a member function GetPassword() on null" error which I think I understand, I mean I get the why but don't know how to correct it.
I tried my regex on both the entity and on the symfony form (separately) but the same error occurs.
Thanks for your feedback (and kindness)
Here's my entity :
#[Assert\Regex('#^\S*(?=\S{8,})(?=\S*[a-z])(?=\S*[A-Z])(?=\S*[\d])\S*$#', message: 'Mot de passe trop faible')]
#[ORM\Column(type:'string')]
private $password;
And here is my controller :
#[Route("/", name:"")]
class LoginController extends AbstractController
{
#[Route(path: '/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(path: '/logout', name: 'app_logout')]
public function logout(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
#[Route('/check', name: 'check')]
public function check(
UserRepository $repoUser
): Response
{
$user = $this->getUser();
$isPassword1234 = password_verify('1234',$user->getPassword());
if($isPassword1234){
return $this->redirectToRoute('mdp');
}
return $this->redirectToRoute('home');
}
#[Route('/mdp', name: 'mdp')]
public function changeMdp(
UserRepository $repository,
EntityManagerInterface $entityManager,
Request $request,
UserPasswordHasherInterface $passwordHasher
): Response {
$user = $this->getUser();
$form = $this->createForm(MdpType::class, $user );
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$user = $this->getUser();
$mdpSimple = $user->GetPassword();
$vraiMdp = $passwordHasher->hashPassword($user,$mdpSimple);
$user->setPassword($vraiMdp);
$entityManager->persist($user);
$entityManager->flush();
return $this->redirectToRoute('home');
}
$this->addFlash('error', ' Les deux mots de passes ne correspondent pas !');
return $this->renderForm('home/mdp.html.twig', compact('form', 'user'));
}
}
My security.yaml :
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'
App\Entity\User:
algorithm: 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:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\UserAuthenticator
logout:
path: app_logout
# where to redirect after logout
target: app_login
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# 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
role_hierarchy:
ROLE_SECTION: ROLE_USER
ROLE_UD: ROLE_SECTION
ROLE_NATIONAL: ROLE_UD
ROLE_ADMIN: ROLE_NATIONAL
access_control:
- { path: ^/recap, roles: ROLE_UD }
- { path: ^/national, roles: ROLE_NATIONAL }
- { path: ^/admin, roles: ROLE_ADMIN }
when#test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
Now that you have shared your security I can tell you that the error is linked to that configuration
add this
access_control:
- { path: ^/check, roles: ROLE_USER }
- { path: ^/recap, roles: ROLE_UD }
- { path: ^/national, roles: ROLE_NATIONAL }
- { path: ^/admin, roles: ROLE_ADMIN }
If you don't tell symfony that the route needs authentication $this->getUser() in your controller will always returns null

Symfony Simple Login no longer works after deployment (Symfony, PHP)

I am currently trying to upload my own project to a server.
Everything worked, I can call up the start page and the connection to the database also works.
When I open the register field I can create an account without any problems.
But when i want to Login with this Account, it just reload the Login-Form but I am not redirected to the actual dashboard. On my XAAMPP Apache Serer evertything worked fine. Anyone has an suggestion?
Here my Code:
security.yaml
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'
App\Entity\User:
algorithm: 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:
lazy: true
provider: app_user_provider
custom_authenticator: App\Security\CustomAuthenticator
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#the-firewall
# 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: ^/login, roles: PUBLIC_ACCESS }
# - { path: ^/login, roles: ROLE_USER }
- { path: ^/ausgabe, roles: ROLE_USER }
when#test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon
Security Controller
<?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');
// }
$now = new \DateTime('now');
$month = $now->format('m');
$year = $now->format('Y');
// 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, 'month' => $month, 'year' => $year, 'aktuelleSeite' => $month, 'aktuellesJahr' => $year]);
}
/**
* #Route("/logout", name="app_logout")
*/
public function logout(): void
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
}
CustomAuthenticator
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class CustomAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
private UrlGeneratorInterface $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function authenticate(Request $request): Passport
{
$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->request->get('_csrf_token')),
]
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$now = new \DateTime('now');
$month = $now->format('m');
$year = $now->format('Y');
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($targetPath);
}
// For example:
return new RedirectResponse($this->urlGenerator->generate('dashboard',[
'aktuelleSeite' => $month,
'aktuellesJahr' => $year,
]));
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
}
protected function getLoginUrl(Request $request): string
{
return $this->urlGenerator->generate(self::LOGIN_ROUTE);
}
}

Symfony 3 login is instantly forgotten unless "Remember Me" is used

We've updated a base project from Symfony 2.8 to 3.4. This has largely gone well, blah blah, but I've noticed quite an important issue.
It seems that logging in is instantly forgotten after the request for "check_path" has completed unless the user chooses the "Remember Me" option. -- We do not provide this option for management interfaces to ensure the user has authenticated properly, therefore the management interface can't be accessed at all.
Request flow goes as shown in Symfony profiler:
Attempt to access firewalled route.
401 response showing login form with Anonymous token.
Submit login form.
302 response with UsernamePassword token. -- This shows the username and password has been accepted.
Redirected to original firewalled route.
200 response with Anonymous token. -- UsernamePassword token has gone!
This response does not appear in the web browser's network debugger.
Redirected to login form again.
401 response showing login form with Anonymous token.
Contents of "app/config/security.yml":
security:
encoders:
App\UserBundle\Entity\User:
algorithm: bcrypt
cost: 16
providers:
local_db:
entity: { class: AppUserBundle:User }
firewalls:
dev:
pattern: ^/(_(profiler|wdt))/
security: false
assets:
pattern: ^/(css|images|js)/
security: false
admin:
pattern: ^/admin
provider: local_db
anonymous: ~
logout_on_user_change: true
form_login:
csrf_token_generator: security.csrf.token_manager
login_path: user_admin_login
check_path: user_admin_login
default_target_path: dashboard
use_forward: true
use_referer: true
logout:
path: user_admin_logout
target: dashboard
handler: auth_listener
invalidate_session: true
switch_user:
role: ROLE_TOP_ADMIN
parameter: _login_as_user
# remember_me:
# secret: "%secret%"
front:
pattern: ^/
provider: local_db
anonymous: ~
logout_on_user_change: true
form_login:
csrf_token_generator: security.csrf.token_manager
# login_path should be "user_account_login" or "user_account_auth" depending on which view you want.
login_path: user_account_login
check_path: user_account_login
default_target_path: user_account
use_forward: true
use_referer: true
logout:
path: user_account_logout
target: home
handler: auth_listener
invalidate_session: true
switch_user:
role: ROLE_ADMIN
parameter: _login_as_user
remember_me:
secret: "%secret%"
access_control:
- { path: ^/admin/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: [ROLE_CONTRIBUTOR, ROLE_EDITOR, ROLE_ADMIN, ROLE_TOP_ADMIN] }
- { path: ^/account/auth$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/register$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/forgot_password$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account/change_password$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/account, roles: [ROLE_USER, ROLE_CONTRIBUTOR, ROLE_EDITOR, ROLE_ADMIN, ROLE_TOP_ADMIN] }
# Change the below "IS_AUTHENTICATED_ANONYMOUSLY" to "ROLE_USER" if this is going to be a private website.
# This will ensure users have to login on the landing page.
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
Routes:
user_admin_login points to "/admin/login"
user_admin_logout points to "/admin/logout"
user_account_auth points to "/account/auth".
user_account_login points to "/account/login".
user_account_logout points to "/account/logout".
I'm wondering if this is cookie related. I did notice that the Symfony session ID cookie ("webapp" below) value is changing after logging in, but it does remain consistent between page navigations elsewhere. It only changes twice when submitting the login form. Using the response references above:
Attempt to access firewalled route:
"webapp" value is "h76n9kcra43stmjb5accnqlg70itnavf" on 401 response.
Submit login form:
"webapp" value is "15iscbl51k2mjs14bck5m54f4m8qhtme" on 302 response.
Redirected to firewalled route:
"webapp" value is "ciibpf8h54u2vp3gdi31bvdm5oj3r3ts" on 200 response.
Redirected to login form:
"webapp" value is "ciibpf8h54u2vp3gdi31bvdm5oj3r3ts" on 401 response.
Contents of "app/config/config.yml" session section:
session:
storage_id: "session.storage.native"
handler_id: "session.handler.native_file"
name: "webapp"
cookie_lifetime: 604800
gc_divisor: 10
gc_probability: 1
gc_maxlifetime: 14400
save_path: "%kernel.root_dir%/../var/sessions"
I tried using different web browsers with very default cookie settings in case something was up with Chrome, no different though.
If it helps, when successfully logged in with the "remember me" option ticked, the token is a RememberMeToken -- not a UsernamePasswordToken.
Please let me know if any further information is required.
The goal here is to be able to login without needing a "remember me" option enabled.
Edit: User entity model
As requested, here is some detail about the user entity model. It's quite big (2016 lines) so I'll just paste in the parts relevant to Symfony's user interface.
Declaration
class User implements AdvancedUserInterface, UserPermissionInterface, DataContentEntityInterface, \Serializable
Interfaces UserPermissionInterface and DataContentEntityInterface are custom for our application. (Irrelevant.)
Serializable relevant parts
/**
* #see \Serializable::serialize()
*/
public function serialize()
{
return serialize([
$this->id,
$this->userName,
$this->email,
$this->password,
// $this->salt,
]);
}
/**
* #see \Serializable::unserialize()
*/
public function unserialize($serialized)
{
list(
$this->id,
$this->userName,
$this->email,
$this->password,
// $this->salt,
) = unserialize($serialized, ["allowed_classes" => false]);
}
UserInterface relevant parts
/**
* #inheritDoc
*/
public function getSalt()
{
return null;
}
/**
* #inheritDoc
*/
public function getRoles()
{
if (!$this->group) {
return [];
}
$rolesArray = array();
foreach ($this->getGroup()->getPermissions() as $k => $permission) {
$role = strtoupper($permission);
$role = str_replace('.', '_', $role);
$role = sprintf("ROLE_%s", $role);
$rolesArray[$k] = $role;
}
$rolesArray[] = $this->getGroup()->etRole();
// If user is top admin, also give admin group
if ($this->getGroup()->getRole() === "ROLE_TOP_ADMIN") {
$rolesArray[] = "ROLE_ADMIN";
}
return $rolesArray;
}
/**
* #inheritDoc
*/
public function eraseCredentials()
{
}
/**
* Get userName
*
* #return string
*/
public function getUserName()
{
return $this->userName;
}
/**
* Get password
*
* #return string
*/
public function getPassword()
{
return $this->password;
}
AdvancedUserInterface relevant parts
public function isAccountNonExpired()
{
if (!$this->expires) {
return true;
}
if (new \DateTime() <= $this->expires) {
return true;
}
return false;
}
public function isAccountNonLocked()
{
return $this->status === self::STATUS_VERIFIED;
}
public function isCredentialsNonExpired()
{
if (!$this->passwordExpires) {
return true;
}
if (new \DateTime() <= $this->passwordExpires) {
return true;
}
return false;
}
public function isEnabled()
{
return $this->isAccountNonLocked() && !$this->activationCode;
}
Entity repository UserRepository declaration
class UserRepository extends EntityRepository implements UserLoaderInterface
Function to load user
/**
* UserLoaderInterface
* #param string $userName User to look for
* #return User|null User entity, or null if not found
*/
public function loadUserByUsername($userName)
{
$qb = $this
->createQueryBuilder("u")
->where("u.userName = :userName OR u.email = :userName")
->setParameter("userName", $userName)
->andWhere("u.status != :statusDeleted")
->setParameter("statusDeleted", User::STATUS_DELETED)
->andWhere("u.status = :statusVerified")
->setParameter("statusVerified", User::STATUS_VERIFIED)
->orderBy("u.status", "DESC")
->addOrderBy("u.group", "ASC")
->addOrderBy("u.created", "ASC")
->setMaxResults(1)
;
$query = $qb->getQuery();
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $query->getSingleResult();
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf("Unable to find an active user identified by \"%s\".", $username), 0, $e);
} catch (NonUniqueResultException $e) {
throw new UsernameNotFoundException(sprintf("Unable to find a unique active user identified by \"%s\".", $username), 0, $e);
}
return $user;
}
This function works fine. A valid user entity is definitely returned.
In your security.yml, remove:
logout_on_user_change: true
or set it to false.
This will solve the instant logout issue, though it will also bypass a security feature of Symfony.
It seems as if something in the serialize() and unserialize() isn't matching, and Symfony then logs the user out as a precaution. With AdvancedUserInterface, Symfony also checks that the AdvancedUserInterface methods match too. If you have anything else going on in those methods which could cause the users to not match (like some bespoke roles management), that could be triggering the logout. To debug, I would suggest returning true in each of the AdvancedUserInterface methods, then re-adding your functionality until the logout gets triggered.
From Symfony's documentation:
If you're curious about the importance of the serialize() method inside
the User class or how the User object is serialized or deserialized,
then this section is for you. If not, feel free to skip this.
Once the user is logged in, the entire User object is serialized into
the session. On the next request, the User object is deserialized.
Then, the value of the id property is used to re-query for a fresh
User object from the database. Finally, the fresh User object is
compared to the deserialized User object to make sure that they
represent the same user. For example, if the username on the 2 User
objects doesn't match for some reason, then the user will be logged
out for security reasons.
Even though this all happens automatically, there are a few important
side-effects.
First, the Serializable interface and its serialize() and
unserialize() methods have been added to allow the User class to be
serialized to the session. This may or may not be needed depending on
your setup, but it's probably a good idea. In theory, only the id
needs to be serialized, because the refreshUser() method refreshes the
user on each request by using the id (as explained above). This gives
us a "fresh" User object.
But Symfony also uses the username, salt, and password to verify that
the User has not changed between requests (it also calls your
AdvancedUserInterface methods if you implement it). Failing to
serialize these may cause you to be logged out on each request. If
your user implements the EquatableInterface, then instead of these
properties being checked, your isEqualTo() method is called, and you
can check whatever properties you want. Unless you understand this,
you probably won't need to implement this interface or worry about it.
Resolved this with insight from #jedge.
Turns out the logout_on_user_change option being true was the cause. Changing this to false resolved the issue. -- I'm not sure what this does as there is little documentation on it, and worryingly this has become true by default in Symfony 4...
Other things we tried were the temporary removal of CSRF, forwarding, and logout event. -- None of these turned out to conflict. We were also able to login programmatically by manually creating a token for a specific user and dispatching an InteractiveLoginEvent, which led us on to the firewall configuration.

Multiple authentication providers in Symfony 2 for a single firewall

I have a Symfony 2.7.6 project with custom Simple Form authentication provider and support for remember me functionality as well as impersonalization feature. Everything works as expected.
However, I want to introduce another authentication provider that will allow requests regardless of session state using two HTTP headers for authentication (e.g. API-Client-Id and API-Client-Token) for third-party applications.
I've created a Simple Pre-Auth authentication provider that validates these header fields and creates authentication token with empty User instance on success.
However, it looks like Symfony is trying to remember those API authentications using session, so I'm getting the following error on the second request: "You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.".
I can set stateless: true flag in my firewall configuration to disable session support, but it will disable it for both auth providers.
SO, how do I preserve existing functionality with my Simple Form authenticator and yet create another layer of authentication to be used for single stateless API requests?
I'm not sure if my approach is conceptually correct. I will gladly accept any suggestions and will provide any relevant information on first request.
Here's my security.yml config:
security:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
anonymous: ~
form_login:
login_path: app.login
check_path: app.session.sign_in
username_parameter: username
password_parameter: password
success_handler: app.security.login_handler
failure_handler: app.security.login_handler
require_previous_session: false
logout:
path: app.session.sign_out
invalidate_session: false
success_handler: app.security.logout_success_handler
# Simple form auth provider
simple_form:
authenticator: app.security.authenticator.out_service
# Token provider
simple_preauth:
authenticator: app.security.authenticator.api_client
remember_me:
name: "%app.session.remember_me.name%"
key: "%secret%"
lifetime: 1209600 # 14 days
path: /
domain: ~
always_remember_me: true
switch_user: { role: ROLE_ADMIN }
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/recover-password, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /, roles: IS_AUTHENTICATED_REMEMBERED }
providers:
main:
entity:
class: App\AppBundle\Model\User
property: id
encoders:
App\AppBundle\Model\User: plaintext
role_hierarchy:
ROLE_ADMIN: [ROLE_USER, ROLE_ACTIVE]
ROLE_API_CLIENT: ~
ROLE_USER: ~
ROLE_ACTIVE: ~
ApiClientAuthenticator.php:
<?php
namespace App\AppBundle\Security;
use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use App\AppBundle\Model\User;
class ApiClientAuthenticator implements SimplePreAuthenticatorInterface
{
/** #var LoggerInterface */
protected $logger;
/** #var array */
protected $clients;
/**
* #param array $clients
*/
public function __construct(array $clients)
{
$this->clients = $clients;
}
public function createToken(Request $request, $providerKey)
{
$clientId = $request->headers->get('Api-Client-Id');
$clientSecret = $request->headers->get('Api-Client-Secret');
if (!$clientId || !$clientSecret) {
return null;
}
return new PreAuthenticatedToken(
'anon.',
[$clientId, $clientSecret],
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
list ($clientId, $clientSecret) = $token->getCredentials();
$foundClient = null;
foreach ($this->clients as $client) {
if ($client['id'] == $clientId) {
if ($client['secret'] == $clientSecret) {
$foundClient = $client;
break;
}
}
}
if (!$foundClient) {
throw new AuthenticationException;
}
$user = new User;
$user->setApiClient(true);
return new PreAuthenticatedToken(
$user,
$foundClient,
$providerKey,
['ROLE_API_CLIENT']
);
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return ($token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey);
}
}

Symfony 2.6 Security Login redirects back to login page on successful login

I am currently setting up a login system with a custom User Provider for Symfony2. I made the User and Provider classes, and the login system works fine with "manual" test variables. For example, this is my provider with test user data that always has a successful login:
<?php
namespace SoftAltern\DashboardBundle\Security\User;
use Symfony\Component\HttpFoundation\Request;
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)
{
$userData = [
'username' => 'test',
'password' => 'testpass',
'roles' => [],
];
if ($userData) {
return new WebserviceUser($userData['username'], $userData['password'], '', $userData['roles']);
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'SoftAltern\DashboardBundle\Security\User\WebserviceUser';
}
}
This works fine obviously. However, when I try adding my own check that returns an array built in the same format, it just goes back to the login page. Here is that example:
<?php
namespace SoftAltern\DashboardBundle\Security\User;
use Symfony\Component\HttpFoundation\Request;
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;
use SoftAltern\DashboardBundle\Helper\I5ToolkitHelper;
class WebserviceUserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
$request = Request::createFromGlobals();
$toolkit = new I5ToolkitHelper;
$userData = $toolkit->checkUserData($username, $request->request->get('_password'));
if ($userData) {
return new WebserviceUser($userData['username'], $userData['password'], '', $userData['roles']);
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'SoftAltern\DashboardBundle\Security\User\WebserviceUser';
}
}
I believe that the issue lies in using the request to get the password. However, in my current system (an AS/400 IBM iSeries), there is no way to just grab the password based on the username. So what I'm trying to do is use the i5ToolKit, check the credentials, and return the userData based on a successful iSeries login or not. It returns the array fine, but still just redirects back to the login page on success.
The reason I believe it has something to do with the request is because if I comment that part out and manually submit a valid username and password to my I5ToolKitHelper, everything works as expected.
I have also already tried the way they suggested using request in this post, but it didn't help either.
Here is my security.yml:
# you can read more about security in the related section of the documentation
# http://symfony.com/doc/current/book/security.html
security:
# http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
encoders:
SoftAltern\DashboardBundle\Security\User\WebserviceUser: plaintext
# http://symfony.com/doc/current/book/security.html#hierarchical-roles
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers:
webservice:
id: webservice_user_provider
# the main part of the security, where you can set up firewalls
# for specific sections of your app
firewalls:
# disables authentication for assets and the profiler, adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# the login page has to be accessible for everybody
login:
pattern: ^/login$
security: false
# secures part of the application
soft_altern_dashboard_secured_area:
pattern: ^/
# it's important to notice that in this case _demo_security_check and _demo_login
# are route names and that they are specified in the AcmeDemoBundle
form_login:
check_path: soft_altern_dashboard_login_check
login_path: soft_altern_dashboard_login
logout:
path: soft_altern_dashboard_logout
target: soft_altern_dashboard_homepage
#anonymous: ~
#http_basic:
# realm: "Secured Demo Area"
# with these settings you can restrict or allow access for different parts
# of your application based on roles, ip, host or methods
# http://symfony.com/doc/current/cookbook/security/access_control.html
access_control:
#- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
Any help would be greatly appreciated!
Edit: I error logged the password during one login and it looks like it hits the login check twice, and on the second go around the password is empty. This behavior might be for security reasons. I'm not sure if I have a setting wrong that might be causing this.
There could be a number of related things. The page that Symfony redirects to is normally stored into session. You can change it. But first, try to add under the form_login setting the following option:
form_login:
#...
use_referer: true

Categories