skip to Main Content

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


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

    public function getCredentials(Request $request)
    {
        if (!$request->headers->has('Authorization')) {
            throw new AuthenticationException();
        }
        ...
    }
    

    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”?

    Login or Signup to reply.
  2. The above answer is somewhat true with a few corrections:

    Any exceptions thrown from within the authenticator (guard) itself will trigger onAuthenticationFailure()

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): JsonResponse
    {
        return new JsonResponse(['message' => 'Forbidden'], Response::HTTP_FORBIDDEN);
    }
    
    public function start(Request $request, AuthenticationException $authException = null): JsonResponse
    {
        return new JsonResponse(['message' => 'Authentication Required'], Response::HTTP_UNAUTHORIZED);
    }
    

    The method start() is called when, for example, you throw an AccessDeniedException 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.

    /** 
     * @Route("/something-special")
     */
    public function somethingSpecial()
    {
        if ($this->getUser()->getUsername() === 'a_banned_user') {
            throw new AccessDeniedException();
        }
    
        // ...
    }
    

    Then test it with:

    $ curl -H "Auth-Header-Name: the_auth_token" http://site.local/something-special
    {"message":"Authentication Required"}
    

    But, on the other hand, if you throw an exception due to a missing token header, then onAuthenticationFailure() will run instead:

    public function getCredentials(Request $request): array
    {
        if (!$request->headers->has('Authorization')) {
            throw new AuthenticationException('Authentication header missing.');
        }
    
        // ...
    }
    

    Then test it with (note: the AuthenticationException message is ignored in onAuthenticationFailure() and just returns a generic “Forbidden” message as you can see above):

    $ curl http://site.local/something-special
    {"message":"Forbidden"}
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search