Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to handle MissingAuthorizationCodeException in a user friendly way #24

Closed
shairyar opened this issue Apr 11, 2016 · 8 comments
Closed

Comments

@shairyar
Copy link

Hi,

In case user clicks on cancel button on facebook permission popup then on development env i see MissingAuthorizationCodeException which is fine, but i need to display a friendly message on frontend rather than seeing a blank white screen on production. How can i handle this inside the FacebookAuthenticator as it does not seem to be going inside onAuthenticationFailure method?

@weaverryan
Copy link
Member

Hi @shairyar!

Can you post your authenticator class? Does it extend SocialAuthenticator? If you use the fetchAccessToken method in your getCredentials() method, then denying access should cause the NoAuthCodeAuthenticationException exception to be thrown. This should ultimately result in a nice error message being shown :). In other words, this should not happen - so let me know what your class looks like!

@shairyar
Copy link
Author

Hi,

Yes my authenticator class extends SocialAuthenticator and this is what my class looks like

namespace AppBundle\Security;
use AppBundle\Entity\User;
use KnpU\OAuth2ClientBundle\Exception\MissingAuthorizationCodeException;
use KnpU\OAuth2ClientBundle\Security\Exception\FinishRegistrationException;
use KnpU\OAuth2ClientBundle\Security\Helper\FinishRegistrationBehavior;
use Doctrine\ORM\EntityManager;
use KnpU\OAuth2ClientBundle\Security\Helper\PreviousUrlHelper;
use KnpU\OAuth2ClientBundle\Security\Helper\SaveAuthFailureMessage;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\Facebook;
use League\OAuth2\Client\Provider\FacebookUser;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouterInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use KnpU\OAuth2ClientBundle\Client\Provider\FacebookClient;

class FacebookAuthenticator extends SocialAuthenticator
{
    use PreviousUrlHelper;
    use SaveAuthFailureMessage;
    use FinishRegistrationBehavior;

    /**
     * @var Facebook
     */
    private $facebookClient;
    /**
     * @var EntityManager
     */
    private $em;
    /**
     * @var RouterInterface
     */
    private $router;

    public function __construct(FacebookClient $facebookClient, EntityManager $em, RouterInterface $router)
    {
        $this->facebookClient = $facebookClient;
        $this->em = $em;
        $this->router = $router;
    }

    public function getCredentials(Request $request)
    {
        if ($request->getPathInfo() != '/connect/facebook-check') {
            // skip authentication unless we're on this URL!
            return null;
        }

        try {

            return $this->facebookClient->getAccessToken($request);
        } catch (IdentityProviderException $e) {
            // you could parse the response to see the problem
            throw $e;
        }
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        /** @var AccessToken $accessToken */
        $accessToken = $credentials;
        $facebookUser = $this->facebookClient->fetchUserFromToken($accessToken);

        if (!$accessToken) {
            echo "no token";
            die;
        }


        // have they logged in with Facebook before? Easy!
        $existingUser = $this->em->getRepository('AppBundle:User')
            ->findOneBy(array('facebookId' => $facebookUser->getId()));
        if ($existingUser) {
            return $existingUser;
        }


        //get user email from session
        $session = new Session();
        if (!empty($session->get('user'))) {
            $username = $session->get('user')->getUsername();
            // get logged in user data from database by matching user by email?
            $user = $this->em->getRepository('AppBundle:User')
                ->findOneBy(array('username' => $username));

            // make sure the Facebook user is set
            $user->setFacebookId($facebookUser->getId());
            $this->em->persist($user);
            $this->em->flush();

            return $user;

        } else {
            //user is trying to login via facebook
            $username = $facebookUser->getId();
            // get logged in user data from database by matching user by email?
            $user = $this->em->getRepository('AppBundle:User')
                ->findOneBy(array('facebookId' => $username));

            if ($user) {
                // make sure the Facebook user is set
                $user->setFacebookId($facebookUser->getId());
                $this->em->persist($user);
                $this->em->flush();

                return $user;
            } else {
                if (!$user) {

                    $session->getFlashBag()->add(
                        'warning',
                        'Please register before linking your Facebook account.'
                    );

                }

            }

        }
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return true;
        // do nothing - the fact that the access token worked means that
        // our app has been authorized with Facebook
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {

        $loginUrl = $this->router->generate('login');
        return new RedirectResponse($loginUrl);

    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if (!$url = $this->getPreviousUrl($request, $providerKey)) {
            $url = $this->router->generate('profile');
        }
        $session = new Session();
        if (!empty($session->get('user'))) {
            $session->getFlashBag()->add(
                'success',
                'Your Facebook account has been successfully linked.'
            );
        }
        return new RedirectResponse($url);
    }

    /**
     * Called when an anonymous user tries to access an protected page.
     *
     * In our app, this is never actually called, because there is only *one*
     * "entry_point" per firewall and in security.yml, we're using
     * app.form_login_authenticator as the entry point (so it's start() method
     * is the one that's called).
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        // not called in our app, but if it were, redirecting to the
        // login page makes sense
        $url = $this->router
            ->generate('login');

        return new RedirectResponse($url);
    }
}

@weaverryan
Copy link
Member

Hi @shairyar!

Ok! In getCredentials(), use $this->fetchAccessToken() instead of fetching the access token yourself. Here's an example: https://github.com/knpuniversity/oauth2-client-bundle#authenticating-with-guard. This may not have existed when you originally wrote this :) - but the fetchAccessToken() method should correctly handle this.

@shairyar
Copy link
Author

Hi @weaverryan,

Many thanks, that worked perfectly but I now have another issue.

The app that I am working on, I can only link social media accounts after creating an account manually. So if I am logged in already and I am at profile page of the app, i have this facebook button to link, suppose on the facebook popup permission i click on cancel it takes me to the login screen rather than my profile page.

Inside my onAuthenticationFailure I have placed a check that if user exist in a session then take him to the profile page other wise take the user to the login page, it seems that this check is not considered no matter what i do, even if i take out the redirect part completely it still takes me to the login page.

Where is this login redirect being handled?

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $session = new Session();
        $session->getFlashBag()->add(
            'warning',
            'You failed to connect with Facebook.'
        );

        if (!empty($session->get('user'))) {
            $profileUrl = $this->router->generate('profile');
            return new RedirectResponse($profileUrl);
        }

        $loginUrl = $this->router->generate('login');
        return new RedirectResponse($loginUrl);
    }

The if part is not being taken into consideration, i var_dump() the session and the session exist.

@weaverryan
Copy link
Member

Hmm, a few things:

  1. In this situation, is onAuthenticationFailure being called at all?
  2. Unless it's something specific to your application, you're checking for the user incorrectly. You should inject the security.authorization_checker service and then use isGranted('IS_AUTHENTICATED_REMEMBERED') to know if the user is logged in (it's possible injecting this will give you a circular reference: if so, inject service_container, then lazily fetch out the security.authorization_checker later at the moment you need it).

Cheers!

@shairyar
Copy link
Author

shairyar commented Apr 26, 2016

Hi,

Thanks for getting back to me. I can confirm that on clicking cancel it does go to onAuthenticationFailure. I then injected the service_container and added the following piece of code inside onAuthenticationFailure but it completely ignore it and the user is logged out :)

if($this->containerInterface->get('security.authorization_checker')->isGranted('ROLE_USER')){    

     $profileUrl = $this->router->generate('profile');
     return new RedirectResponse($profileUrl);
 }

It looks like to me before even getting to onAuthenticationFailure the user is logged out somewhere.

@weaverryan
Copy link
Member

Yes, when you fail authentication, it clears the token (i.e. logs you out). I forgot about that :).

The general problem is that - since the user is already authenticated - we don't really need to send them through the authentication process again (i.e. Guard). There are two possible solutions:

  1. Refactor your code so that if the user is already logged in, you set the redirect URL for Facebook to some normal (non Guard) controller. There, you simply make your API requests to get the user's FB information and update the user. This really has nothing to do with authentication, so it makes sense. However, you'd probably need to move some of the logic you have on your authenticator into a general service class, so that you can re-use it in this controller and in your authenticator.

  2. The only other option is to be "clever" in your authenticator. In getCredentials(), you'd wrap the fetchAccessToken in a try-catch block and catch the NoAuthCodeAuthenticationException error. Then, in that function, you'd determine if the user is logged in. If the user is not logged in, throw the NoAuthCodeAuthenticationException you just caught. If the user IS logged in, then return null to skip authentication. Ultimately, this will let the request continue to your controller. Then, in your controller (which is probably empty right now, because until now it never was executed for this URL), you'll redirect the user to their profile page.

(2) is a little ugly, but a little easier. (1) is the proper solution :)

Cheers!

@shairyar
Copy link
Author

Many thanks @weaverryan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants