1
votes

I have problem with symfony2 (I have tried the same with Symfony 2.0 & Symfony 2.3 just to see if its a Symfony bug), I am loosing the security token in the next page load / redirect after authentication.

I have created custom authenticator for Symfony 2.3 to authenticate with a 3rd party service as specified here: http://symfony.com/doc/current/cookbook/security/custom_authentication_provider.html

The application authenticates with an external service and sets the token in the callback URL '/success' and i can see from the debug bar that user is authenticated but when i go to '/' (which is under the same firewall) i am getting "A Token was not found in the SecurityContext." Error and user is no longer authenticated.

Here are the files:

security.yml

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        users:
            entity: { class: AcmeStoreBundle:User, property: email }

    firewalls:
        login:
            pattern:  ^/login$
            security: false
        noa:
            pattern:  ^/
            provider: users
            noa: true
            logout:
                path:   /logout
                target: /login

    access_control:
        - { path: ^/success, roles: IS_AUTHENTICATED_ANONYMOUSLY }

NoaUserToken.php

<?php
namespace Acme\StoreBundle\Security\Authentication\Token;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;

class NoaUserToken extends AbstractToken
{
    public $expires;
    public $mobile;
    public $email;

    public function __construct(array $roles = array())
    {
        parent::__construct($roles);

        parent::setAuthenticated(true);
    }

    public function getCredentials()
    {
        return '';
    }
}

NoaProvider.php

<?php
namespace Acme\StoreBundle\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 Acme\StoreBundle\Security\Authentication\Token\NoaUserToken;

class NoaProvider implements AuthenticationProviderInterface
{
    private $userProvider;
    private $cacheDir;

    public function __construct(UserProviderInterface $userProvider, $cacheDir)
    {
        $this->userProvider = $userProvider;
        $this->cacheDir     = $cacheDir;
    }

    public function authenticate(TokenInterface $token)
    {
        $userEmail = $token->getUser();

        $user = $this->userProvider->loadUserByUsername($userEmail);

        if ($user && $this->validateToken($token->expires) && !$user->getHidden()) {
            $authenticatedToken = new NoaUserToken($user->getRoles());
            $authenticatedToken->expires = $token->expires;
            $authenticatedToken->mobile = $token->mobile;
            $authenticatedToken->email = $token->email;
            $authenticatedToken->setUser($user);
            $authenticatedToken->setAuthenticated(true);

            return $authenticatedToken;
        }

        throw new AuthenticationException('The NOA authentication failed.');
    }

    protected function validateToken($expires)
    {
        // Check if the token has expired.
        if (strtotime($expires) <= time()) {
            return false;
        }
    }

    public function supports(TokenInterface $token)
    {
        return $token instanceof NoaUserToken;
    }
}

NoaListener.php

<?php
namespace Acme\StoreBundle\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 Acme\StoreBundle\Security\Authentication\Token\NoaUserToken;

class NoaListener implements ListenerInterface
{
    protected $securityContext;
    protected $authenticationManager;

    public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager)
    {
        $this->securityContext = $securityContext;
        $this->authenticationManager = $authenticationManager;
    }

    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();

        if (! preg_match('/^\/app_dev.php\/success/', $request->getRequestUri())) {
            return;
        }

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

        try {
            \NOA_Sso_Web::getInstance()->createSession();
        } 
        catch (Exception $e) {
            // Handle error situation here
        }

        if (isset($_SESSION['userInfo'])) {
            $token = new NoaUserToken();
            $token->setUser($_SESSION['userInfo']['email']);

            $token->mobile   = $_SESSION['userInfo']['mobileVerified'] ? $_SESSION['userInfo']['mobile'] : null;
            $token->email    = $_SESSION['userInfo']['emailVerified'] ? $_SESSION['userInfo']['email'] : null;
            $token->expires  = $_SESSION['tokenInfo']['expires'];

            try {
                $authToken = $this->authenticationManager->authenticate($token);
                $this->securityContext->setToken($authToken);
                return;
            } catch (AuthenticationException $failed) {
                // Do nothing and go for the default 403
            }
        }

        $this->securityContext->setToken(null);

        // Deny authentication with a '403 Forbidden' HTTP response
        $response = new Response();
        $response->setStatusCode(403);
        $event->setResponse($response);
    }
}

NoaFactory.php

<?php
namespace Acme\StoreBundle\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 NoaFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.noa.'.$id;
        $container
            ->setDefinition($providerId, new DefinitionDecorator('noa.security.authentication.provider'))
            ->replaceArgument(0, new Reference($userProvider))
        ;

        $listenerId = 'security.authentication.listener.noa.'.$id;
        $listener = $container->setDefinition($listenerId, new DefinitionDecorator('noa.security.authentication.listener'));

        return array($providerId, $listenerId, $defaultEntryPoint);
    }

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'noa';
    }

    public function addConfiguration(NodeDefinition $node)
    {
    }
}

DefaultController.php

<?php

namespace Acme\StoreBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function indexAction()
    {
        $token = $this->container->get('security.context')->getToken();

        // Not reached
        print '<pre>';
        print_r($token->getUser());
        print '</pre>';
        return $this->render('AcmeStoreBundle:Default:index.html.twig', array('name' => $token->getUser()->gerUsername()));
    }
    public function loginAction()
    {
        return $this->render('AcmeStoreBundle:Default:login.html.twig', array());
    }
    public function successAction()
    {
        $token = $this->container->get('security.context')->getToken();
        $this->container->get('event_dispatcher')->dispatch(
            SecurityEvents::INTERACTIVE_LOGIN,
            new InteractiveLoginEvent($this->container->get('request'), $token)
        );

        // This prints the user object
        print '<pre>';
        print_r($token->getUser());
        print '</pre>';
        return new Response('<script>//window.top.refreshPage();</script>');
    }
}

I have checked all similar questions in stackoverflow and spent around a week to solve this issue, any help is greatly appreciated.

1

1 Answers

0
votes

Rather than using $_SESSION in NoaListener, you ought to be using the session interface on the request object. Symfony does its own session management and may ignore or overwrite your session (e.g. it's common to migrate sessions upon successful login to prevent session fixation attacks).

Use $request = $event->getRequest() as you already have, then $request->getSesssion()->get('userInfo'), etc.