简体   繁体   中英

Symfony guard : access_control has no effect

I'm new to symfony, and I'm trying to get a basic guard authenticator based on JWT. The work is mainly from the article here, where I have removed any user check (for now): http://kolabdigital.com/lab-time/symfony-json-web-tokens-authentication-guard

I think there's something I don't get, because I can't make it work. More precisely, it works everywhere, even in the exceptions I put in place.

Here is the Check service, basically the same as the article, without the users management, and with a bit of logging :

<?php

namespace AppBundle\Security;

use Lexik\Bundle\JWTAuthenticationBundle\Encoder\DefaultEncoder;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\AuthorizationHeaderTokenExtractor;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Psr\Log\LoggerInterface;

class TokenAuthenticator extends AbstractGuardAuthenticator
{
    private $jwtEncoder;
    private $logger;

    public function __construct(DefaultEncoder $jwtEncoder, LoggerInterface $logger)
    {
        $this->logger = $logger;
        $this->jwtEncoder = $jwtEncoder;
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
        $route = $request->attributes->get('_route');
        $url = $request->getUri();
        $this->logger->info($route . ' : ' . $url);
        return new JsonResponse('Authentication required', 401);
    }

    public function getCredentials(Request $request)
    {

        if(!$request->headers->has('Authorization')) {
            return;
        }

        $extractor = new AuthorizationHeaderTokenExtractor(
            'Bearer',
            'Authorization'
        );

        $token = $extractor->extract($request);

        if(!$token) {
            return;
        }

        return $token;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $data = $this->jwtEncoder->decode($credentials);

        if(!$data){
            return;
        }

        $username = $data['username'];

        // TODO get user from user collection
        $user = ['username' => $username];

        // Is user is encoded in token and exists, then it's fine
        if(!$user){
            return;
        }

        return $user;
    }


    public function checkCredentials($credentials, UserInterface $user)
    {
        return true;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        return new JsonResponse([
            'message' => $exception->getMessage()
        ], 401);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        return;
    }

    public function supportsRememberMe()
    {
        return false;
    }

}

And the security.yml, with EVERYTHING excluded, just to check the behavior.

# To get started with security, check out the documentation:
# http://symfony.com/doc/current/security.html
security:

    # http://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
    providers:
        in_memory:
            memory: ~

    firewalls:
        # disables authentication for assets and the profiler, adapt it according to your needs
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

#################################
# Secured section
#

        # Custom authentication firewall for all request thats starts from /api
        api:
            pattern: ^/api
            guard:
                authenticators:
                    - app.token_authenticator


#################################
# Main Configuration
#

        main:
            anonymous: ~
            # activate different ways to authenticate

            # http_basic: ~
            # http://symfony.com/doc/current/security.html#a-configuring-how-your-users-will-authenticate

            # form_login: ~
            # http://symfony.com/doc/current/cookbook/security/form_login_setup.html



    access_control:
        #- { path: ^/auth, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        #- { path: ^/version, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        #- { path: ^/api, roles: [ROLE_USER, ROLE_API_USER] }
        - { path: ^/api, roles: IS_AUTHENTICATED_ANONYMOUSLY }

        #- { path: ^/(css|js), roles: IS_AUTHENTICATED_ANONYMOUSLY }
        #- { path: ^/(_wdt|_profiler), roles: IS_AUTHENTICATED_ANONYMOUSLY }
        #- { path: ^/, roles: ROLE_USER }

I just put the guard in place on ^/api, and put a control_access on the same path to allow ANONYMOUS. I expect that the guard service is not called on any path with that configuration, but it's called everytime. I guess I'm missing some understanding on how it works. What I understand is:

  • Access control is checked before anything else
  • If there's a matching line, it takes it (the first one)
  • If IS_AUTHENTICATED_ANONYMOUSLY is set, then the firewall is not checked
  • Else, the next check is the firewall configuration, where it tells to check with the TokenAuthenticator

The initial aim is to lock /api , except /api/auth and /api/version that can be accessed without control.

Thanks for the help, I think after 1 day and a half on it, I'm not able to think straight about it.

For the record, I managed to workaround this issue.

First, Guard Authenticator is built upon real User Repository, which is not what we want. We want a fast check with Redis, and a UserRepository in Mongo. Also, we don't want PHP sessions, we want a stateless system (only the active token is in redis).

So what I did is create a dummy User object for Guard Authenticator, implementing the needed interface.

On access, we check if the user is already known by getting its token in redis, with additional data. These additional data include the needed User object.

On connection, we actually check the user in database, and if it's fine, we create the dummy User object, and push it in redis with the token.

With that system everything is fine. It's not the prettiest solution, but it allows using Guard in a stateless environment with possibly multiple instances.

Hello i recently got the same problem. There is a lack of well documentation about this use-case when we have custom guard authenticator with acces_control.

First of all:

What I understand is:

  • Access control is checked before anything else

No, This sentence is FAIL and because of this - it projects the understanding of the rest.

As you can see for example here , the job of FIREWALL (your Token Authenticator Guard) is to start the authentication process which is the first thing the system does - to check if the given credentials (if any) will result in authenticated token or not.

If this is determined - then system will do his second job which is authorization - for example to possibly deny access to certain resources because of insufficient role using access_control.

Knowing all of this, you can think of your guard token authenticator as some kind of (i know it is not the real thing but just for purpose of clean explanation) abstract-virtual-variation of login form mechanism, which will only run when you hit your login path. Only difference is here your guard will run not when you hit some login path, but when the request have Authorization header (and this is fully customizable as you defined it like this in getCredentials function)

Btw, in newest symfony there is new supports function which is called before getCredentials in which you should check does request have sufficient headers to start Authentication (or whatever check you want).

So basically you said: "run guard when request have Authorization header" , no matter which uri you requests. Probably because you send your basic auth credentials that way - your request (even for api/version and especially for api/auth) have this header. Then your guard is triggered before anything else which result in behavior you described.

As a one of possible solutions for this - inside your guard, you could set it to trigger when request have X-AUTH-TOKEN header (so you can use different headers for public access paths and different for private). That way your call to api/version wont trigger guard, and even call for api/auth wont do it as you will be sending credentials using Authorization header, not X-AUTH-TOKEN header.

If so your guard will not be triggered and THEN with determined unauthenticated token you hit your accesss control which will decide if you are allowed to resource or not (yes for IS_AUTHENTICATED_ANONYMOUSLY for that path).

I hope i explained to you a bit.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM