I'm implementing a custom authentication provider for using an external api following roughly the cookbook on the symfony website. It works almost everything correctly, the listener listens the login form properly, then it calls the authenticate function which returns the authenticated token, the problem is that even if i set a authenticated token to the securityContextInterface, the system returns to the login page with wrong credentials. Under the code i've used What could it be?
security.yml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
login:
pattern: ^/app/login$
security: false
api_secured:
provider: in_memory
pattern: ^/app
form_login:
login_path: /app/login
check_path: /app/login_check
logout:
path: /app/logout
target: /
api: true
services.yml
api.security.authentication.provider:
class: Manuel\Myapp\MyAppBundle\Security\Authentication\Provider\ApiProvider
arguments: ['', %kernel.cache_dir%/security/nonces]
api.security.authentication.listener:
class: Manuel\Myapp\MyAppBundle\Security\Firewall\ApiListener
arguments: [@security.context, @security.authentication.manager, %api.url%]
ApiFactory.php
namespace Manuel\Myapp\MyAppBundle\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 ApiFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.api.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('api.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentication.listener.api.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('api.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'api';
}
public function addConfiguration(NodeDefinition $node)
{
}
}
ApiListener.php
namespace Manuel\Myapp\MyAppBundle\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 Manuel\Myapp\MyAppBundle\Security\Authentication\Token\ApiUserToken;
use Httpful\Request;
class ApiListener implements ListenerInterface {
protected $securityContext;
protected $authenticationManager;
protected $container;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, $api)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
//Prendo l'url delle api
//Viene passato da services.yml alla classe
$this->api = $api;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$data = $request->request->all();
//Esiste username e password ?
if(!array_key_exists('_username', $data) || !array_key_exists('_password', $data)) {
//Ritorna alla pagina di login con bad credentials
$this->securityContext->setToken(null);
return;
}
//Autentico in remoto
$username = $data['_username'];
$password = $data['_password'];
$response = Request::post($this->api."/token/new.json")
->body(array(
'username'=> $username,
'password'=> $password))
->expectsJson()
->sendsForm()
->send();
$decode = json_decode($response);
//Se esiste allora vado avanti se no muoio
if(!$decode->success) {
$this->securityContext->setToken(null);
return;
}
$token = new ApiUserToken();
$token->setUser(''.$decode->user);
$token->token = $decode->token;
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($authToken);
} catch (AuthenticationException $failed) {
// ... si potrebbe loggare qualcosa in questo punto
// Per negare l'autenticazione, pulire il token. L'utente sarà rinviato alla pagina di login.
$this->securityContext->setToken(null);
return;
// Negare l'autenticazione con una risposta HTTP '403 Forbidden'
//$response = new Response();
//$response->setStatusCode(403);
//$event->setResponse($response);
}
}
}
If i write:
$authToken = $this->authenticationManager->authenticate($token);
var_dump($authToken); die();
$this->securityContext->setToken($authToken);
The results is:
object(Manuel\Myapp\MyAppBundle\Security\Authentication\Token\ApiUserToken)#4780 (5) {["user":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=> object(Symfony\Component\Security\Core\User\User)#4782 (7) { ["username":"Symfony\Component\Security\Core\User\User":private]=> string(4) "user" ["password":"Symfony\Component\Security\Core\User\User":private]=> string(15) "10dmao!?postino" ["enabled":"Symfony\Component\Security\Core\User\User":private]=> bool(true) ["accountNonExpired":"Symfony\Component\Security\Core\User\User":private]=> bool(true) ["credentialsNonExpired":"Symfony\Component\Security\Core\User\User":private]=> bool(true) ["accountNonLocked":"Symfony\Component\Security\Core\User\User":private]=> bool(true) ["roles":"Symfony\Component\Security\Core\User\User":private]=> array(1) { [0]=> string(9) "ROLE_USER" } } ["roles":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=> array(1) { [0]=> object(Symfony\Component\Security\Core\Role\Role)#4779 (1) { ["role":"Symfony\Component\Security\Core\Role\Role":private]=> string(9) "ROLE_USER" } } ["authenticated":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=> bool(true) ["attributes":"Symfony\Component\Security\Core\Authentication\Token\AbstractToken":private]=> array(0) { } }
So it is correct.
ApiUserToken.php
namespace Manuel\Myapp\MyAppBundle\Security\Authentication\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class ApiUserToken extends AbstractToken
{
public $token;
public function __construct(array $roles = array())
{
parent::__construct($roles);
// If the user has roles, consider it authenticated
$this->setAuthenticated(true);
}
public function getCredentials()
{
return '';
}
}
ApiProvider.php
namespace Manuel\Myapp\MyAppBundle\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 Manuel\Myapp\MyAppBundle\Security\Authentication\Token\ApiUserToken;
class ApiProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
public function __construct(UserProviderInterface $userProvider, $cacheDir)
{
$this->userProvider = $userProvider;
$this->cacheDir = $cacheDir;
}
public function authenticate(TokenInterface $token)
{
//Devo aggiungere utente
$user = $this->userProvider->loadUserByUsername("user");
if ($user) {
$authenticatedToken = new ApiUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The API authentication failed.');
}
public function supports(TokenInterface $token) {
return $token instanceof ApiUserToken;
}
}
I've resolved modifying security.yml
and ApiListener.php
Because on every url under firewall (app/*) symfony calls the handle method of my listener, if the user is already logged the security token is already setted and I return
and check_login function
check_login is the action of the login form, the check_login action is under firewall, so, the handle method of my listerner will be called for the first time, if credentials are correct (using my external api) I forced symfony to use the in_memory user for login and than the check_login action will be executed. Then, when the user visit another page under firewall, the handle method will be recalled but the authentication token is already setted, so the handle method will return and all works
External api login now works !