Search code examples
symfonyauthenticationoauth-2.0api-key

Combining fos_oauth authenticator with single key authenticator


I set up two authentication methods for my api:

Both works like a charm separately.

I tried to combine those systems with this security config:

security:
    encoders:
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
        ROLE_ALLOWED_TO_SWITCH: ~
        ROLE_SUPPORT:           ~
        ROLE_ADMIN:             [ROLE_SONATA_ADMIN]
        ROLE_SUPER_ADMIN:       [ROLE_ADMIN, ROLE_SUPPORT, ROLE_ALLOWED_TO_SWITCH]

    providers:
        fos_userbundle:
            id: fos_user.user_provider.username_email
        api_key_user:
            id: security.user.provider.api_key

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

        oauth_token:
            pattern:    ^/oauth/v2/token
            security:   false

        api:
            pattern: ^/api
            stateless: true
            simple_preauth:
                authenticator: security.authentication.authenticator.api_key
            fos_oauth: true

        main:
            pattern: ^/
            form_login:
                provider: fos_userbundle
                csrf_provider: form.csrf_provider
                login_path: /login
                check_path: /login_check

            anonymous: ~
            logout:
                path: /logout
            switch_user: true

If I try to get an access token with this curl command:

curl "http://localhost:8000/oauth/v2/token?client_id=1_2rqa1al0trwgso8g8co4swsks48cwsckgc8cokswkcgos4csog&client_secret=25a78plm6c2ss044k4skckkwoo8kw4kcoccg8sg0skook4sgwg&grant_type=password&username=test&password=test

It works an I get an access_token, but when I try to use it:

curl -X GET http://localhost:8000/api/changelogs.json -H "Authorization: Bearer MmI2OWNkNjhjMGYwOTUyNDA2OTdlMDBjNjA1YmI3MjVhNTBiMTNhMjI0MGE1YmM3NzgwNjVmZWZmYWNhM2E4YQ" | json_pp

I get:

{
   "error" : "invalid_grant",
   "error_description" : "The provided access token is invalid."
}

By deactivating my single_preauth api key authenticator, it's works and I can access to my API.

It seems my api key authenticator block all another system.

Here, my ApiKeyAuthenticator class:

class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
    private $userProvider;

    /**
     * @param ApiKeyUserProvider $userProvider
     */
    public function __construct(ApiKeyUserProvider $userProvider)
    {
        $this->userProvider = $userProvider;
    }

    /**
     * {@inheritdoc}
     */
    public function createToken(Request $request, $providerKey)
    {
        $apiKey = str_replace('Bearer ', '', $request->headers->get('Authorization', ''));

        if (!$apiKey) {
            throw new BadCredentialsException('No API key given.');
        }

        return new PreAuthenticatedToken('anon.', $apiKey, $providerKey);
    }

    /**
     * {@inheritdoc}
     */
    public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
    {
        $apiKey = $token->getCredentials();
        $username = $this->userProvider->getUsernameForApiKey($apiKey);

        if (!$username) {
            throw new AuthenticationException('The provided access token is invalid.');
        }

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

        return new PreAuthenticatedToken(
            $user,
            $apiKey,
            $providerKey,
            $user->getRoles()
        );
    }

    /**
     * {@inheritdoc}
     */
    public function supportsToken(TokenInterface $token, $providerKey)
    {
        return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
    }

    /**
     * {@inheritdoc}
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        return new JsonResponse([
            'error'             => 'invalid_grant',
            'error_description' => $exception->getMessage()
        ], 401);
    }
}

But I can't find why.

How to combine this two authenticator methods?

Thanks for help.


Solution

  • Finaly found how to handle it but not sure it's the better and proper way.

    Don't hesitate to suggest improvements on comments! ;)

    First of all, remove fos_oauth key from security.yml file. It should looks like:

    security:
        firewalls:
            # [...]
            api:
                pattern: ^/api
                stateless: true
                # This will handle both oauth access token and simple private token
                simple_preauth:
                    authenticator: security.authentication.authenticator.api_key
            # [...]
    

    On ApiKeyUserProvider::getUsernameForApiKey method, you will search on both custom api key manager and OAuth access token manager.

    The complete class should look like this.

    class ApiKeyUserProvider implements UserProviderInterface
    {
        /**
         * @var UserManagerInterface
         */
        private $userManager;
    
        /**
         * @var ApiKeyManager
         */
        private $apiKeyManager;
    
        /**
         * @var AccessTokenManagerInterface
         */
        private $accessTokenManager;
    
        /**
         * @param UserManagerInterface        $userManager
         * @param ApiKeyManager               $apiKeyManager
         * @param AccessTokenManagerInterface $accessTokenManager
         */
        public function __construct(UserManagerInterface $userManager, ApiKeyManager $apiKeyManager, AccessTokenManagerInterface $accessTokenManager)
        {
            $this->userManager = $userManager;
            $this->apiKeyManager = $apiKeyManager;
            $this->accessTokenManager = $accessTokenManager;
        }
    
        /**
         * @param string $apiKey
         *
         * @return string|null
         */
        public function getUsernameForApiKey($apiKey)
        {
            // FOSOAuth system
            $token = $this->accessTokenManager->findTokenByToken($apiKey);
            if ($token) {
                return $token->getUser()->getUsername();
            }
    
            // Private key system
            return $this->apiKeyManager->getUsernameForToken($apiKey);
        }
    
        /**
         * {@inheritdoc}
         */
        public function loadUserByUsername($username)
        {
            return $this->userManager->findUserByUsername($username);
        }
    
        /**
         * {@inheritdoc}
         */
        public function refreshUser(UserInterface $user)
        {
            throw new UnsupportedUserException();
        }
    
        /**
         * {@inheritdoc}
         */
        public function supportsClass($class)
        {
            return 'FOS\UserBundle\Model\User' === $class;
        }
    }
    

    And voila! Both Private and OAuth token are correctly managed.