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:
apiKey
parameter to authenticate for any route, except the developer profiler etc obviously$request->getUser()
in a controller to get the currently logged in userMy 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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With