knpuniversity / oauth2-client-bundle

Easily talk to an OAuth2 server for social functionality in Symfony
https://symfonycasts.com
MIT License
777 stars 146 forks source link

Authenticated NO #23

Closed shairyar closed 8 years ago

shairyar commented 8 years ago

Hi,

I am having a weird problem when I register the first time ever and then i fill up the registration password fields i get logged it but the profiler says "Authenticated NO" but if i log out and log back in using the same facebook account this time the Authenticated is YES.

There is one thing I changed to integrated it in current system

  1. We don't need username and email for authentication, we only have email address that we authenticate against, so instead of creating email property in entity we are using username property to save email and then i made the necessary changes required inside the controller to save email in username.

I am having hard time finding out why the first time Authenticated is No and then its Yes once i log out and log back in.

rwitchell commented 8 years ago

Hi Shairyar, You'll need to provide some more info if you want help.

1) Can you explain your configuration a bit more? 2) Can you post some of the code which is relevant to the user? 3) What bundles you are using? 4) Have you looked at the user in the database to confirm data is correct?

Off the top of my head: i would say you are using the FOSUserBundle - correct? I too don't want a username and email. I fixed this by doing the following in my entity\user.php

     /**
     * @param string $username
     *
     * @return $this
     */
    public function setUsername($username)
    {
        parent::setUsername($this->email);

        return $this;
    }

To solve the authenticated NO issue, i would say there could be an issue with your config or your authentication or your controller - hard to tell without code.

shairyar commented 8 years ago

Hi @rwitchell,

Thanks for getting back, I am not using any 3rd party bundle other than oauth2 bundle. I am only having trouble first time when i register, once the user is registered and come through facebook again using the same user things work fine.

Here is my security.yml

security:

    encoders:
        # Our user class and the algorithm we'll use to encode passwords
        # http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
        AppBundle\Entity\User: bcrypt

    providers:
        # Simple example of loading users via Doctrine
        # To load users from somewhere else: http://symfony.com/doc/current/cookbook/security/custom_provider.html
        database_users:
            entity: { class: AppBundle:User, property: username }

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt|error)|css|images|js)/
            security: false

        main:
            anonymous: ~
            logout: ~
            guard:
                authenticators:
                    - app.form_login_authenticator
                    - app.facebook_authenticator
                    - app.linkedin_authenticator
                # by default, use the start() function from FormLoginAuthenticator
                entry_point: app.form_login_authenticator
    access_control:
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/, roles: ROLE_ADMIN }
        - { path: ^/candidate, roles: ROLE_JOBSEEKER }

Here is my FacebookConnectController

<?php

namespace AppBundle\Controller;

use AppBundle\Entity\JobSeekerProfile;
use AppBundle\Entity\User;
use AppBundle\Form\JobSeekerRegistrationType;
use League\OAuth2\Client\Provider\FacebookUser;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class FacebookConnectController extends Controller
{
    /**
     * @Route("/connect/facebook", name="connect_facebook")
     */
    public function connectFacebookAction(Request $request)
    {
        // redirect to Facebook
        $facebookClient = $this->get('knpu.oauth2.registry')
            ->getClient('my_facebook_client');

        return $facebookClient->redirect();
    }

    /**
     * @Route("/connect/facebook-check", name="connect_facebook_check")
     */
    public function connectFacebookActionCheck()
    {
        // will not be reached!
    }

    /**
     * @Route("/connect/facebook/registration", name="connect_facebook_registration")
     */
    public function finishRegistration(Request $request)
    {
        /** @var FacebookUser $facebookUser */
        $facebookUser = $this->get('app.facebook_authenticator')
            ->getUserInfoFromSession($request);

        if (!$facebookUser) {
            throw $this->createNotFoundException('How did you get here without user information!?');
        }
        $user = new User();
        $user->setFacebookId($facebookUser->getId());
        $user->setUsername($facebookUser->getEmail());
        $user->setIsActive('1');
        $user->setRoles(array('ROLE_JOBSEEKER'));

        $JS = new JobSeekerProfile();
        $JS->setFirstName($facebookUser->getFirstName());
        $JS->setLastName($facebookUser->getLastName());
        $JS->setGender($facebookUser->getGender());
        $JS->setCountry($facebookUser->getHometown());
        $user->setJobSeekerProfile($JS);
        $form = $this->createForm(JobSeekerRegistrationType::class, $user);

        $form->handleRequest($request);
        if ($form->isValid()) {
            // encode the password manually
            $plainPassword = $form['plainPassword']->getData();
            $encodedPassword = $this->get('security.password_encoder')
                ->encodePassword($user, $plainPassword);
            $user->setPassword($encodedPassword);

            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->persist($JS);
            $em->flush();

            // remove the session information
            $request->getSession()->remove('facebook_user');

            // log the user in manually
            $guardHandler = $this->container->get('security.authentication.guard_handler');
            return $guardHandler->authenticateUserAndHandleSuccess(
                $user,
                $request,
                $this->container->get('app.facebook_authenticator'),
                'main' // the firewall key
            );
        }

        return $this->render('AppBundle::registration.html.twig', array(
            'form' => $form->createView()
        ));
    }
}

Here is my User Entity

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface, AdvancedUserInterface
{

    private $id;
    private $username;
    private $password;    
    private $roles = array();
    private $isActive;
    private $apiToken;
    private $lastLoginTime;   
    private $facebookId;
    private $linkedinId;

    /**
     * @var \AppBundle\Entity\JobSeekerProfile
     */
    private $jobSeekerProfile;

    public function __construct()
    {
        $this->apiToken = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
    }

    public function getId()
    {
        return $this->id;
    }

    public function getPassword()
    {
        return $this->password;
    }
    public function setPassword($password)
    {
        $this->password = $password;
    }

    /**
     * Returns the roles or permissions granted to the user for security.
     */
    public function getRoles()
    {
        $roles = $this->roles;

        // guarantees that a user always has at least one role for security
        if (empty($roles)) {
            $roles[] = 'ROLE_JOBSEEKER';
        }

        return array_unique($roles);

    }

    public function setRoles($roles)
    {
        $this->roles = $roles;
    }

    /**
     * Returns the salt that was originally used to encode the password.
     */
    public function getSalt()
    {
        return;
    }

    public function getUsername()
    {
        return $this->username;
    }
    /**
     * Set username
     *
     * @param string $username
     *
     * @return User
     */
    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }
    /**
     * Removes sensitive data from the user.
     */
    public function eraseCredentials()
    {
        // if you had a plainPassword property, you'd nullify it here
        // $this->plainPassword = null;
    }

    /**
     * @param string $apiToken
     */
    public function setApiToken($apiToken)
    {
        $this->apiToken = $apiToken;
    }

    public function setLastLoginTime(\DateTime $lastLoginTime)
    {
        $this->lastLoginTime = $lastLoginTime;
    }

    public function getFacebookId()
    {
        return $this->facebookId;
    }

    public function setFacebookId($facebookId)
    {
        $this->facebookId = $facebookId;
    }

    /**
     * Get apiToken
     *
     * @return string
     */
    public function getApiToken()
    {
        return $this->apiToken;
    }

    /**
     * Get lastLoginTime
     *
     * @return \DateTime
     */
    public function getLastLoginTime()
    {
        return $this->lastLoginTime;
    }

    /**
     * Set linkedinId
     *
     * @param string $linkedinId
     *
     * @return User
     */
    public function setLinkedinId($linkedinId)
    {
        $this->linkedinId = $linkedinId;

        return $this;
    }

    /**
     * Get linkedinId
     *
     * @return string
     */
    public function getLinkedinId()
    {
        return $this->linkedinId;
    }

    public function isAccountNonExpired()
    {
        return true;
    }

    public function isAccountNonLocked()
    {
        return true;
    }

    public function isCredentialsNonExpired()
    {
        return true;
    }

    public function isEnabled()
    {
        return $this->isActive;
    }

    public function setIsActive($isActive)
    {
        $this->isActive = $isActive;

        return $this;
    }

    public function getIsActive()
    {
        return $this->isActive;
    }

    public function setJobSeekerProfile(\AppBundle\Entity\JobSeekerProfile $jobSeekerProfile = null)
    {
        $this->jobSeekerProfile = $jobSeekerProfile;
        $jobSeekerProfile->setJobSeekerUser($this);
        return $this;
    }

    public function getJobSeekerProfile()
    {
        return $this->jobSeekerProfile;
    }

}
weaverryan commented 8 years ago

Hmm, this is tricky. The biggest reason for the "Authenticated No" thing is that your user has no roles - in fact it's the only way that I'm aware of for this to happen. When you see "Authenticated No" - what roles does it show in the web debug toolbar / profiler? Your User class looks correct - it looks like it always returns at least one role, but I still think this is the issue somehow.

shairyar commented 8 years ago

The role is see is ROLE_JOBSEEKER..

shairyar commented 8 years ago

I think problem occurs when using AdvancedUserInterface if I get rid of AdvancedUserInterface from my User entity things start working fine, i am able to register the first time it self and Authenticated is Yes but the moment I put back AdvancedUserInterface the registration process breaks down and i am not authenticated the first time. At the moment all i have in the methods of AdvancedUserInterface is as following.

public function isAccountNonExpired()
    {
        return true;
    }

    public function isAccountNonLocked()
    {
        return true;
    }

    public function isCredentialsNonExpired()
    {
        return true;
    }

    public function isEnabled()
    {
        return $this->isActive;
    }
weaverryan commented 8 years ago

Ah, nice detective work! If you simply return true from isEnabled, does that fix it (I imagine it does)? Also, can you double-check that isActive is true after you finish registration (right before you make the Guard call). I'm sure it is - but trying to find the issue :). Finally, does your User class implement the SerializableInterface? And if so, is the isActive field included in the serialized data (it should be).

There are some pitfalls - especially when using AdvancedUserInterface - it may be one of those, or may be Guard related - I'm not sure yet :).

Thanks!

shairyar commented 8 years ago

Hi,

Ah, nice detective work! If you simply return true from isEnabled, does that fix it (I imagine it does)?

Yes if I hardcode this inside the Entity it does fix the problem.

    public function isEnabled()
    {
        return true;
    }

Also, can you double-check that isActive is true after you finish registration (right before you make the Guard call).

I am already doing this as I am saving the isActive value inside database

Finally, does your User class implement the SerializableInterface? And if so, is the isActive field included in the serialized data (it should be).

No I am not using SerializableInterface

weaverryan commented 8 years ago

@shairyar if you're able to post a small project with this issue, I can debug further. Unfortunately, I can't seem to repeat this locally, even if I use AdvancedUserInterface. Typically, this occurs because after you login, on the next request, this hasChangedMethod on your token returns false. In your case, somehow, the isEnabled return value is changing - something related to how the User object is being serialized :/. It still may be a legitimate issue, but if it is, it's subtle.

shairyar commented 8 years ago

I think we can close this issue for now, i will investigate more and create the small project if this issue resurface again. Many thanks for all the help.