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.
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.