Extend eZ 5.3 – Change user authentication to login with email

Sometimes it is required that a user needs to be logged in additionally via email address, not only via username. In eZ Publish prior to version 5.3 this was possible since eZ provided legacy settings (site.ini) for it:

[UserSettings]
AuthenticateMatch=login;email

Starting with eZ 5.3 the logic has been changed and eZ Publish is now using the security feature of Symfony. This means a login with the email address isn´t possible anymore and eZ will use the login only.  The new version seems to ignore the settings defined in „AuthenticateMatch“.

If you want to allow this functionality, there are two things that you need to do:

  • Override eZ UserProvider
  • Override eZ AuthenticationProvider

Override eZ UserProvider

Override the functionality

<?php
/**
 * 
 *
 * This file contains the class UserProvider
 *
 * @version $Version$
 * @package silver.e-shop
 */
 
namespace Silversolutions\Bundle\EshopBundle\Service\Security;
 
use eZ\Publish\Core\MVC\Symfony\Security\User\Provider;
use eZ\Publish\Core\MVC\Symfony\Security\User;
use eZ\Publish\Core\MVC\Symfony\Security\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
 
/**
 * This provider is responsible for loading the user from eZ
 * eZ functionality is overridden here to be able to load user additionally via email address
 * 
 *
 * Class UserProvider
 */
class UserProvider extends Provider
{
 
    /**
     * override the eZ functionality to fetch user by username or additionally email address
     * $user can be either the username/email or an instance of \eZ\Publish\Core\MVC\Symfony\Security\User
     *
     * @param string|\eZ\Publish\Core\MVC\Symfony\Security\User $user
     * Either the username/email to load an instance of User object.
     *
     * @return \eZ\Publish\Core\MVC\Symfony\Security\UserInterface
     *
     * @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException if the user is not found
     */
    public function loadUserByUsername($user)
    {
        try {
            // SecurityContext always tries to authenticate anonymous users when checking granted access.
            // In that case $user is an instance of \eZ\Publish\Core\MVC\Symfony\Security\User.
            // We don't need to reload the user here.
            if ($user instanceof UserInterface) {
                return $user;
            }
 
            return new User($this->repository->getUserService()->loadUserByLogin($user), array('ROLE_USER'));
        } catch (NotFoundException $e) {
            try {
                $users = $this->repository->getUserService()->loadUsersByEmail($user);
                if (isset($users[0])) {
                    return new User($users[0], array('ROLE_USER'));
                }
            } catch (NotFoundException $e) {
                throw new UsernameNotFoundException( $e->getMessage(), 0, $e );
            }
            throw new UsernameNotFoundException( $e->getMessage(), 0, $e );
        }
    }
 
}

Override the service definition

<parameters>
        <!-- override the security providers -->
        <parameter key="ezpublish.security.user_provider.class">Silversolutions\Bundle\EshopBundle\Service\Security\UserProvider</parameter>
</parameters>

Override eZ AutheticationProvider

Override the functionality

<?php
/**
 
 *
 * This file contains the class AuthentificationProvider
 *
 *  
 * @version $Version$
 * @package silver.e-shop
 */
 
namespace Silversolutions\Bundle\EshopBundle\Service\Security;
 
use eZ\Publish\Core\MVC\Symfony\Security\Authentication\RepositoryAuthenticationProvider;
use eZ\Publish\API\Repository\Exceptions\NotFoundException;
use eZ\Publish\API\Repository\Repository;
use eZ\Publish\Core\MVC\Symfony\Security\User as EzUser;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserInterface;
 
/**
 * This provider is responsible for user authentication
 * eZ functionality is overridden here to be able to load user additionally via email address
 * or later the load user from different trees
 *
 * additionally the session id of anonymous user is stored in the session here
 *
 * Class AuthenticationProvider
 */
class AuthenticationProvider extends RepositoryAuthenticationProvider
{
    const SES_ANONYMOUS_SESSION_ID = 'sesAnonymousSessionId';
 
    /**
     * @var \eZ\Publish\API\Repository\Repository $repository
     */
    protected $repository;
 
    /**
     * @var Session $session
     */
    protected $session;
 
    /**
     * set the dependency to the repository
     *
     * @param Repository $repository
     */
    public function setRepository(Repository $repository)
    {
        $this->repository = $repository;
    }
 
    /**
     * set the dependency to the session
     *
     * @param Session $session
     */
    public function setSession(Session $session)
    {
        $this->session = $session;
    }
 
    /**
     * override the eZ functionality to fetch user additionally by email address
     * if the user was authenticated successfully the session id of anonymous user
     * is stored in the session for later purposes
     *
     * @param UserInterface $user
     * @param UsernamePasswordToken $token
     * @return bool|void
     * @throws \Symfony\Component\Security\Core\Exception\BadCredentialsException
     */
    protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
    {
        if (!$user instanceof EzUser) {
            return parent::checkAuthentication($user, $token);
        }
 
        // $currentUser can either be an instance of UserInterface or just the username/email (e.g. during form login).
        /** @var EzUser|string $currentUser */
        $currentUser = $token->getUser();
        if ($currentUser instanceof UserInterface) {
            if ($currentUser->getPassword() !== $user->getPassword()) {
                throw new BadCredentialsException( 'The credentials were changed from another session.' );
            }
 
            $apiUser = $currentUser->getAPIUser();
        } else  {
            try {
                $apiUser = $this->repository->getUserService()
                    ->loadUserByCredentials($token->getUsername(), $token->getCredentials());
            } catch (NotFoundException $e) {
                //user was not found by username, try to get the login and load by credentials again
                try {
                    $users = $this->repository->getUserService()->loadUsersByEmail($token->getUsername());
                    $userLogin = $users[0]->login;
                    $apiUser = $this->repository->getUserService()
                        ->loadUserByCredentials($userLogin, $token->getCredentials());
                } catch(NotFoundException $e) {
                    throw new BadCredentialsException('Invalid credentials', 0, $e);
                }
            }
        }
 
        //store session id the session before it will be refreshed by Symfony
        $this->storeAnonymousSessionId();
        // Finally inject current user in the Repository
        $this->repository->setCurrentUser($apiUser);
 
        return true;
    }
 
    /**
     * stores the session id of the anonymous user in the session
     */
    protected function storeAnonymousSessionId()
    {
        $sessionId = $this->session->getId();
 
        $this->session->set(self::SES_ANONYMOUS_SESSION_ID, $sessionId);
    }
 
}

Override the service definition

<parameters>
        <!-- override the security providers -->
        <parameter key="security.authentication.provider.dao.class">Silversolutions\Bundle\EshopBundle\Service\Security\AuthenticationProvider</parameter>
</parameters>
 
<services>
        <service id="security.authentication.provider.dao" class="%security.authentication.provider.dao.class%" abstract="true" public="false">
            <argument /> <!-- User Provider -->
            <argument type="service" id="security.user_checker" />
            <argument /> <!-- Provider-shared Key -->
            <argument type="service" id="security.encoder_factory" />
            <argument>%security.authentication.hide_user_not_found%</argument>
            <call method="setRepository">
                <argument type="service" id="ezpublish.api.repository" />
            </call>
            <call method="setSession">
                <argument type="service" id="session" />
            </call>
        </service>
</services>

 

Many thanks to our product development team, in particular to Andrea Zhupa, who figured this one out.

 

 

3 thoughts on “Extend eZ 5.3 – Change user authentication to login with email

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.