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
apiKeyparameter 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
apiKeyfrom 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.
Also note how at the bottom of the authenticator we do actually return a new PreAuthenticatedToken with a user found from the database:
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.




access_control: - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }securityinsecurity.yml. No change.publicandprotectedaccess modifiers and the user was stillnull.