Search code examples
phpauthenticationsymfonysymfony-2.3

Symfony Custom Authentication Provider - User Not Fully Logged In (Logged In, Not Authenticated)


I'm working on a creating a custom authentication provider. I've written my own Authentication Provider, Listener, Token and everything. It's based off a form login, and I've stepped through the code and everything seems to be configured properly. Everything is called in the right order, and my authentication provider is invoked perfectly. The authentication provider successfully authenticates the user, and returns the authenticated token. I extend AbstractAuthenticationListener which, in the handle method, will set the security context.

The user seems to be logged in, but in the debug toolbar, the token is not set and I see "You are not authenticated" and "No token".

Is there any configuration settings that I'm missing? Why would the user would be logging in, authentication provider returning successfully, with an authenticated token, being set in the security context but still be not authenticated? Any tips on how to debug this?

(I will post code as needed.)

EDIT: Token Definition:

This is very simple, just extending from AbstractToken:

class UserToken extends AbstractToken
{
    private $username;
    private $password;
    private $domain;
    private $providerKey;

    public function __construct($username, $password, $domain, $provider_key, array $roles = array('ROLE_USER'))
    {
        parent::__construct($roles);
        $this->username = $username;
        $this->password = $password;
        $this->domain = $domain;
        $this->providerKey = $provider_key;

    }

    public function getCredentials()
    {
        return '';
    }

    function getUsername() {
        return $this->username;
    }

    function getDomain() {
        return $this->domain;
    }
    function getPassword() {
        return $this->password;
    }

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

Authentication Listener:

class Listener extends AbstractAuthenticationListener
{
    protected $authenticationManager;

    public function __construct(
      SecurityContextInterface $securityContext,
      AuthenticationManagerInterface $authenticationManager,
      SessionAuthenticationStrategyInterface $sessionStrategy,
      HttpUtils $httpUtils,
      $providerKey,
      AuthenticationSuccessHandlerInterface $successHandler,
      AuthenticationFailureHandlerInterface $failureHandler,
      array $options = array(),
      LoggerInterface $logger = null,
      EventDispatcherInterface $dispatcher = null
      //CsrfProviderInterface $csrfProvider = null
    ) {

        parent::__construct(
          $securityContext,
          $authenticationManager,
          $sessionStrategy,
          $httpUtils,
          $providerKey,
          $successHandler,
          $failureHandler,
          array_merge(
            array(
              'username_parameter' => '_username',
              'password_parameter' => '_password',
              'domain_parameter' => '_domain',
              'csrf_parameter' => '_csrf_token',
              'intention' => 'authenticate',
              'post_only' => true,
            ),
            $options
          ),
          $logger,
          $dispatcher
        );

    }

    /**
     * Performs authentication.
     *
     * @param Request $request A Request instance
     *
     * @return TokenInterface|Response|null The authenticated token, null if full authentication is not possible, or a Response
     *
     * @throws AuthenticationException if the authentication fails
     */
    protected function attemptAuthentication(Request $request)
    {
        // Create initial unauthenticated token and pass data to the authentication manager.
        // TODO validate request data.
        $username = trim($request->request->get($this->options['username_parameter'], null, true));
        $password = $request->request->get($this->options['password_parameter'], null, true);
        $domain = $request->request->get($this->options['domain_parameter'], null, true);
        $token =  $this->authenticationManager->authenticate(new UserToken($username, $password, $domain, $this->providerKey));
        return $token;
    }
}

The above code will invoke the the auth function on the provider via the AuthenticationManager:

//This is from the AuthenticationProvider
public function authenticate(TokenInterface $token) {
    $loginHandler = new LoginAuthenticationHandler($token->getUsername(), $token->getPassword(), $token->getDomain());
 //This code just calls our web service and authenticates. I removed some business logic here, but this shows the gist of it.
    if(!$boAuthenticationToken = $loginHandler->authenticate())
    {
        throw new AuthenticationException('Bad credentials');
    }
    else{
        $user = $this->userProvider->loadUserByUsername($token->getUsername());
        //$user = $this->userProvider->getUser($token, $boAuthenticationToken);

        // Set the user which will be invoked in the controllers.
        $token->setUser($user);
        $token->setAuthenticated(true);
        return $token;
    }
}

Bundle Services.yml

parameters:

services:
    ws.security.authentication.provider:
      #http://blog.vandenbrand.org/2012/06/19/symfony2-authentication-provider-authenticate-against-webservice/
      class: Aurora\OurCustomBundle\Security\Authentication\Provider\Provider
      arguments: ["bo_remove_this_with_bo_auth_service", "", "@security.user_checker", "", "@security.encoder_factory"]

    ws.security.authentication.listener:
      class: Aurora\OurCustomBundle\Security\Firewall\Listener
      parent: security.authentication.listener.abstract
      abstract: true
      #arguments: []
      arguments: ["@security.context", "@security.authentication.manager", "@security.authentication.session_strategy", "@security.http_utils", "ws.user_provider", "@security.authentication.customized_success_handler", "@main.cas.rest.user.authentication.failure.service"]


    ws.user_provider:
      class: Aurora\OurCustomBundle\Security\User\UserProvider

And lastly, the UserProvider

class UserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        //Just return a simple user for now.
        return new User($username, array('ROLE_USER'));
    }

    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 === 'Aurora\OurCustomBundle\Security\User\User';
    }
}

Solution

  • After many hours of hair pulling, I figured out the problem!

    The token implementation was incorrect. Since I was implementing my own Token, which extends from AbstractToken, I needed to also implement the serialize() and unserialize() functions.

    Once I did that, the code worked. The updated Token class is below for future reference:

    class UserToken extends AbstractToken
    {
        private $username;
        private $password;
        private $domain;
        private $providerKey;
    
        public function __construct($username, $password, $domain, $provider_key, array $roles = array('ROLE_USER'))
        {
            parent::__construct($roles);
            $this->username = $username;
            $this->password = $password;
            $this->domain = $domain;
            $this->providerKey = $provider_key;
    
        }
    
        public function getCredentials()
        {
            return '';
        }
    
        function getUsername() {
            return $this->username;
        }
    
        function getDomain() {
            return $this->domain;
        }
        function getPassword() {
            return $this->password;
        }
    
        function getProviderKey(){
            return $this->providerKey;
        }
    
        function serialize(){
            return serialize(array($this->username, $this->password, $this->domain, parent::serialize()));
        }
    
        function unserialize($serialized){
            list($this->username, $this->password, $this->domain, $parentSerialization) = unserialize($serialized);
            parent::unserialize($parentSerialization);
        }
    }