hipsterjazzbo / LaraParse

LaraParse provides a nice integration for using Parse (parse.com) with Laravel 5+
MIT License
27 stars 19 forks source link

Facebook Login #12

Closed iamraffe closed 9 years ago

iamraffe commented 9 years ago

Has anyone got it working with Facebook Login/Registration?

That being with Socialite or any of the other alternatives available.

nicklee1990 commented 9 years ago

I haven't tried this yet, it's on my backlog for my current project so I'll hopefully get to have a look at it this week. Would be good if @HipsterJazzbo has any pointers!

hipsterjazzbo commented 9 years ago

I don't currently have any need for this so it might be a while till I get round to it. Happy to accept a PR though!

iamraffe commented 9 years ago

@nicklee1990 @HipsterJazzbo I think I have a working login/register function for Facebook.

I modified the create function in Registrar.php

public function create(array $data)
{
    $userSubclass   = ParseObject::getRegisteredSubclass('_User');
    $user           = new $userSubclass;
    $user->username = $data['email'];
    $user->email    = $data['email'];
    $user->password = $data['password'];
    if(isset($data['name'])){
        $fullName = explode(" ", $data['name']);
        $user->name = $fullName[0];
        $user->lastname = $fullName[1];
    }
    if(isset($data['id'])){
        $user->facebookId = $data['id'];
        $user->pictureURL = $data['avatar'];
        $user->name = $data['first_name'];
        $user->lastname = $data['last_name'];
    }
    $user->signUp();

    return $user;
}

And added a function called fbLogin, in AuthController

public function fbLogin(Request $request){

    if($request->has('code')){
        $fbUser = \Socialize::with('facebook')->user();

        $user = $this->userProvider->retrieveByCredentials($fbUser->user);
        if($user == null){
            $fbUser->user['password'] = Hash::make($user['name']);
            $fbUser->user['avatar'] = $fbUser->avatar;
            //return dd($fbUser->user);
            $this->auth->login($this->registrar->create($fbUser->user));

            return redirect($this->redirectPath());

        }

        if ($this->auth->loginUsingId($user->objectId, true))
        {
            return redirect()->intended($this->redirectPath());
        }

    }else{

        return \Socialize::with('facebook')->redirect();

    }

}

I am using Socialite, so it must be added with composer first, as well as as a service provider and facade.

Let me know what do you guys think.

oflannabhra commented 9 years ago

This is amazing and is identical to my own needs. I'm going to build a twitter Login function similar to this.

nicklee1990 commented 9 years ago

I would've thought this would make use of the ParseUser logInWithFacebook and linkWithFacebook methods so that the authData is correctly populated in Parse and to ensure that the ParseUser is set in the Session?

nicklee1990 commented 9 years ago

@raffe90 @oflannabhra perhaps a slightly more Parse integrated implementation could be as follows (looks like a lot more code but i've just padded it out with comments):

/**
 * Logs a user in using a social network
 * @param $provider
 * @param $request
 * @return mixed
 */
public function handleReturn(Request $request, $provider)
{
    try{
        $user = Socialite::driver($provider)->user(); // I use Socialite Alias as per 5.1 docs
        $expiration_date = new \DateTime();
        $expiration_date->setTimestamp(time() + 86400 * 60); // Set token expiry for below
        $existingUser = $this->userProvider->retrieveByCredentials($user->user); // is there a better way to do this??
        if($existingUser == null){
            $name = Collection::make(explode(' ',$user->getName()));
            $data = [
                "authData" => [
                    $provider => [
                        "id" => $user->getId(), "access_token" => $user->token,
                        "expiration_date" => ParseClient::getProperDateFormat($expiration_date)
                    ]
                ], // you'll need to update Registrar to set the authData array
                "firstName" => $name->first(), // use first and last in case of middle name
                "lastName" => $name->last(),
                "email" => $user->getEmail(), 
                "username" => $user->getEmail(), //this could be different dependent on your app
                "profilePicture" => $user->getAvatar(),
                "password" => \Hash::make($user->getId()),
                "password_confirm" => \Hash::make($user->getId()), //app dependent
            ];
            $this->auth->login($this->registrar->create($data));
        }
        else{
            $data = ["authData" => [
                $provider => [
                    "id" => $user->getId(), "access_token" => $user->token,
                    "expiration_date" => ParseClient::getProperDateFormat($expiration_date)
                ]
            ]];
            // Login with parse to get session token
            $result = ParseClient::_request("POST", "/1/users", "", json_encode($data));
            // This is own subclass but the LaraParse one will do
            User::become($result['sessionToken']);
            // Login using the user object from Parse
            $this->auth->login(User::getCurrentUser());
            return redirect()->intended($this->redirectPath());
        }
    } catch (\Exception $e){
        // If user rejects it throws an exception which we can handle here
    }
}

 /**
 * @param $provider
 * @return mixed
 */
public function redirectToProvider($provider){
    return Socialite::driver($provider)->redirect();
}

Then I use this route:

Route::match(['get','post'],'/auth/login/{provider}','Auth\AuthController@redirectToProvider');
Route::match(['get','post'],'/auth/login/{provider}/return','Auth\AuthController@handleReturn');

I haven't tested this much, only with Facebook. This should work with Twitter however you'll have to ensure that you don't require an email address because annoyingly Twitter doesn't return an email!

iamraffe commented 9 years ago

@nicklee1990 You are totally right. While still developing I was going through the Parse PHP SDK and came accross the logInWithFacebook method and I also ended up copying the contents into my fbLogin method. I was looking into using that method instead of copying but I did not know what to place in the access_token field, what exactly should go there?

Also, I would like to get rid of is the retrieveByCredentials method and use something else. Any ideas on that?

Lastly, I think there is a typo on your code:

$existingUser = $this->userProvider->retrieveByCredentials($fbUser->user);

Should be

$existingUser = $this->userProvider->retrieveByCredentials($user->user);

If I'm not mistaken.

nicklee1990 commented 9 years ago

the access_token you use is just the token you get back from Socialite ($user->token). Using the loginWithFacebook method didn't seem to work for me but in all honesty I didn't spend too long trying to make it work (it throws some Parse Exception). Yeah you're right that's a type in my code, I've updated that now. I split mine into two methods and caught exceptions because otherwise you get stuck in a redirect loop if the user says no on the Social Media site.

As for the retrieveByCredentials method there's different options. I personally use Repositories and have my UserRepository injected into the controller that these methods are in so I just do

$this->user->findBy('username', $user->getId()); // or $user->getEmail() if your username = email

although to be honest using the retrieveByCredentials isn't bad. Also FYI here's an interesting package I've used which seems good http://socialiteproviders.github.io/

oflannabhra commented 9 years ago

Do you guys have more example code? I've got something working, but I feel like it's pretty hacky.

in Auth/ParseUserProvider.php, I added a method to check for existing user by FBId (I store Facebook Id as a separate column):

 public function retrieveByFacebookId($id)
    {
        $query = new ParseQuery('_User');
        $query->equalTo('facebookUserId', $id);

        $user = $query->first(true);

        return empty($user) ? null : $user;
    }

and then use the following code to login an existing user (currently directly in AuthController.php, but should probably be elsewhere)

$parse_user = User::logInWithFacebook($user->id, $user->token); // exp date set automatically in ParseUser
$current_user = User::become($parse_user->getSessionToken());
$current_user->setEmail($user->email);
$current_user->setUsername($user->email);
$current_user->set('facebookUserId', $user->id);
$current_user->set('firstName', head(explode(' ', $user->name)));
$current_user->set('lastName', last(explode(' ', $user->name)));
\Auth::login(User::getCurrentUser());

return redirect('user');

User is my custom subclass of Laraparse\Subclasses\User

iamraffe commented 9 years ago

@nicklee1990 how are you setting the authData when you create the user? I can't get that to work correctly.

oflannabhra commented 9 years ago

@raffe90 are you using the ParseSDK loginWithFacebook() method? It handles setting all of that automatically.

iamraffe commented 9 years ago

@oflannabhra I am not using that method. I am using a code similar to what @nicklee1990 posted, but I can't get it to work correctly.

nicklee1990 commented 9 years ago

I'd recommend using loginWithFacebook() where possible. the only reason i wasn't using it was because there was a bug which has been fixed by the latest update of the parse sdk. In any case, in Registrar when you are setting the properties on the user, you just need to do:

isset($data['authData']) ? $user->setArray("authData", $data['authData']) : null;

This key is only available in the data array if you set it how i did in the code above i.e.

$data = [
    "authData" => [
        $provider => [
            "id" => $user->getId(), "access_token" => $user->token,
            "expiration_date" => ParseClient::getProperDateFormat($expiration_date)
         ]
    ],
    // And the rest
iamraffe commented 9 years ago

The thing is, no matter what I try, I think I am having a problem with Socialite.

I keep getting an error "Invalid State Exception", and it's pretty random. Have you guys had any errors like this?

nicklee1990 commented 9 years ago

Nope never. Do you want to paste your login and registration code and I'll take a look?

iamraffe commented 9 years ago

When trying to use the logInWithFacebook() method I get this:

"You must specify a Parse class name or register the appropriate subclass when creating a new Object. Use ParseObject::create to create a subclass object."

I actually copy/pasted your code @oflannabhra and keep getting that message. Did you get that?

iamraffe commented 9 years ago

Nevermind. I figured it out with the parse php sdk update on line 180.

oflannabhra commented 9 years ago

haha, yeah I did, but submitted a PR to fix it. Glad you got it working!!

mnek84 commented 9 years ago

hi! how do you solve the "You must specify a Parse class name or register the appropriate subclass when creating a new Object. Use ParseObject::create to create a subclass object." message?

iamraffe commented 9 years ago

@mnek84 what I did was took a look at the Parse PHP SDK and realized they had changed the ParseUser class, however my files didn't have that updated code.

So, I changed it manually. On line 180, I changed this:

$user = new ParseUser();

with this code:

$user = ParseObject::create('_User');

Just like they did here:

https://github.com/ParsePlatform/parse-php-sdk/commit/83fd79b0fbcca2060916c1b3c3eed03b62ecd8d2

On their latest commit.

Hope that helps

mnek84 commented 9 years ago

great! change that and it work! thank you @raffe90

inmanpaul commented 8 years ago

Hi, interested to see you have a working login - would someone be able to put together a working PR or even a gist of this code?