symfony2 login_check and custom authentication provider

Arkan picture Arkan · Aug 14, 2012 · Viewed 14k times · Source

I have the following code files from my project that works perfectly relating to custom authentication provider, and I can intercept the request in the authenticate(TokenInterface $token), I've put echo "it worked"; exit; to print the message on the screen to let me know that it entered authenticate() in the file AuthProvider.php.

But the problem rises when I try to change the login_path:/login to login_path:/user/login and check_path:/login_check to check_path:/user/login_check and also modify the routing.yml to let UserBundle to have the prefix /user, then the code will work but it will not enter the authenticate(), but it will print "The presented password is invalid" instead of "Bad credentials", how can I change the login_check & check_path to other values otherthan login & login_check respectively and to let my custom authentication provider works with the new settings?

// app/config/security.yml

security:
    factories:
        - "%kernel.root_dir%/../src/FD/UserBundle/Resources/config/security_factories.yml"

    firewalls:
        checkpoint:
            pattern: ^/
            user: true
            form_login:
                login_path: /login
                check_path: /login_check
            logout:
                path:   /logout
                target: /
            anonymous: ~

    encoders:
        FD\UserBundle\Entity\User: sha512

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
         user_provider:
             id: user_provider_service

    access_control:
        - { path: ^/_internal, role: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
        - { path: ^/user/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/event/new, roles: ROLE_ADMIN }
        - { path: ^/hello, roles: ROLE_USER }

// app/config/config.yml

imports:
    - { resource: parameters.ini }
    - { resource: security.yml }
    - { resource: @UserBundle/Resources/config/services.yml }

framework:
    #esi:             ~
    translator:      { fallback: %locale% }
    secret:          %secret%
    charset:         UTF-8
    router:          { resource: "%kernel.root_dir%/config/routing.yml" }
    form:            true
    csrf_protection: false
    validation: { enabled: true }
    #validation:      { enable_annotations: true }
    templating:      { engines: ['twig'] } #assets_version: SomeVersionScheme
    session:
        default_locale: %locale%
        auto_start:     true

# Twig Configuration
twig:
    debug:            %kernel.debug%
    strict_variables: %kernel.debug%

# Assetic Configuration
assetic:
    debug:          %kernel.debug%
    use_controller: false
    # java: /usr/bin/java
    filters:
        cssrewrite: ~
        # closure:
        #     jar: %kernel.root_dir%/java/compiler.jar
        # yui_css:
        #     jar: %kernel.root_dir%/java/yuicompressor-2.4.2.jar

# Doctrine Configuration
doctrine:
    dbal:
        default_connection:    default
        connections:
            default:      
                driver:   %database_driver%
                host:     %database_host%
                port:     %database_port%
                dbname:   %database_name%
                user:     %database_user%
                password: %database_password%
                charset:  UTF8
                mapping_types:
                    enum: string
                    set: string
                    blob: object
    orm:
        auto_generate_proxy_classes: %kernel.debug%    
        default_entity_manager:         default # The first defined is used if not set
        entity_managers:
            default:      
                # The name of a DBAL connection (the one marked as default is used if not set)
                connection: ~
                mappings: # Required
                    FDHelloBundle: ~
                    UserBundle: { type: annotation }

      # mappings:
        #   FDHelloBundle: { type: yml, dir: Resources/config/doctrine/metadata/orm }         

# Swiftmailer Configuration
swiftmailer:
    transport: %mailer_transport%
    host:      %mailer_host%
    username:  %mailer_user%
    password:  %mailer_password%

jms_security_extra:
    secure_controllers:  true
    secure_all_services: false

services:
    fd_hello.twig.extension.debug:
        class:        Twig_Extension_Debug
        tags:
             - { name: 'twig.extension' }

    user_provider_service:
        class: FD\UserBundle\Security\User\UserProvider

// app/config/routing.yml

FDHelloBundle:
    resource: "@FDHelloBundle/Resources/config/routing.yml"
    prefix:   /

FDUserBundle:
    resource: "@UserBundle/Controller"
    prefix:   /user
    type: annotation

// src/FD/UserBundle/Controller/LoginController.php

namespace FD\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Security\Core\SecurityContext;

class LoginController extends Controller
{
    /**
     * @Route("login", name="login")
     */
    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();

        // get the login error if there is one
        $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
        $session->remove(SecurityContext::AUTHENTICATION_ERROR);

        return $this->render('UserBundle:Login:login.html.twig', array(
            // last username entered by the user
            'last_username' => $session->get(SecurityContext::LAST_USERNAME),
            'error'         => $error,
        ));
    }

    /**
     * @Route("login_check", name="login_check")
     */
    public function loginCheckAction()
    {

    }

    /**
     * @Route("logout", name="logout")
     */
    public function logoutAction()
    {

    }
}

// src/FD/UserBundle/DependencyInjection/Security/Factory/UserFactory.php

namespace FD\UserBundle\DependencyInjection\Security\Factory;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;

class UserFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.user.'.$id;
        $container
            ->setDefinition($providerId, new DefinitionDecorator('user.security.authentication.provider'))
            ->replaceArgument(0, new Reference($userProvider))
        ;
        $listenerId = 'security.authentication.listener.user.'.$id;
        $listener = $container->setDefinition($listenerId, new DefinitionDecorator('user.security.authentication.listener'));

        return array($providerId, $listenerId, $defaultEntryPoint);
    }

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'user';
    }

    public function addConfiguration(NodeDefinition $node)
    {}
}

// FD/UserBundle/Resources/config/services.yml

services:
    user.security.authentication.provider:
        class:  FD\UserBundle\Security\Authentication\Provider\AuthProvider
        arguments: ['', %kernel.cache_dir%/security/nonces]

    user.security.authentication.listener:
        class:  FD\UserBundle\Security\Firewall\AuthListener

// FD/UserBundle/Resources/config/security_factories.yml

services:
    security.authentication.factory.user:
        class:  FD\UserBundle\DependencyInjection\Security\Factory\UserFactory
        tags:
            - { name: security.listener.factory }

// FD/UserBundle/Security/Authentication/Provider/AuthProvider.php

namespace FD\UserBundle\Security\Authentication\Provider;

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;

use FD\UserBundle\Security\Authentication\Token\UserToken;

class AuthProvider implements AuthenticationProviderInterface
{
    private $userProvider;
    private $cacheDir;

    public function __construct(UserProviderInterface $userProvider, $cacheDir)
    {
        $this->userProvider = $userProvider;
        $this->cacheDir     = $cacheDir;
    }

    public function authenticate(TokenInterface $token)
    {
//        $user = $this->userProvider->loadUserByUsername($token->getUsername());
//        $userToken = new UserToken();
//        $userToken->setUser($user);
echo "it worked"; exit;
        $newToken = new UserToken($token->getUser(), $token->getCredentials(), "user", array("ROLE_ADMIN"));
        $username = $newToken->getUser();
        if (empty($username)) {
            throw new BadCredentialsException('Bad credentials :)');
        }
        return $newToken;

//        if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) {
//            $authenticatedToken = new UserToken($user->getRoles());
//            $authenticatedToken->setUser($user);
//
//            return $authenticatedToken;
//        }
    }

    public function supports(TokenInterface $token)
    {
        return $token instanceof UserToken;
    }
}

// FD/UserBundle/Security/Authenticaion/Token/UserToken.php

namespace FD\UserBundle\Security\Authentication\Token;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;

/**
 * UsernamePasswordToken implements a username and password token.
 *
 */

class UserToken extends AbstractToken
{
    private $credentials;
    private $providerKey;

    /**
     * Constructor.
     *
     * @param string $user        The username (like a nickname, email address, etc.)
     * @param string $credentials This usually is the password of the user
     * @param string $providerKey The provider key
     * @param array  $roles       An array of roles
     *
     * @throws \InvalidArgumentException
     */
    public function __construct($user, $credentials, $providerKey, array $roles = array())
    {
        parent::__construct($roles);

        if (empty($providerKey)) {
            throw new \InvalidArgumentException('$providerKey must not be empty.');
        }

        $this->setUser($user);
        $this->credentials = $credentials;
        $this->providerKey = $providerKey;

        parent::setAuthenticated(count($roles) > 0);
    }

    /**
     * {@inheritdoc}
     */
    public function setAuthenticated($isAuthenticated)
    {
        if ($isAuthenticated) {
            throw new \LogicException('Cannot set this token to trusted after instantiation.');
        }

        parent::setAuthenticated(false);
    }

    public function getCredentials()
    {
        return $this->credentials;
    }

    public function getProviderKey()
    {
        return $this->providerKey;
    }

    /**
     * {@inheritdoc}
     */
    public function eraseCredentials()
    {
        parent::eraseCredentials();

        $this->credentials = null;
    }

    public function serialize()
    {
        return serialize(array($this->credentials, $this->providerKey, parent::serialize()));
    }

    public function unserialize($str)
    {
        list($this->credentials, $this->providerKey, $parentStr) = unserialize($str);
        parent::unserialize($parentStr);
    }
}

// FD/UserBundle/Security/Firewall/AuthListener.php

namespace FD\UserBundle\Security\Firewall;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
use Symfony\Component\Security\Http\HttpUtils;


use FD\UserBundle\Security\Authentication\Token\UserToken;

class AuthListener extends AbstractAuthenticationListener
{
    protected $securityContext;
    protected $authenticationManager;
    protected $httpUtils;

    public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager,
                                SessionAuthenticationStrategyInterface $sessionStrategy, HttpUtils $httpUtils, $options = array())
    {
        parent::__construct($securityContext, $authenticationManager, $sessionStrategy, $httpUtils, "user", array_merge(array(
            'username_parameter' => '_username',
            'password_parameter' => '_password',
            'intention' => 'authenticate',
            'post_only' => true,
        ), $options));
    }

    /**
     * Performs authentication.
     *
     * @param  Request $request A Request instance
     *
     * @return TokenInterface The authenticated token, or null if full authentication is not possible
     *
     * @throws AuthenticationException if the authentication fails
     */
    protected function attemptAuthentication(Request $request)
    {

        $username = trim($request->get($this->options['username_parameter'], null, true));
        $password = $request->get($this->options['password_parameter'], null, true);

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

        return $this->authenticationManager->authenticate(new UserToken($username, $password, $this->providerKey));

    }

    public function getHttpUtils()
    {
        return $this->httpUtils;
    }

    public function setHttpUtils($httpUtils)
    {
        $this->httpUtils = $httpUtils;
    }
}

// src/FD/UserBundle/Security/User/UserProvider.php

namespace FD\UserBundle\Security\User;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;

use FD\UserBundle\Entity\User;

class UserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        // make a call to your webservice here
        // $userData = ...
        // pretend it returns an array on success, false if there is no user
        $user = new User();
        $user->setUsername($username);
        $user->setPassword("1234");
        $user->setRoles(array("ROLE_ADMIN"));

        return $user;

//        if ($userData) {
//            // $password = '...';
//            // ...
//
//            return new WebserviceUser($username, $password, $salt, $roles)
//        } else {
//            throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
//        }
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'FD\UserBundle\Entity\User';
    }
}

Answer

Vitalii Zurian picture Vitalii Zurian · Aug 14, 2012

Seems like you are doing everything correct.

Probably you are importing wrong resource

FDUserBundle:
    resource: "@UserBundle/Controller"

Shouldn't it be as below?

FDUserBundle:
    resource: "@FDUserBundle/Controller"