Search code examples
silexsymfony-security

Check if the user is authenticated on not secured route


I'm trying to build a simple login using symofny/security package in Silex, but I have a small problem with authentication checking.

The structure:

/
/login
/admin
    /login_check
    /logout

In order to get to the /admin route user needs to be authenticated, if he's not, he gets redirected to /login form, which then, as recommended, is asking admin/login_check for authentication (that's all provided by symofny's security firewalls).

The firewalls configuration:

'security.firewalls' => array(
    'login' => array(
        'pattern' => '^/login$',
    ),
    'admin' => array(
        'pattern' => '^/admin',
        'http' => true,
        'form' => array(
            'login_path' => '/login',
            'check_path' => '/admin/login'
        ),
        'logout' => array(
            'logout_path' => '/admin/logout',
            'invalidate_session' => true
        ),
        'users' => ...
    ),
)

Everything works fine, but the user can enter the /login route even-though he's already authenticated, which is not that bad, but I'd like to avoid it. I tired to check the user authentication status, but I guess it does not work as I'm checking it on /login controller, which is not in "secured" area of the website.

Here's the code:

public function index(Request $request, Application $app)
{
    if ($app['security.authorization_checker']->isGranted('ROLE_ADMIN')) {
        return $app->redirect($app['url_generator']->generate('admin'));
    }
    return $app['twig']->render('login/index.twig', array(
        'error'         => $app['security.last_error']($request),
        'last_username' => $app['session']->get('_security.last_username'),
    ));
}

That throws an error: The token storage contains no authentication token. One possible reason may be that there is no firewall configured for this URL. So, the question, is there any way to do this using symfony/security natively (without any walk-arounds)? or is it somehow possible to create the /login route inside secured area with possibility to access it even-though the user is not logged in (e.g. make an exception for GET request) which would solve the problem?

UPDATE

I've also added the firewall configuration for /login page with an option anonymous: true, which resulted in not throwing an error anymore, but yet, when I'm logged in on the /admin route, method isGranted('ROLE_ADMIN') results in true, whereas on /login route it results in false (I'm still logged in there).


Solution

  • You can easily understand the behavior of the security component by dumping the current token.

    public function index(Request $request, Application $app)
    {
        var_dump($application['security.token_storage']->getToken());
    }
    

    When you don't set anonymous option for login page (by default it is false):

    null
    

    When you set anonymous option for login page to true:

    object(Symfony\Component\Security\Core\Authentication\Token\AnonymousToken)
        private 'secret' => string 'login' (length=5)
        private 'user' (Symfony\Component\Security\Core\Authentication\Token\AbstractToken) => string 'anon.' (length=5)
        private 'roles' (Symfony\Component\Security\Core\Authentication\Token\AbstractToken) => 
            array (size=0)
                empty
        private 'authenticated' (Symfony\Component\Security\Core\Authentication\Token\AbstractToken) => boolean true
        private 'attributes' (Symfony\Component\Security\Core\Authentication\Token\AbstractToken) => 
            array (size=0)
               empty
    

    Following example describes why you are getting error in your initial code example.

    How to share security token?

    To share security token between multiple firewalls you have to set the same Firewall Context.

    SecurityServiceProvider registers context listener for protected firewall you have declared with pattern security.context_listener.<name>. In your example it is registered as security.context_listener.admin. Thus context you want to use is named admin.

    Also a firewall context key is stored in session, so every firewall using it must set its stateless option to false.

    What is more, to invoke authentication on login page the anonymous option must be set to true

    'security.firewalls' => array(
        'login' => array(
            'context' => 'admin',
            'stateless' => false,
            'anonymous' => true,
            'pattern' => '^/login$',
        ),
        'admin' => array(
            'context' => 'admin',
            'stateless' => false,
            'pattern' => '^/admin',
            'http' => true,
            'form' => array(
                'login_path' => '/login',
                'check_path' => '/admin/login'
            ),
            'logout' => array(
                'logout_path' => '/admin/logout',
                'invalidate_session' => true
            ),
            'users' => ...
        ),
    )
    

    After the change, if you are logged in on admin panel and login page you will get an instance of Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken as a token and Symfony\Component\Security\Core\Authentication\Token\AnonymousToken if you are not logged in.

    Because of that, you can safely check permission using $app['security.authorization_checker']->isGranted('ROLE_ADMIN') and get the same results on both firewalls.