I know there are a lot of threads on this subject, but none of them helped me…
I am working on an application on which you have to send an access token in the headers for each request.
I manage this security with Guard.
For my tests, when I send a false token, or when I do not send it, the start() or onAuthenticationFailure() method must be called as appropriate.
But it does not work. I have the same error every time.
Looks like these methods are never called.
No authorization sent
GET /BileMo/web/app_dev.php/api/products/2 HTTP/1.1
Host: localhost:8888
Content-Type: application/json
{
"message": "Username could not be found."
}
Invalid access token
GET /BileMo/web/app_dev.php/api/products/2 HTTP/1.1
Host: localhost:8888
Content-Type: application/json
Authorization: *Fake Facebook Token*
{
"message": "Username could not be found."
}
instead of:
{
"message": "Authorization required"
}
or
{
"message": "The facebook access token is wrong!"
}
With a correct access token, requests are returned to the user correctly.
Example of a request :
GET /BileMo/web/app_dev.php/api/products/2 HTTP/1.1
Host: localhost:8888
Content-Type: application/json
Authorization: *Facebook Token*
Here are the important parts of my code:
security.yml
security:
encoders:
FOSUserBundleModelUserInterface: sha512
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_USER
providers:
fos_userbundle:
id: fos_user.user_provider.username_email
api_key_user_provider:
entity:
class: FacebookTokenBundle:User
property: facebook_access_token
firewalls:
api:
pattern: ^/api
stateless: true
anonymous: false
guard:
authenticators:
- AppBundleSecurityFacebookAuthenticator
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
form_login:
provider: fos_userbundle
csrf_token_generator: security.csrf.token_manager
login_path: /login
check_path: /login_check
oauth:
resource_owners:
facebook: "/login/check-facebook"
login_path: /login
failure_path: /login
oauth_user_provider:
#this is my custom user provider, created from FOSUBUserProvider - will manage the
#automatic user registration on your site, with data from the provider (facebook. google, etc.)
service: my_user_provider
logout: true
anonymous: true
login:
pattern: ^/login$
security: false
remember_me:
secret: "%secret%"
lifetime: 31536000 # 365 days in seconds
path: /
domain: ~ # Defaults to the current domain from $_SERVER
access_control:
- { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin/, role: ROLE_ADMIN }
- { path: ^/api, role: ROLE_USER }
FacebookAuthenticator.php
namespace AppBundleSecurity;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationJsonResponse;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSecurityGuardAbstractGuardAuthenticator;
use SymfonyComponentSecurityCoreAuthenticationTokenTokenInterface;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;
use HWIBundleOAuthBundleOAuthResponseUserResponseInterface;
use DoctrineORMEntityManager;
use SymfonyComponentSecurityCoreUserUserProviderInterface;
class FacebookAuthenticator extends AbstractGuardAuthenticator
{
public function __construct(EntityManager $em)
{
$this->em = $em;
}
/**
* Called when authentication is needed, but it's not sent
*/
public function start(Request $request, AuthenticationException $authException = null)
{
$data = array(
// you might translate this message
'message' => 'Authentication Required'
);
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
/**
* Called on every request. Return whatever credentials you want to
* be passed to getUser(). Returning null will cause this authenticator
* to be skipped.
*/
public function getCredentials(Request $request)
{
if (!$token = $request->headers->get('Authorization')) {
// No token?
$token = null;
}
// What you return here will be passed to getUser() as $credentials
return array(
'token' => $token,
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
$user = $this->em->getRepository('FacebookTokenBundle:User')
->findOneBy(array('facebook_access_token' => $credentials));
return $user;
}
public function checkCredentials($credentials, UserInterface $user)
{
if ($user->getFacebookAccessToken() === $credentials['token']) {
return true;
}
return new JsonResponse(array('message' => 'The facebook access token is wrong!', Response::HTTP_FORBIDDEN));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
$data = array(
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
);
return new JsonResponse($data, Response::HTTP_FORBIDDEN);
}
public function supportsRememberMe()
{
return false;
}
}
2
Answers
This behaviour is expected. AbstractGuardAuthenticator has too generic interface, and you need to tailor it to your needs, if you want.
For example, to have error “Authorization required” – you may throw AuthenticationException inside getCredentials() methods. Exception will be catch in symfony core and method start() will be called finally.
Method onAuthenticationFailure() is usually used to redirect user to login page in case of wrong credentials. In case of API key in header this functionality is not needed. Also in current implementation how to separate, when “API key not correct” and when “user is not found”?
The above answer is somewhat true with a few corrections:
Any exceptions thrown from within the authenticator (guard) itself will trigger
onAuthenticationFailure()
The method
start()
is called when, for example, you throw anAccessDeniedException
from within your app like in the controller. Maybe a good use case for that would be, say, you want to blacklist one specific user for one specific route and you don’t want to fill up your guard authenticator with unneeded bloat.Then test it with:
But, on the other hand, if you throw an exception due to a missing token header, then
onAuthenticationFailure()
will run instead:Then test it with (note: the AuthenticationException message is ignored in onAuthenticationFailure() and just returns a generic “Forbidden” message as you can see above):