I'm working on a web application with Symfony 2. More specifically, I'm trying to make a Custom Authentication Provider thanks to the instructions of the Symfony Cookbook.
I have to make custom authentication provider because I must not check the username and the password entered in my login form with the ones which are in stored in my database. But, anyway, that's not my problem here.
The problem is, reading my code (pasted below), anyone should be able to login entering a username which is already in my database, no matter the password is correct or not (compared to the one stored in the database) since I only check if the username entered exists in the database. But that's not the case : magically, the password IS checked and I get the 'Bad credentials' error message when I try to login with a wrong password.
I paste my code. My app/config/security.yml :
jms_security_extra:
secure_all_services: false
expressions: true
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
Epilille\UserBundle\Entity\User: plaintext
role_hierarchy:
ROLE_STUDENT: ROLE_USER
ROLE_AER: ROLE_STUDENT
ROLE_PROF: ROLE_AER
ROLE_ADMIN: ROLE_PROF
ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
in_memory:
memory:
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
student: { password: studentpass, roles: ['ROLE_STUDENT'] }
mestag_a: { password: mestag_apass, roles: ['ROLE_AER'] }
intra_provider:
entity: { class: EpililleUserBundle:User, property: username }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/login$
anonymous: true
intra_secured:
pattern: ^/
intra: true
provider: intra_provider
form_login:
login_path: login
check_path: login_check
default_target_path: epilille_home_homepage
logout:
path: logout
target: login
access_control:
# - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY}
The architecture of my UserBundle :
.
├── Controller/
│ └── SecurityController.php
├── DataFixtures/
│ └── ORM/
│ └── Users.php
├── DependencyInjection/
│ ├── Configuration.php
│ ├── EpililleUserExtension.php
│ └── Security/
│ └── Factory/
│ └── IntraFactory.php
├── Entity/
│ ├── User.php
│ └── UserRepository.php
├── EpililleUserBundle.php
├── EpiLogin.class.php
├── Resources/
│ ├── config/
│ │ └── services.yml
│ ├── doc/
│ │ └── index.rst
│ ├── public/
│ │ ├── css/
│ │ ├── images/
│ │ └── js/
│ ├── translations/
│ │ └── messages.fr.xlf
│ └── views/
│ ├── layout.html.twig
│ └── User/
│ └── login.html.twig
└── Security/
├── Authentication/
│ ├── Provider/
│ │ └── IntraProvider.php
│ └── Token/
│ └── IntraUserToken.php
├── Firewall/
│ └── IntraListener.php
└── User/
└── IntraUserProvider.php
My factory :
<?php
namespace Epilille\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 IntraFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.intra.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('intra.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentication.listener.intra.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('intra.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'intra';
}
public function addConfiguration(NodeDefinition $node)
{
}
}
My token :
<?php
namespace Epilille\UserBundle\Security\Authentication\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class IntraUserToken extends AbstractToken
{
public function __construct(array $roles = array())
{
parent::__construct($roles);
$this->setAuthenticated(count($roles) > 0);
}
public function getCredentials()
{
return '';
}
}
My authentication provider :
<?php
namespace Epilille\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 Epilille\UserBundle\Security\Authentication\Token\IntraUserToken;
class IntraProvider implements AuthenticationProviderInterface
{
private $userProvider;
public function __construct(UserProviderInterface $userProvider)
{
$this->userProvider = $userProvider;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
if ($user) {
$authenticatedToken = new IntraUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The Intranet authentication failed.');
}
public function supports(TokenInterface $token)
{
// I noticed something with this method I tell you below
return $token instanceof IntraUserToken;
}
}
My listener :
<?php
namespace Epilille\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 Epilille\UserBundle\Security\Authentication\Token\IntraUserToken;
use Epilille\UserBundle\Security\Authentication\Provider\IntraProvider;
class IntraListener implements ListenerInterface
{
protected $securityContext;
protected $authenticationManager;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
}
public function handle(GetResponseEvent $event)
{
// I have to do this check because it seems like this function is called several times and if
// it's not the first time, it will try to reset a new IntraUserToken with _username and _password not set.
if ($this->securityContext->getToken()) {
return;
}
$request = $event->getRequest();
$token = new IntraUserToken();
$token->setUser($request->get('_username'));
$token->password = $request->get('_password');
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($authToken);
} catch (AuthenticationException $failed) {
// ... you might log something here
// To deny the authentication clear the token. This will redirect to the login page.
$this->securityContext->setToken(null);
return;
// Deny authentication with a '403 Forbidden' HTTP response
/* $response = new Response(); */
/* $response->setStatusCode(403); */
/* $event->setResponse($response); */
}
}
}
So, with this code, that's what happens :
- If I enter a username which doesn't exist in my database, I get the error message 'Bad credentials'.
- If I enter a username which does exist in my database :
- with a wrong password, I get 'Bad credentials'.
- with the correct password, I'm authenticated and the profiler says I'm logged with the Token class 'UsernamePasswordToken'.
Now I want to tell you about the method IntraProvider::supports. Now, it's like that :
public function supports(TokenInterface $token)
{
return $token instanceof IntraUserToken;
}
And if I change it like that :
public function supports(TokenInterface $token)
{
return (true);
}
There's a differences with the case in which I enter a good username (no matters the password is) : I'm authenticated and the profiler now says I'm with the Token class 'IntraUserToken' (it seems to be exactly what I want).
So there my problems are :
- Why doesn't my authentication solution work with the first version of the IntraProvider::supports method ?
- Why does it work when I return true every time in the IntraProvider::supports method ?
In the Cookbook, they do it as I do in the first version of this method, so I don't think it's a good idea to return true every time, is it ?
I hope I made myself clear about my problem, thanks for your time and your answers !