Remove/Replace the Username Field with Email Using Fosuserbundle in Symfony2/Symfony3

Remove / Replace the username field with email using FOSUserBundle in Symfony2 / Symfony3

A complete overview of what needs to be done

Here is a complete overview of what needs to be done. I have listed the different sources found here and there at the end of this post.

1. Override setter in Acme\UserBundle\Entity\User

public function setEmail($email)
{
$email = is_null($email) ? '' : $email;
parent::setEmail($email);
$this->setUsername($email);

return $this;
}

2. Remove the username field from your form type

(in both RegistrationFormType and ProfileFormType)

public function buildForm(FormBuilder $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->remove('username'); // we use email as the username
//..
}

3. Validation constraints

As shown by @nurikabe, we have to get rid of the validation constraints provided by FOSUserBundle and create our own. This means that we will have to recreate all the constraints that were previously created in FOSUserBundle and remove the ones that concern the username field. The new validation groups that we will be creating are AcmeRegistration and AcmeProfile. We are therefore completely overriding the ones provided by the FOSUserBundle.

3.a. Update config file in Acme\UserBundle\Resources\config\config.yml

fos_user:
db_driver: orm
firewall_name: main
user_class: Acme\UserBundle\Entity\User
registration:
form:
type: acme_user_registration
validation_groups: [AcmeRegistration]
profile:
form:
type: acme_user_profile
validation_groups: [AcmeProfile]

3.b. Create Validation file Acme\UserBundle\Resources\config\validation.yml

That's the long bit:

Acme\UserBundle\Entity\User:
properties:
# Your custom fields in your user entity, here is an example with FirstName
firstName:
- NotBlank:
message: acme_user.first_name.blank
groups: [ "AcmeProfile" ]
- Length:
min: 2
minMessage: acme_user.first_name.short
max: 255
maxMessage: acme_user.first_name.long
groups: [ "AcmeProfile" ]

# Note: We still want to validate the email
# See FOSUserBundle/Resources/config/validation/orm.xml to understand
# the UniqueEntity constraint that was originally applied to both
# username and email fields
#
# As you can see, we are only applying the UniqueEntity constraint to
# the email field and not the username field.
FOS\UserBundle\Model\User:
constraints:
- Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity:
fields: email
errorPath: email
message: fos_user.email.already_used
groups: [ "AcmeRegistration", "AcmeProfile" ]

properties:
email:
- NotBlank:
message: fos_user.email.blank
groups: [ "AcmeRegistration", "AcmeProfile" ]
- Length:
min: 2
minMessage: fos_user.email.short
max: 255
maxMessage: fos_user.email.long
groups: [ "AcmeRegistration", "ResetPassword" ]
- Email:
message: fos_user.email.invalid
groups: [ "AcmeRegistration", "AcmeProfile" ]
plainPassword:
- NotBlank:
message: fos_user.password.blank
groups: [ "AcmeRegistration", "ResetPassword", "ChangePassword" ]
- Length:
min: 2
max: 4096
minMessage: fos_user.password.short
groups: [ "AcmeRegistration", "AcmeProfile", "ResetPassword", "ChangePassword"]

FOS\UserBundle\Model\Group:
properties:
name:
- NotBlank:
message: fos_user.group.blank
groups: [ "AcmeRegistration" ]
- Length:
min: 2
minMessage: fos_user.group.short
max: 255
maxMessage: fos_user.group.long
groups: [ "AcmeRegistration" ]

FOS\UserBundle\Propel\User:
properties:
email:
- NotBlank:
message: fos_user.email.blank
groups: [ "AcmeRegistration", "AcmeProfile" ]
- Length:
min: 2
minMessage: fos_user.email.short
max: 255
maxMessage: fos_user.email.long
groups: [ "AcmeRegistration", "ResetPassword" ]
- Email:
message: fos_user.email.invalid
groups: [ "AcmeRegistration", "AcmeProfile" ]

plainPassword:
- NotBlank:
message: fos_user.password.blank
groups: [ "AcmeRegistration", "ResetPassword", "ChangePassword" ]
- Length:
min: 2
max: 4096
minMessage: fos_user.password.short
groups: [ "AcmeRegistration", "AcmeProfile", "ResetPassword", "ChangePassword"]

FOS\UserBundle\Propel\Group:
properties:
name:
- NotBlank:
message: fos_user.group.blank
groups: [ "AcmeRegistration" ]
- Length:
min: 2
minMessage: fos_user.group.short
max: 255
maxMessage: fos_user.group.long
groups: [ "AcmeRegistration" ]

4. End

That's it! You should be good to go!


Documents used for this post:

  • Best way to remove usernames from FOSUserBundle
  • [Validation] Doesn't override properly
  • UniqueEntity
  • Validating fosuserbundle registration form
  • How to use validation groups in Symfony
  • Symfony2 using validation groups in form

Remove email field FOSUserBundle

Hack the entity setter:

public function setUsername($username) {
$username = is_null($username) ? '' : $username;
parent::setUsername($username);
$this->setEmail($username);
return $this;
}

Remove the field from the FormType:

public function buildForm(FormBuilder $builder, array $options) {
parent::buildForm($builder, $options);
$builder->remove('email');
}

BUT before hack it, you should have a look to this presentation from jolicode.

If you are currently doing this kind of modifications, it is because FosUserBundle is not adapted to your project. I think you shouldn't use it. (Personnaly, I think this is not a good bundle, read the complete presentation above to make your own opinion)

If you want to replace it, I advice you to use this excellent tutorial to create your own security system. (Time to code/paste/understand it : 2 or 3 hours)

FOSUserBundle override mapping to remove need for username

I think the easiest way to go about this is to leave the bundle as is and rather setup your user class to have a username equal to the email address.

Do this by overriding the setEmail() method to also set the $username property to the $email parameter and the setEmailCanonical() to also set the $usernameCanonical to the $emailCanonical.

public function setEmail($email){
$this->email = $email;
$this->username = $email;
}

public function setEmailCanonical($emailCanonical){
$this->emailCanonical = $emailCanonical;
$this->usernameCanonical = $emailCanonical;
}

All you will have to do other than this is semantics related. Like having your form label read E-mail instead of the default Username label. You can do this by overriding the translations files. I'll leave this up to you (or someone else) since it might not even be necessary for you.

With this strategy you will have redundant data in your database but it will save you a lot of remapping headache.

Set the email manually with FOSUserBundle

You should write event listener for fos_user.registration.initialize. From code docs:

/**
* The REGISTRATION_INITIALIZE event occurs when the registration process is initialized.
*
* This event allows you to modify the default values of the user before binding the form.
* The event listener method receives a FOS\UserBundle\Event\UserEvent instance.
*/
const REGISTRATION_INITIALIZE = 'fos_user.registration.initialize';

More info about event dispatcher: http://symfony.com/doc/current/components/event_dispatcher/introduction.html
And example event listener: http://symfony.com/doc/current/cookbook/service_container/event_listener.html

UPDATE - how to code?

In your config.yml (or services.yml or other extension like xml, php) define service like this:

demo_bundle.listener.user_registration:
class: Acme\DemoBundle\EventListener\Registration
tags:
- { name: kernel.event_listener, event: fos_user.registration.initialize, method: overrideUserEmail }

Next, define listener class:

namespace Acme\DemoBundle\EventListener;

class Registration
{
protected function overrideUserEmail(UserEvent $args)
{
$request = $args->getRequest();
$formFields = $request->get('fos_user_registration_form');
// here you can define specific email, ex:
$email = $formFields['username'] . '@sth.com';
$formFields['email'] = $email;
$request->request->set('fos_user_registration_form', $formFields);
}
}

Notice: Of course you can validate this email via injecting @validator to the listener.

Now you should hide email field in registration form. YOu can do that by overriden register_content.html.twig or (in my oppinion better way) override FOS RegistrationFormType like this:

namespace Acme\DemoBundle\Form\Type;

use FOS\UserBundle\Form\Type\RegistrationFormType as BaseType;
use Symfony\Component\Form\FormBuilderInterface;

class RegistrationFormType extends BaseType
{
// some code like __construct(), getName() etc.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// some code for your form builder
->add('email', 'hidden', array('label' => 'form.email', 'translation_domain' => 'FOSUserBundle'))
;
}
}

Now your application is ready for setting email manually.

Symfony 3 FosUserBundle non unique username login

So after looking through alot of the documentation for the Symfony Security module we figured it out.

We added an extra field (displayname) to the User model because Symfony is completely build around the fact that usernames are Unique. It always fetches the first user with the given username, this is not what we wanted.

So we started with writing our own Guard Authentication System, this was pretty straight forward although we had to make some adjustments.
This was all working well, but we ran into a problem with the built-in UsernamePasswordFormAuthenticationListener, this listener was still picking up the displayname from the login form. We actually want the unique username so that Symfony knows which user to use.

We created a custom listener that extended the standard listener and made sure the username was not fetched from the login form but from the user token.

So our flow is now like this: The user fills in his username (actually his displayname) and password, the system fetches all users with that displayname. Then we loop these users and check if someone has that password. If so, authenticate the user.
On user create the admin fills in the displayname and the system will autoincrement this as a username. (admin_1, admin_2, ...).

We have to monitor if what @kero said is true, but with Bcrypt it seems that even with simple passwords like "123", it results in a different hash for each user.

The only thing that is left is to have a UniqueConstraint on the unique combination of the displayname and email. If anyone knows how this can be achieved in our orm.xml and form, thank you.

http://symfony.com/doc/current/security/guard_authentication.html

Custom Guard Authenticator

class Authenticator extends AbstractGuardAuthenticator
{
private $encoderFactory;
private $userRepository;
private $tokenStorage;
private $router;

public function __construct(EncoderFactoryInterface $encoderFactory, UserRepositoryInterface $userRepository, TokenStorageInterface $tokenStorage, Router $router)
{
$this->encoderFactory = $encoderFactory;
$this->userRepository = $userRepository;
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}

/**
* Called on every request. Return whatever credentials you want,
* or null to stop authentication.
*/
public function getCredentials(Request $request)
{
$encoder = $this->encoderFactory->getEncoder(new User());
$displayname = $request->request->get('_username');
$password = $request->request->get('_password');

$users = $this->userRepository->findByDisplayname($displayname);

if ($users !== []) {
foreach ($users as $user) {
if ($encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt())) {
return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}
} else {
if ($this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();

return ['username' => $user->getUsername(), 'password' => $user->getPassword()];
}
}

return null;
}

public function getUser($credentials, UserProviderInterface $userProvider)
{
if ($credentials !== null) {
return $userProvider->loadUserByUsername($credentials["username"]);
}

return null;
}

public function checkCredentials($credentials, UserInterface $user)
{
if ($user !== null) {
return true;
} else {
return false;
}
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return null;
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$exclusions = ['/login'];

if (!in_array($request->getPathInfo(), $exclusions)) {
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
throw $exception;
}
}

/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
// you might translate this message
'message' => 'Authentication Required'
);

return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}

public function supportsRememberMe()
{
return false;
}
}

Custom listener

class CustomAuthListener extends UsernamePasswordFormAuthenticationListener
{
private $csrfTokenManager;
private $tokenStorage;

public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $providerKey, AuthenticationSuccessHandlerInterface $successHandler, AuthenticationFailureHandlerInterface $failureHandler, array $options = array(), LoggerInterface $logger = null, EventDispatcherInterface $dispatcher = null, CsrfTokenManagerInterface $csrfTokenManager = null)
{
parent::__construct($tokenStorage, $authenticationManager, $sessionStrategy, $httpUtils, $providerKey, $successHandler, $failureHandler, array_merge(array(
'username_parameter' => '_username',
'password_parameter' => '_password',
'csrf_parameter' => '_csrf_token',
'csrf_token_id' => 'authenticate',
'post_only' => true,
), $options), $logger, $dispatcher);

$this->csrfTokenManager = $csrfTokenManager;
$this->tokenStorage = $tokenStorage;
}

/**
* {@inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($user = $this->tokenStorage->getToken() !== null) {
$user = $this->tokenStorage->getToken()->getUser();
$username = $user->getUsername();

if ($this->options['post_only']) {
$password = ParameterBagUtils::getParameterBagValue($request->request, $this->options['password_parameter']);
} else {
$password = ParameterBagUtils::getRequestParameterValue($request, $this->options['password_parameter']);
}

if (strlen($username) > Security::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Invalid username.');
}

$request->getSession()->set(Security::LAST_USERNAME, $username);

return $this->authenticationManager->authenticate(new UsernamePasswordToken($username, $password, $this->providerKey));
} else {
return null;
}
}
}

Listener service

<service id="security.authentication.listener.form" class="Your\Path\To\CustomAuthListener" parent="security.authentication.listener.abstract" abstract="true" />

custom form to accept only specific email address FOSUserBundle 2.0/ Symfony3

I would not use the Event Dispatcher component for this, I'll just go simpler and create a custom validation constraint. Here is the link to the docs.

You can create a constraint that will check if, for example, your user has an specific domain in their email. Is the constraint doesn't pass, validation will fail, and thus, the registration process, giving you proper error information.

The docs explain it really well, but if you need help with the implementation let me know and I can update my answer.

UPDATE:

class RegistrationForm extends OtherForm
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('firstName', TextType::class, [
'required' => true,
'constraints' => [
new NotBlank(),
new Length(['min' => 2, 'max' => 20]),
]
])
->add('lastName', TextType::class, [
'required' => true,
'constraints' => [
new NotBlank(),
new Length(['min' => 2, 'max' => 20]),
]
])
->add('secondLastName', TextType::class, [
'required' => true,
'constraints' => [
new NotBlank(),
new Length(['min' => 2, 'max' => 20]),
]
])
->add('email', TextType::class, [
'required' => true,
'constraints' => [
new NotBlank(),
new CustomConstraint(),
new Length(['min' => 2, 'max' => 20]),
]
])
;
}

public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'csrf_protection' => false,
'allow_extra_fields' => true,
]);
}

FOSUserBundle: Remove unique index for emailCanonical

The only way to do this is to extend the FOS\UserBundle\Model\User class and then re-do all of the mapping (everything in User.orm.xml) yourself.

Sources:

  • Replacing the mapping of the bundle
  • FOSUserBundle Issue #345


Related Topics



Leave a reply



Submit