4

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:
        FOS\UserBundle\Model\UserInterface: 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:
                    - AppBundle\Security\FacebookAuthenticator

        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 AppBundle\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Security\Core\User\UserProviderInterface;

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;
    }
}
Stephan Vierkant
  • 9,674
  • 8
  • 61
  • 97
Nathan Meyer
  • 312
  • 7
  • 18
  • Does it work with correct access_token in headers? And you only complaining about cases when you submit wrong/missing header? Could you also add a CURL request command, to which application replies with this error? So that we see request URL, headers, etc. – Maksym Moskvychev Nov 14 '17 at 15:53
  • 1
    @MaksymMoskvychev Thank you for your answer, I just edited the thread – Nathan Meyer Nov 14 '17 at 16:03

2 Answers2

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

Maksym Moskvychev
  • 1,471
  • 8
  • 11
  • Ok I understand now for the onAuthenticationFailure () method. Regarding your question, I thought the checkCredentials () method allowed me to do that, right? – Nathan Meyer Nov 14 '17 at 16:59
  • checkCredentials() is called after you already found User by API key. In case you had username+password - you could check username in getUser() and check password in checkCredentials(). But in you case you have only one field - API key. So, you check it in getUser() only. checkCredentials() also might be used in case you already has User in session. But in you case firewall is stateless, so no sessions. – Maksym Moskvychev Nov 14 '17 at 17:04
1

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"}
Yes Barry
  • 9,514
  • 5
  • 50
  • 69