-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Description
Description
Today I tried to convert my old simple_preauth
authenticator to use the new Guard system for API key authentication.
While implementing my new Guard authenticator, I ran into a problem: I need access to the credentials (i.e. the API key) in AuthenticatorInterface#createAuthenticatedToken()
. However, createAuthenticatedToken()
only has the $user
and a $providerKey
as parameters.
Ideally, the credentials would be passed to createAuthenticatedToken()
just like they are passed to getUser()
or checkCredentials()
, but I understand that might not be possible while keeping perfect BC. Perhaps there is another nice way to do this?
My use case is the following: I have a App\Entity\User
entity, implementing UserInterface
. A User
can have multiple ApiKey
s that the user can use to authenticate with the API. An ApiKey
has an accessLevel
, which can be either read
or write
. Depending on the accessLevel
, my authenticator would grant different roles which could then be used for access control.
With my old simple_preauth
authenticator, I had the following:
<?php
// ...
class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface
{
// ...
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!($userProvider instanceof ApiKeyUserProvider))
throw new \InvalidArgumentException(sprintf('The user provider must be an instance of ApiKeyUserProvider (%s given).', get_class($userProvider)));
// Check if the API key is valid
$key = $token->getCredentials();
$apiKey = $userProvider->getApiKey($key);
if ($apiKey === null)
throw new AuthenticationException(sprintf('API key "%s" is invalid.', $key));
// Remember when this API key was last used
$apiKey->setLastUsed(new \DateTime());
$this->doctrine->getManagerForClass(ApiKey::class)->flush();
// Load the user that owns this API key
$username = $apiKey->getUser()->getUsername();
$user = $userProvider->loadUserByUsername($username);
$roles = $user->getRoles();
// Grant additional roles depending on the API key access level
if ($apiKey->getAccessLevel() === ApiKey::ACCESS_LEVEL_WRITE)
$roles[] = 'ROLE_API_WRITE';
elseif ($apiKey->getAccessLevel() === ApiKey::ACCESS_LEVEL_READ)
$roles[] = 'ROLE_API_READ';
return new ApiKeyToken($user, $key, $providerKey, $roles, $apiKey);
}
// ...
}
As you can see, I can grant different roles depending on the accessLevel
of the ApiKey
. This makes it easy to secure the API controllers; simply require ROLE_API_READ
for read-only methods, or ROLE_API_WRITE
for methods that need write access.
I can also store the ApiKey
in the authentication token, which allows me easy access to the ApiKey
in my controllers, which allows me to log which API key was used to perform a certain action.
I can't do either of these with Guard, because in createAuthenticatedToken()
, where the token should be created, I don't have access to the ApiKey
any more. I also don't have access to the request (where I originally extract the API key from, in getCredentials()
).
Example
This is what I imagine it to look like:
class ApiKeyAuthenticator implements AuthenticatorInterface
{
// ...
public function createAuthenticatedToken(UserInterface $user, $providerKey, $credentials)
{
$apiKey = $this->em->getRepository(ApiKey::class)->findOneByKey($credentials);
// Remember the last usage date
$apiKey->setLastUsed(new \DateTime());
$this->em->flush();
// Grant additional roles depending on the API key access level
$roles = $user->getRoles();
if ($apiKey->getAccessLevel() === ApiKey::ACCESS_LEVEL_WRITE)
$roles[] = 'ROLE_API_WRITE';
elseif ($apiKey->getAccessLevel() === ApiKey::ACCESS_LEVEL_READ)
$roles[] = 'ROLE_API_READ';
return new ApiKeyToken($user, $credentials, $providerKey, $roles, $apiKey);
}
// ...
}
The extra $credentials
parameter allows me to fetch the ApiKey
from the DB, grant additional roles, and store the ApiKey
as part of the token.