How can we configure Symfony 4 Remember me functionality to use email instead of username (as set by default) when creating the cookie and storing it in the browser?
My issue is that by using email to authenticate in S4, the cookie is created with the username instead of the email in its hash, stored in the browser but when S4 check my cookie to see if IS_AUTHENTICATED_REMEMBERED is true, it checks it against the username stored in the DB which doesn’t make sens. It should check it against email. So my remember me functionality doesn’t work.
If I use the username to login, then it works, but that’s not what I want, I’d like my users to log in with their email address.
I’ve configurered the login to work with email instead of the default username behavior, but I can’t have remember me working that way.
I tried the following in my security.yaml
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
user_provider:
entity:
class: App\Entity\User
property: email
in_memory: { memory: ~ }
our_db_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
http_basic: ~
provider: our_db_provider
anonymous: ~
form_login:
login_path: login
check_path: login
default_target_path: dashboard
username_parameter: email
password_parameter: password
remember_me: true
remember_me:
secret: '%kernel.secret%'
lifetime: 31536000 # 1 week in seconds
path: /
domain: ~
secure: true
name: REMEMBERME
remember_me_parameter: remember_me
always_remember_me: true
logout:
path: /logout
target: /
but this doesn’t let you parameter what field remember is using to generate the hash stored in the cookie.
If you’ve managed to set up your login / authentication & remember me working with a field different than username, please share :)
UPDATE: I tried Ahmed answer with the following lines on services but it’s not working:
App\Security\TokenBasedRememberMeServices:
decorates: Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices
it says You have requested a non-existent service "Symfony\Component\Security\Http\RememberMe\TokenBasedRememberMeServices”.
The problem that security component use getUsername() getter to build the token https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Http/RememberMe/TokenBasedRememberMeServices.php#L74
So we need to overide the service responosible for creating the remember-me cookie, wish is security.authentication.rememberme.services.simplehash.class.
Firstable: Update the onLoginSuccess method L74 so it uses the email instead of the username.
namespace App\Security;
...
class TokenBasedRememberMeServices extends AbstractRememberMeServices
{
....
protected function onLoginSuccess(Request $request, Response $response, TokenInterface $token)
{
...
$value = $this->generateCookieValue(\get_class($user), $user->getEmail(), $expires, $user->getPassword());
...
}
...
Second: Register your class as a service.
App\Security\TokenBasedRememberMeServices:
decorates: 'security.authentication.rememberme.services.simplehash.class'
Or you can flow the contract under UserInterface It define and returns the username used to authenticate the user. In our case it's the email property.
public function getUsername()
{
return $this->email;
}
I don't have enough SO reputation to add a comment for answer upon, so I write it here.
Instead of decorating service:
security.authentication.rememberme.services.simplehash.class
Decorate:
security.authentication.rememberme.services.simplehash
It works on Symfony 4.4
Related
I am new to symfony and this is my first project and I'm trying to create a login form. I created it using make:auth, but when I try to login I get this error:
I replaced
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
with
return new RedirectResponse($this->urlGenerator->generate('home'));
but it shows me the same error.
This is my home route:
/**
* #Route("/", name="home")
* #Method({"GET"})
*/
public function index(){
return $this->render('tenis/index.html.twig');
}
For the first solution, you can just make a null return in the onAuthenticationSuccess(...) function (line 99 of your AppCustomAthenticator.php) if you don't have any particular processing to do when the User has connected with success and add the redirection route directly in the ./config/packages/security.yaml file like this.
security:
providers:
# used to reload user from session & other features (e.g.switch_user) #
app_user_provider:
entity:
class: App\Entity\User
property: email
encoders:
# use your user class name here
App\Entity\User:
# Use native password encoder
# This value auto-selects the best possible hashing algorithm
# (i.e. Sodium when available).
algorithm: auto
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
form_login:
login_path: app_login
check_path: app_login
csrf_token_generator: security.csrf.token_manager # It's optional
default_target_path: home # add here your redirect path
always_use_default_target_path: true
guard: # you need to check this file and
authenticators: # make sure that all your configurations
- App\Security\LoginFormAuthenticator # are right
logout:
path: app_logout
target: app_login
Or you can comment out the line 99 of your AppCustomAthenticator.php (in the onAuthenticationSuccess(...) function)
throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
and change it to.
return new RedirectResponse($this->urlGenerator->generate('home'));
You can change the value of 'home' by the name of your redirect symfony route.
Make sure you save the change. If it does not work, try to clear the cache:
bin/console cache:clear
In your SecurityAuthenticator file, find the function SecurityAuthenticator and uncomment this line:
return new RedirectResponse ($ this-> urlGenerator-> generate ('home'));
to put your redirection
In my application I wish to redirect users to profile after login, so I'm using an hidden input with name _target_path and value /profile/{userid} in my TWIG page (I've tried to make an IF statement to check when the app.user.username variable exist, but this isn't the right logic)
<input type="hidden" name="_target_path" value="/profile/{% if app.user.username is defined %}{{ app.user.username }}{% endif %}" />
This isn't working as expected cause naturally when I render the form I've not already set the username in session.
Here is my loginAction() in AuthController.php
/**
* #Route("login/", name="login")
*/
public function loginAction(Request $request)
{
$authenticationUtils = $this->get('security.authentication_utils');
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('auth/login.html.twig', array(
'last_username' => $lastUsername,
'error' => $error,
));
}
I can reach the profile page using the parameter ID, so the url is like example.com/profile/1 without the ID it's causing a 404.
So I need to redirect the user after login to profile/{id} taking it from autenticationUtils
I'm really sure that my logic is incorrect, but I can't find nothing in the web that can solve my issue.
Here is my security.yml
security:
providers:
user_db:
entity: { class: AppBundle\Entity\User, property: username }
encoders:
Symfony\Component\Security\Core\User\User:
algorithm: bcrypt
cost: 12
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
form_login:
login_path: login
check_path: login
logout: true
logout:
csrf_parameter: _csrf_token
csrf_token_generator: ~
csrf_token_id: logout
path: /logout
target: /
success_handler: ~
invalidate_session: true
delete_cookies:
name:
path: null
domain: null
handlers: []
admin:
pattern: ^/
provider: user_db
http_basic:
realm: 'Admin Area'
provider: in_memory
form_login: ~
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/profilo, roles: [ROLE_USER, ROLE_ADMIN] }
What you need is changing the DefaultAuthenticationSuccessHandler (given you use the plain symfony mechanism wihtout any bundles like FOSUserBundle involved).
First thing make your own Handler (or parts, here especially onAuthenticationSuccess):
namespace ...
use ...
class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler
{
/**
* {#inheritdoc}
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
**DO your logic here**
//or call parent if you want default behaviour
return parent::onAuthenticationSuccess($request, $token);
}
}
Inject more services as needed.
Second overwrite the default service in your DI:
security.authentication.success_handler:
class: AppBundle\Handler\AuthenticationSuccessHandler
arguments: ['#security.http_utils', {}]
tags:
- { name: 'monolog.logger', channel: 'security' }
From the Symfony documentation: Changing the default Page
Changing the default Page:
First, the default page can be set (i.e. the page the user is redirected to if no previous page was stored in the session). To set it to the default_security_target route use the following config:
# app/config/security.yml
security:
# ...
firewalls:
main:
form_login:
# ...
default_target_path: default_security_target
Now, when no URL is set in the session, users will be sent to the default_security_target route.
You can make it so that users are always redirected to the default page regardless of what URL they had requested previously by setting the always_use_default_target_path option to true:
# app/config/security.yml
security:
# ...
firewalls:
main:
form_login:
# ...
always_use_default_target_path: true
You should do something like this in PHP after login :
return $this->redirectToRoute('name_profile_route', { id : $this->getUser()->getId()});
I'm trying to build a custom firewall for my Symfony3 website. I've been following the documentation, and was able to get it to work for the main firewall. My desired functionality is the ability for a user to login with their username and password, using Symfony's native classes. Here is my SecurityController:
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class SecurityController extends Controller
{
/**
* #Route("/m/login", name="model_login")
*/
public function loginAction(Request $request)
{
$authenticationUtils = $this->get("security.authentication_utils");
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('Model/login.html.twig', [
"error" => $error,
"lastUsername" => $lastUsername
]);
}
}
As you can see, it is exactly the same as the sample code, except with the routing settings changed. My login form renders fine. I get no errors when I submit the form, and I have my form POSTing to this exact controller. My view works perfectly as I am able to authenticate when my security settings are under "main." Here is my security.yml:
security:
providers:
in_memory:
memory: ~
doctrine_provider:
entity:
class: AppBundle:Model
property: username
encoders:
AppBundle\Entity\Model:
algorithm: bcrypt
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: ~
model_area:
anonymous: ~
provider: doctrine_provider
pattern: ^/m/
form_login:
login_path: model_login
check_path: model_login
access_control:
- { path: ^/m/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/m/, roles: ROLE_MODEL }
My providers and encoders function properly, as exemplified by my success when authenticating under the "main" firewall. However, when I attempt to put my settings under "model_area," submitting my form just redirects me to the login form, with no authentication. I only added the pattern: ^/m/ and - { path: ^/m/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } so I do not block access to my login form. Everything else remained the same (even the route names!).
I have a route /m/model_dashboard, which throws an error, saying "full authentication is required to access this resource." So clearly my access control settings are working properly, however, it is not redirecting to my login form when I attempt to access a protected resource.
Is there something I'm missing? I'm extremely confused as to why the authentication would work under main but not under my custom firewall, which had the same exact settings.
The problem was my main firewall. I did not know that firewalls are determined similarly to routes, top to bottom. Every request was being filed under the "main" firewall (which has no form_login), which was why my login code wasn't working. I removed the main firewall and it is working beautifully. Here is what my updated security.yml looks like now:
security:
providers:
in_memory:
memory: ~
doctrine_provider:
entity:
class: AppBundle:Model
property: username
encoders:
AppBundle\Entity\Model:
algorithm: bcrypt
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
model_area:
anonymous: ~
provider: doctrine_provider
pattern: ^/m/
form_login:
login_path: model_login
check_path: model_login
access_control:
- { path: ^/m/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/m/, roles: ROLE_MODEL }
Just recently I got into testing pieces of a symfony application... For starters, I thought I would try and test a login form that should grant access to a private area.
The users for this private area are correctly persisted to the database and have been tried on the development enviroment. The config_test.yml file goes like this:
imports:
- { resource: config_dev.yml }
framework:
test: ~
session:
storage_id: session.storage.mock_file
profiler:
collect: false
web_profiler:
toolbar: false
intercept_redirects: false
swiftmailer:
disable_delivery: true
So it should use the database configuration exposed in config_dev.yml.
There's nothing special about my user provider:
providers:
users:
entity:
class: MyBundle:User
property: login
As requested in the comments section, here's the security info. Please note that all namespaces, class names and relevant data have been changed, as I can't publish the real thing. If you spot any mistake it must have been me changing the relevant code.
security:
encoders:
MyBundle\Entity\User:
algorithm: bcrypt
cost: 12
role_hierarchy:
ROLE_CUSER: ROLE_USER
ROLE_CUSER_A: ROLE_CUSER
ROLE_CUSER_B: ROLE_CUSER
ROLE_CUSER_C: ROLE_CUSER
ROLE_CUSER_D: ROLE_CUSER
ROLE_CUSER_E: ROLE_CUSER
providers:
users:
entity:
class: MyBundle:User
property: login
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
user_tools_login_firewall:
pattern: ^/user_tools/login$
anonymous: ~
user_tools:
pattern: ^/user_tools
http_basic: ~
provider: users
form_login:
login_path: /user_tools/login
check_path: /user_tools/login_check
logout:
path: /user_tools/logout
target: /user_tools/home
invalidate_session: false
access_control:
- {path: /user_tools/login, roles: IS_AUTHENTICATED_ANONYMOUSLY}
- {path: ^/user_tools/users/, roles: ROLE_CUSER_A}
- {path: ^/user_tools/news/, roles: ROLE_CUSER_B}
Let's take a look at the testing unit...
class SecurityControllerTest extends WebTestCase
{
const LOGIN_ERROR='this_is_a_non_existing_user';
const PASS_ERROR='with_a_non_existing_pass';
const LOGIN='this_is_a_valid_user';
const PASS='with_a_valid_pass';
const PASS_CRYPT='##and_this_is_the_encripted_pass_as_pasted_from_the_database##';
const ID_SUBMIT='btn_submit';
private $client;
private $crawler;
public function testIndex()
{
$this->client=static::createClient();
$this->crawler=$this->client->request('GET', '/route_to_login');
//Let's check that there are users in the database...
$em=$this->client->getContainer()->get('doctrine')->getManager();
$users=$em->getRepository("MyBundle:User")->findAll();
$this->assertGreaterThan(0, count($users));
//Now let's check that there's a valid user...
$valid_user=$em->getRepository("MyBundle:User")->findBy(
array('login' => self::LOGIN,
'pass' => self::PASS_CRYPT));
$this->assertGreaterThan(0, count($valid_user));
//Let's check that there is no invalid user ;).
$invalid_user=$em->getRepository("MyBundle:User")->findBy(
array('login' => self::LOGIN_ERROR,
'pass' => self::PASS_ERROR));
$this->assertEquals(0, count($invalid_user));
//The view should be the one I expect...
$this->assertEquals('MyBundle\Controller\User\SecurityController::loginAction', $this->client->getRequest()->attributes->get('_controller'));
//Let's try an invalid access...
$this->form_login(self::LOGIN_ERROR, self::PASS_ERROR);
$this->assertEquals('MyBundle\Controller\User\SecurityController::loginAction', $this->client->getRequest()->attributes->get('_controller'));
//And now, let's try the good one!.
self::form_login(self::LOGIN, self::PASS);
$this->assertEquals('MyBundle\Controller\User\HomeController::homeAction', $this->client->getRequest()->attributes->get('_controller'));
}
private function form_login($login, $pass)
{
$form=$this->crawler->selectButton(self::ID_SUBMIT)->form();
$form['_username']=$login;
$form['_password']=$pass;
$this->client->submit($form);
$this->crawler=$this->client->followRedirect();
}
}
The thing is, all tests are passed except the last one (that is, the good login). It does not go to the expected controller, but back to the login.
Here's the phpunit error:
There was 1 failure:
1) MyBundle\Tests\Controller\User\SecurityControllerTest::testIndex
Failed asserting that null matches expected 'MyBundle\Controller\User\HomeController::homeAction'.
Is there something I am missing here?. Everything is working as expected into the dev enviroment. Users do exists (and are asserted, as you can see). I am prepared to give more info as requested.
Thanks!.
You forgot to define the default_target_path in your form_login configuration, try to add your path like this:
form_login:
login_path: /user_tools/login
check_path: /user_tools/login_check
default_target_path: your_target_path
I have a custom user provider called CustomUserProvider, which I use to manage the logins. The problem I've been having, is that it doesn't authenticate/login the user - it refreshes the page and nothing happens.
I even followed it through, to see if it's getting into the loadUserByUsername method, and it was. $userData gets populated with the correct user, and it should be returning it. To take it 1 step further, I even setup a custom authenticator as well, to make sure that the token was getting generated properly - even with that, it wasn't logging them in - just refreshing the page, and nothing actually happening.
Here's the loadUserByUsername snippet from CustomUserProvider.
public function loadUserByUsername($username)
{
// query database and do the custom salt/password check
$userData = $this->em->createQuery(
'....'
)->setParameter('username', $username)
->getResult();
if (count($userData) > 0) {
return $userData[0];
} else {
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
}
And here's my security.yml file:
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
Project\Bundle\MainBundle\Entity\Accounts: sha512
providers:
chain_provider:
chain:
providers: [my_fos_facebook_provider, custom_provider]
my_fos_facebook_provider:
id: my.facebook.user
custom_provider:
id: custom_user_provider
firewalls:
secured_area:
pattern: ^/
fos_facebook:
app_url: "http://apps.facebook.com/sendioso-dev/"
server_url: "http://www.website.com/facebookApp/"
login_path: _security_login
check_path: _security_check
default_target_path: /
provider: my_fos_facebook_provider
form_login:
login_path: project_main_login
check_path: project_main_login_check
provider: custom_provider
logout:
path: project_main_logout
anonymous: ~
#simple_preauth:
# authenticator: custom_authenticator
access_control:
- { path: ^/account, roles: ROLE_USER }
* UPDATE *
I was checking dev.log and this came up:
[2014-02-21 21:39:40] security.INFO: User "email#email.com" has been authenticated successfully [] []
So it looks like it's somewhat working, but it doesn't actually log me in - just refreshes the page and that's it.
* UPDATE 2 *
I did some more digging, and found out that it's passing an account object with all NULL values, when calling refreshUser(UserInterface $user) in CustomUserProvider.
Anyone know why it would do that?
I found the issue.
When it was serializing the 'Accounts' object, it wasn't persisting the data in the serialization. So, whenever it deserialized, it made everything NULL.
I went into the Entity and fixed the way it did the serialization, so now it persists the columns that I need for this to work.