Question

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;
    }
}
Was it helpful?

Solution

I've resolved modifying security.yml

firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
            pattern:  ^/app/login$
            security: false

        secured_area:
            pattern:    ^/app
            api: true
            logout:
                path:   /app/logout
                target: /

and ApiListener.php

public function handle(GetResponseEvent $event) {

        if( $this->securityContext->getToken() ){
            return;
        }

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

public function securityCheckAction() {
        // The security layer will NOT intercept this request
        return $this->redirect($this->generateUrl('manuel_myapp_index_after_login'));

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 !

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top