Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony - Authentication with an API Token - Request token user is null

For the record, I'm using PHP 7.0.0, in a Vagrant Box, with PHPStorm. Oh, and Symfony 3.

I'm following the API Key Authentication documentation. My goal is:

  • To allow the user to provide a key as a GET apiKey parameter to authenticate for any route, except the developer profiler etc obviously
  • To allow the developer to write $request->getUser() in a controller to get the currently logged in user

My problem is that, although I believe I've followed the documentation to the letter, I'm still getting a null for $request->getUser() in the controller.

Note: I've removed error checking to keep the code short

ApiKeyAuthenticator.php

The thing that processes the part of the request to grab the API key from it. It can be a header or anything, but I'm sticking with apiKey from GET.

Differences from documentation, pretty much 0 apart from that I'm trying to keep the user authenticated in the session following this part of the docs.

class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
    public function createToken(Request $request, $providerKey)
    {
        $apiKey = $request->query->get('apiKey');

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

    public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
    {
        $apiKey = $token->getCredentials();
        $username = $userProvider->getUsernameForApiKey($apiKey);

        // The part where we try and keep the user in the session!
        $user = $token->getUser();
        if ($user instanceof ApiKeyUser) {
            return new PreAuthenticatedToken(
                $user,
                $apiKey,
                $providerKey,
                $user->getRoles()
            );
        }


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

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

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

ApiKeyUserProvider.php

The custom user provider to load a user object from wherever it can be loaded from - I'm sticking with the default DB implementation.

Differences: only the fact that I have to inject the repository into the constructor to make calls to the DB, as the docs allude to but don't show, and also returning $user in refreshUser().

class ApiKeyUserProvider implements UserProviderInterface
{
    protected $repo;

    // I'm injecting the Repo here (docs don't help with this)
    public function __construct(UserRepository $repo)
    {
        $this->repo = $repo;
    }

    public function getUsernameForApiKey($apiKey)
    {
        $data = $this->repo->findUsernameByApiKey($apiKey);

        $username = (!is_null($data)) ? $data->getUsername() : null;

        return $username;
    }

    public function loadUserByUsername($username)
    {
        return $this->repo->findOneBy(['username' => $username]);
    }

    public function refreshUser(UserInterface $user)
    {
        // docs state to return here if we don't want stateless
        return $user;
    }

    public function supportsClass($class)
    {
        return 'Symfony\Component\Security\Core\User\User' === $class;
    }
}

ApiKeyUser.php

This is my custom user object.

The only difference I have here is that it contains doctrine annotations (removed for your sanity) and a custom field for the token. Also, I removed \Serializable as it didn't seem to be doing anything and apparently Symfony only needs the $id value to recreate the user which it can do itself.

class ApiKeyUser implements UserInterface
{
    private $id;
    private $username;
    private $password;
    private $email;
    private $salt;
    private $apiKey;
    private $isActive;

    public function __construct($username, $password, $salt, $apiKey, $isActive = true)
    {
        $this->username = $username;
        $this->password = $password;
        $this->salt = $salt;
        $this->apiKey = $apiKey;
        $this->isActive = $isActive;
    }

    //-- SNIP getters --//
}

security.yml

# Here is my custom user provider class from above
providers:
    api_key_user_provider:
        id: api_key_user_provider

firewalls:
    # Authentication disabled for dev (default settings)
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false
    # My new settings, with stateless set to false
    secured_area:
        pattern: ^/
        stateless: false
        simple_preauth:
            authenticator: apikey_authenticator
        provider:
            api_key_user_provider

services.yml

Obviously I need to be able to inject the repository into the provider.

api_key_user_repository:
    class: Doctrine\ORM\EntityRepository
    factory: ["@doctrine.orm.entity_manager", getRepository]
    arguments: [AppBundle\Security\ApiKeyUser]

api_key_user_provider:
    class:  AppBundle\Security\ApiKeyUserProvider
    factory_service: doctrine.orm.default_entity_manager
    factory_method: getRepository
    arguments: ["@api_key_user_repository"]

apikey_authenticator:
    class: AppBundle\Security\ApiKeyAuthenticator
    public: false

Debugging. It's interesting to note that, in ApiKeyAuthenticator.php, the call to $user = $token->getUser(); in authenticateToken() always shows an anon. user, so it's clearly not being stored in the session.

Debug 1 Debug 2

Also note how at the bottom of the authenticator we do actually return a new PreAuthenticatedToken with a user found from the database:

Debug 3 Debug 4

So it's clearly found me and is returning what it's supposed to here, but the user call in the controller returns null. What am I doing wrong? Is it a failure to serialise into the session because of my custom user or something? I tried setting all the user properties to public as somewhere in the documentation suggested but that made no difference.

like image 259
Jimbo Avatar asked Feb 18 '16 11:02

Jimbo


1 Answers

So it turns out that calling $request->getUser() in the controller doesn't actually return the currently authenticated user as I would have expected it to. This would make the most sense for this object API imho.

If you actually look at the code for Request::getUser(), it looks like this:

/**
 * Returns the user.
 *
 * @return string|null
 */
public function getUser()
{
    return $this->headers->get('PHP_AUTH_USER');
}

That's for HTTP Basic Auth! In order to get the currently logged in user, you need to do this every single time:

$this->get('security.token_storage')->getToken()->getUser();

This does, indeed, give me the currently logged in user. Hopefully the question above shows how to authenticate successfully by API token anyway.

Alternatively, don't call $this->get() as it's a service locator. Decouple yourself from the controller and inject the token service instead to get the token and user from it.

like image 133
Jimbo Avatar answered Oct 22 '22 14:10

Jimbo