0
votes

Things we want to achieve in our application are:

  • Non-unique usernames [Done]
  • Unique username and email combination
  • FosUserBundle will fetch all users (on user login) with given username and checks if any of the users has the given password (hashed with bcrypt). When a user is found it logs the user in.

Making username non unique was quite simple by just overriding the username field in the user ORM. But we're kinda stuck with how to proceed in achieving the last two points. We've started creating a custom User Provider but it seems Symfony Security can only handle one user(name).

Is there anyone with experience that might be able to help us? If you need more information or code snippets, please ask. Thank you in advance!

1
IMO you should not use FOSUserBundle to achieve this, you should create your own UserBundle. - Pierre
We're currently looking into implementing our own Guard, it seems to work nicely with FOSUserBundle and it's coming together as we speak. Source: symfony.com/doc/current/security/guard_authentication.html - Stephan Bisschop
And if two people with same username share the same password? - kero
Try adding @UniqueEntity with both fields before your user class - Namoz
@kero We're using bcrypt to hash our passwords, it seems it generates a random salt for each user. Namoz, the combination has to be unique, multiple usernames can be attached to one email address. - Stephan Bisschop

1 Answers

1
votes

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" />