I'm having trouble (again) with the security component of Symfony2.
First thing first, there are severals objects involved in the process of authentication :
- SecurityFactoryInterface
- AbstractAuthenticationListener
- AuthenticationProviderInterface
- Custom Token (inherits from AbstractToken)
- UserProviderInterface
Flow
If I understand well, the aim of the Security factory is to configure the custom authentication.
The Listener is the conductor of the authentication. Throught its attemptAuthentication
method it captures the form submission (containing user's credentials) and tries to authenticate the user. Inside this method, the Listener will create an AbstractToken
and then pass the token to the authenticate
method of the AuthenticationProvider
.
After that, the AuthenticationProvider
calls the UserProvider
to retrieve the users data from a webservice or database etc...
Once the UserProvider
did its magic, it returns an User object to the AuthenticationProvider
.
The AuthenticationProvider then creates an new Token filled with the user retrieved by the UserProvider
and then retunrs it to the Listener
.
After getting the new Token, the Listener
does some unknown magic (I think it sets the token into the security context but I'm not sure).
Problem
At each steps I did a var_dump of my User object. The roles were set in every steps except in the "final steps": in the Listener
.
When the Listener
retrieve the authenticated token from the UserProvider
, user's roles are empty. I can't figure out why...
SecurityFactory
class CompanyFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.company.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('company.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider));
$listenerId = 'security.authentication.listener.company.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('company.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'company';
}
public function addConfiguration(NodeDefinition $node)
{
}
}
Listener
class CompanyListener extends AbstractAuthenticationListener
{
// Custructor stuff removed
protected function requiresAuthentication(Request $request)
{
if ($this->options['post_only'] && !$request->isMethod('post'))
{
return false;
}
return parent::requiresAuthentication($request);
}
protected function attemptAuthentication(Request $request)
{
$username = trim($request->get($this->options['username_parameter'], null, true));
$password = $request->get($this->options['password_parameter'], null, true);
$ip = $request->getClientIp();
$request->getSession()->set(SecurityContextInterface::LAST_USERNAME, $username);
$authToken = $this->authenticationManager->authenticate(new CompanyUserToken($username, $password, $ip));
return $authToken;
}
}
AuthenticationProvider
class CompanyProvider implements AuthenticationProviderInterface
{
private $userProvider;
public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsernamePassword($token->user, $token->getPassword(), $token->getIp());
$authenticatedToken = new CompanyUserToken($user->getUsername(), $user->getPassword(), $user->getIp(), $user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
public function supports(TokenInterface $token)
{
return $token instanceof CompanyUserToken;
}
}
Custom Token
class CompanyUserToken extends AbstractToken
{
private $password;
private $ip;
public function __construct($username, $password, $ip, array $roles = array())
{
parent::__construct($roles);
$this->password = $password;
$this->user = $username;
$this->ip = $ip;
// If the user has roles, consider it authenticated
$this->setAuthenticated(count($roles) > 0);
}
public function getCredentials()
{
return '';
}
public function getPassword()
{
return $this->password;
}
public function getIp()
{
return $this->ip;
}
}
User Provider
class CompanyUserProvider implements UserProviderInterface
{
private $documentManager;
public function __construct($doctrineMongoDB)
{
$this->doctrineMongoDB = $doctrineMongoDB;
}
public function loadUserByUsername($username)
{
// Not used but needed by the interface
}
public function loadUserByUsernamePassword($username, $password, $ip)
{
// Does the magic, retrieve user datas from DB.
return $user;
}
public function refreshUser(UserInterface $user)
{
// Does nearly the same thing that the above method
return $refreshedUser;
}
public function supportsClass($class)
{
return $class === 'Company\UserBundle\Document\User';
}
}
service.yml
parameters:
security.authentication.handler.class: Company\UserBundle\Security\Authentication\Handler\CompnayAuthenticationHandler
company_user_provider.class: Company\UserBundle\Security\User\CompanyUserProvider
services:
security.authentication.handler:
class: %security.authentication.handler.class%
public: false
arguments: [@router, @security.http_utils]
company.security.authentication.provider:
class: Company\UserBundle\Security\Authentication\Provider\CompanyProvider
arguments: [@company_user_provider]
company.security.authentication.listener:
class: Company\UserBundle\Security\Firewall\CompanyListener
arguments: [@security.context, @security.authentication.manager, @security.authentication.session_strategy, @security.http_utils, "company", @security.authentication.handler, @security.authentication.handler, {}, @logger, @event_dispatcher, @user.service.captcha]
company_user_provider:
class: %company_user_provider.class%
arguments: [@doctrine_mongodb]
user.service.captcha:
class: Company\UserBundle\Services\CaptchaService
arguments: [@form.factory]
security.yml
jms_security_extra:
secure_all_services: false
expressions: true
security:
encoders:
Company\UserBundle\Document\User: plaintext
role_hierarchy:
ROLE_VIP_USER: ROLE_USER
ROLE_ADMIN: [ROLE_USER, ROLE_VIP_USER]
providers:
webservice:
id: company_user_provider
firewalls:
company_secured:
pattern: ^/
company: true
anonymous: true
form_login:
login_path: login
check_path: login_check
post_only: true
use_referer: false
success_handler: security.authentication.handler
failure_handler: security.authentication.handler
logout:
path: /logout
target: login
access_control:
- { path: ^/admin, role: ROLE_ADMIN }
Update
Here are some var_dumps
to explain my problem :
UserProvider
var_dump($user);
object(Company\UserBundle\Document\User)[142]
protected 'username' => string 'Supacoco' (length=13)
protected 'roles' =>
array (size=1)
0 => string 'ROLE_ADMIN' (length=10)
AuthenticationProvider
var_dump($authenticatedToken->getUser());
object(Company\UserBundle\Document\User)[142]
protected 'username' => string 'Supacoco' (length=13)
protected 'roles' =>
array (size=1)
0 => string 'ROLE_ADMIN' (length=10)
Listener
var_dump($authToken->getUser());
object(Company\UserBundle\Document\User)[142]
protected 'username' => string 'Supacoco' (length=13)
protected 'roles' =>
array (size=0)
empty