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
Related
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');
}
In an attempt to refactor entities from Class Table Inheritance to separate user type classes I've run into this:
The "App\Security\LoginFormAuthenticator::getUser()" method must
return a UserInterface. You returned "array".
From the docs I configured a chain of user providers. I'm at a loss. For a custom user provider I've found Symfony's ChainUserProvider but integrating it into the process is not at all clear. security.yaml doesn't like including it either in firewalls.main.provider or anywhere in providers.all_users
Surely there's something else to add, but what and where?
security.yaml:
security:
encoders:
App\Entity\Admin:
algorithm: auto
App\Entity\Representative:
algorithm: auto
App\Entity\Volunteer:
algorithm: auto
role_hierarchy:
ROLE_SUPER_ADMIN: [ROLE_ADMIN]
providers:
# used to reload user from session & other features (e.g. switch_user)
admin_provider:
entity:
class: 'App\Entity\Admin'
property: email
representative_provider:
entity:
class: 'App\Entity\Representative'
property: email
volunteer_provider:
entity:
class: 'App\Entity\Volunteer'
property: email
all_users:
chain:
providers: ['admin_provider', 'representative_provider', 'volunteer_provider']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
provider: all_users
user_checker: App\Security\UserChecker
anonymous: ~
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
target: /
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week in seconds
path: /
access_control:
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
and modified UserChecker:
use App\Entity\Admin;
use App\Entity\Representative;
use App\Entity\Volunteer;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class UserChecker implements UserCheckerInterface
{
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof Admin && !$user instanceof Representative && !$user instanceof Volunteer) {
return;
}
}
public function checkPostAuth(UserInterface $user)
{
if (!$user instanceof Admin && !$user instanceof Representative && !$user instanceof Volunteer) {
return;
}
...
}
and modified LoginFormAuthenticator:
public function getUser($credentials, UserProviderInterface $userProvider)
{
$token = new CsrfToken('authenticate', $credentials['csrf_token']);
if (!$this->csrfTokenManager->isTokenValid($token)) {
throw new InvalidCsrfTokenException();
}
$admin = $this->entityManager->getRepository(Admin::class)->findBy(['email' => $credentials['email']]);
$rep = $this->entityManager->getRepository(Representative::class)->findBy(['email' => $credentials['email']]);
$vol = $this->entityManager->getRepository(Volunteer::class)->findBy(['email' => $credentials['email']]);
$user = $admin ?? $rep ?? $vol ?? null;
if (!$user) {
// fail authentication with a custom error
throw new CustomUserMessageAuthenticationException('Credentials could not be found.');
}
return $user;
}
The issue is that the Doctrine Repository method findBy returns an array, so your attempts to get the user here result with an array:
$admin = $this->entityManager->getRepository(Admin::class)->findBy(['email' => $credentials['email']]);
$rep = $this->entityManager->getRepository(Representative::class)->findBy(['email' => $credentials['email']]);
$vol = $this->entityManager->getRepository(Volunteer::class)->findBy(['email' => $credentials['email']]);
$user = $admin ?? $rep ?? $vol ?? null;
Since I assume you only expect one user when searching by email, you can substitute it with findOneBy, which returns an object or null.
I made the Login-Action Tutorial from the symfony site: http://symfony.com/doc/current/cookbook/security/form_login_setup.html
When I do my Login in the form, I get the following error:
The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller?
Here is my security.yml
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_USER: ROLE_USER
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
in_memory:
memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
access_denied_url: no_access
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/login
security: false
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: /login
check_path: /login_check
default_target_path: home
logout:
path: /logout
target: /login
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
- { path: ^/, roles: ROLE_USER }
My Security Controller:
class SecurityController extends Controller
{
/**
* #Route("/login", name="login_route")
* #Template()
*/
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render(
'TripAdminBundle:Main:login.html.twig',
array(
// last username entered by the user
'last_username' => $lastUsername,
'error' => $error,
)
);
}
/**
* #Route("/login_check", name="login_check")
*/
public function loginCheckAction()
{
// this controller will not be executed,
// as the route is handled by the Security system
}
}
The redirect after Click on the submit Button is to login_check but there is no code in it because symfony says: the route is handled by the Security system
Bute I get this error. Can someone help me with this please?
return array directly , don't use render method and twig file name .
return array(
// last username entered by the user
'last_username' => $lastUsername,
'error' => $error,
)
);
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);
}
}
I am trying to configure remember me feature of symfony using their default mechanism as described here; but couldn't make it work.
Cookie named REMEMBERME is created, but is set to deleted and its expire date is 1970. This is why I suppose remember me function is not working. However, when I use (always_remember_me: true) in security.yml, the code works nicely but it doesn't suite my purpose. Using (always_remember_me: true) even if user doesn't check REMEMBER ME checkbox in UI the cookie gets created.
Any help is highly appreciated
I am using the version 2.3.7
This is my complete security.yml file:
security:
firewalls:
main:
pattern: ^/
anonymous: ~
remember_me:
key: "%secret%"
lifetime: 31536000 # a year
domain: ~
path: /
remember_me_parameter: _remember_me
#always_remember_me: true
form_login:
login_path: _my_login
check_path: _my_login_check
always_use_default_target_path: true
default_target_path: /out/homepage
remember_me: true
logout:
path: _my_logout
target: _my_login
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_REMEMBERED }
providers:
chain_provider:
chain:
providers: [in_memory, user_db]
in_memory:
memory:
users:
user: { password: user, roles: 'ROLE_USER' }
admin: { password: admin, roles: ['ROLE_ADMIN', 'ROLE_USER'] }
user_db:
entity: {class: ProjectName\StoreBundle\Entity\User, property: username}
encoders:
ProjectName\StoreBundle\Entity\User:
algorithm: sha1
iterations: 1
encode_as_base64: false
Symfony\Component\Security\Core\User\User: plaintext
LoginController.php
class LoginController extends Controller
{
/**
* #Route("/login", name="_my_login")
* #Template()
*/
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 array(
// last username entered by the user
'last_username' => $session->get(SecurityContext::LAST_USERNAME),
'error' => $error,
);
}
/**
* #Route("/login_check", name="_my_login_check")
*/
public function loginCheckAction()
{
}
I added the value attribute inside the input tag. After removing it, it worked for now. :)
<input type="checkbox" id="remember_me" name="_remember_me" checked /> Remember me
I suppose the checkbox is named incorrectly. It should have the 'name' attribute set to '_remember_me' to make the magic happen. Can you post the content of the form template?