0

im using socialite to give the users the option to log in with either facebook or github. but when a user logs in with facebook and after that with github, 2 separate accounts are created. So my question is , is there a way to combine these 2 accounts into one? for example if a user that has logged in with facebook is using the same email address to log in with github, no new account will be created and they will simply be logged in

    <?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('profile');
            $table->string('slug');
            $table->string('provider_id');
            $table->string('email')->unique();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('users');
    }
}

login/register code

/**
 * Redirect the user to the provider authentication page.
 *
 * @return Response
 */
public function redirectToProvider($provider)
{
    return Socialite::driver($provider)->redirect();
}

/**
 * Obtain the user information from the provider.
 *
 * @return Response
 */
public function handleProviderCallback($provider)
{
    $SocialUser = Socialite::driver($provider)->stateless()->user();

    $user = $this -> findOrCreateUser($SocialUser,$provider);

    auth()->login($user,true);


    return redirect('/');

}

protected function findOrCreateUser($SocialUser,$provider)
{
    $user = User::firstOrNew(['provider_id' => $SocialUser->id]);

        if ($user->exists) return $user;
    $user->fill([
    'name' => $SocialUser->nickname?:$SocialUser->name,
    'slug' => str_slug($SocialUser->nickname?:$SocialUser->name).'-'.uniqid(),
        'email' => $SocialUser->email,
        'avatar' => $SocialUser->avatar,
        'profile' => Hash::make('no pic'),
        'password' => Hash::make('no need for password token based'),
    // 'website' => 'add a website',
    // 'github_profile' => 'add github profile',
    'email_notifications' => 1
    ])->save();
    $user->assignRole('user');
    \Mail::to($user)->send(new Welcome($user));
    session()->flash('message','Welcome to '.config('app.name').' '.$user->name);
    return $user;
}

}

2 Answers2

1

I have a solution that's using the spirit of everything in this question and answer.

/**
 * @param  string $provider
 * @param  \Laravel\Socialite\Contracts\User $sUser
 * @return \App\User|false
 */
protected function findOrCreateUser($provider, $sUser)
{
    $oauthProvider = OAuthProvider::where('provider', $provider)
        ->where('provider_user_id', $sUser->id)
        ->first();

    if ($oauthProvider) {
        $oauthProvider->update([
            'access_token' => $sUser->token,
            'refresh_token' => $sUser->refreshToken ?? null,
        ]);

        return $oauthProvider->user;
    }

    $user = User::firstWhere('email', $sUser->email);

    if ($user) {
        return $this->createUser($provider, $sUser, $user);
    }

    return $this->createUser($provider, $sUser);
}

/**
 * If a User already exists for the email, skip user creation
 * and add this provider to the list of `$user->oauthProviders`.
 * @param  string $provider
 * @param  \Laravel\Socialite\Contracts\User $sUser
 * @param \App\User $user
 * @return \App\User
 */
protected function createUser($provider, $sUser, User $user = null)
{
    if (!$user) {
        $user = User::create([
            'name' => $sUser->name,
            'email' => $sUser->email,
            'email_verified_at' => now(),
        ]);
    } else if ($user->email_verified_at === null) {
        $user->email_verified_at = now();
        $user->save();
    }

    $user->oauthProviders()->create([
        'provider' => $provider,
        'provider_user_id' => $sUser->id,
        'access_token' => $sUser->token,
        'refresh_token' => $sUser->refreshToken ?? null,
    ]);

    return $user;
}

Before, it had a check for if User::where('email', $sUser->email), and if so, reject the request with an "email already taken" message.

With the oauth_providers table and $user->oauthProviders relationship (User hasMany OAuthProviders), rather than create a new User in the users table every time someone uses oauth, it attaches that oauth record with the existing user $user = User::firstWhere('email', $sUser->email);

If anyone wants a little more, I modified this repo here to make both GitHub and Twitter oauth work: https://github.com/cretueusebiu/laravel-vue-spa. Base yourself around OAuthController.

With the above code, I can register a user via the registration form to capture an email, then login as GitHub and Twitter and have my user plus two oauth providers.

Most of the magic of my solution comes in with the 3rd param on createUser. It will remain to be seen if it works better to leave createUser as always creating, and then make a new method called addProviderToUser. That might be slightly more code, but it might also be simpler and more friendly to unit tests.

Here are my oauth redirect and callback methods too, for science reasons:

/**
 * Redirect the user to the provider authentication page. Twitter uses OAuth1.0a, and does not support
 * Socialite::driver($provider)->stateless(), so library `abraham/twitteroauth` is used to handle everything.
 *
 * @param  string $provider
 * @return \Illuminate\Http\RedirectResponse
 */
public function redirectToProvider($provider)
{
    if ($provider === 'twitter') {
        $tempId = Str::random(40);

        $connection = new TwitterOAuth(config('services.twitter.client_id'), config('services.twitter.client_secret'));
        $requestToken = $connection->oauth('oauth/request_token', array('oauth_callback' => config('services.twitter.callback_url').'?user='.$tempId));

        \Cache::put($tempId, $requestToken['oauth_token_secret'], 86400); // 86400 seconds = 1 day

        $url = $connection->url('oauth/authorize', array('oauth_token' => $requestToken['oauth_token']));
    } else {
        $url = Socialite::driver($provider)->stateless()->redirect()->getTargetUrl();
    }

    return [
        'url' => $url,
    ];
}

/**
 * Obtain the user information from the provider.
 *
 * @param  string $driver
 * @return \Illuminate\Http\Response
 */
public function handleProviderCallback(Request $request, $provider)
{
    if ($provider === 'twitter') {
        $connection = new TwitterOAuth(config('services.twitter.client_id'), config('services.twitter.client_secret'), $request->oauth_token, \Cache::get($request->user));
        $access_token = $connection->oauth('oauth/access_token', ['oauth_verifier' => $request->oauth_verifier]);

        $connection = new TwitterOAuth(config('services.twitter.client_id'), config('services.twitter.client_secret'), $access_token['oauth_token'], $access_token['oauth_token_secret']);
        $user = $connection->get('account/verify_credentials', ['include_email' => 'true']);

        $user->token = $access_token['oauth_token'];
    } else {
        $user = Socialite::driver($provider)->stateless()->user();
    }
    $user = $this->findOrCreateUser($provider, $user);

    $this->guard()->setToken(
        $token = $this->guard()->login($user)
    );

    return view('oauth/callback', [
        'token' => $token,
        'token_type' => 'bearer',
        'expires_in' => $this->guard()->getPayload()->get('exp') - time(),
    ]);
}

config/services.php

'github' => [
    'client_id' => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'callback_url' => env('GITHUB_CALLBACK_URL'),
    'provider_name' => env('GITHUB_PROVIDER_NAME', 'GitHub'),
],

'twitter' => [
    'client_id' => env('TWITTER_CLIENT_ID'),
    'client_secret' => env('TWITTER_CLIENT_SECRET'),
    'callback_url' => env('TWITTER_CALLBACK_URL'),
    'provider_name' => env('TWITTER_PROVIDER_NAME', 'Twitter'),
],

.env

# localhost
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_CALLBACK_URL=https://valet.test/api/oauth/github

TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=
TWITTER_CALLBACK_URL=https://valet.test/api/oauth/twitter/callback

You'd have to look in the above sample repo to figure out how those env variables are being consumed, but hint: look at spa.blade.php, vuex, and api.php

agm1984
  • 15,500
  • 6
  • 89
  • 113
0

try changing your code to this:

$user = User::where('email', $SocialUser->email)->first();

if (!empty($user) && in_array($SocialUser->id, $user->provider_id) ) {

    session()->flash('message','Welcome to '.config('app.name').' '.$user->name);

    return $user;

}

if (empty($user) ) {

    $user = User::create([
        'name' => $SocialUser->nickname?:$SocialUser->name,
        'slug' => str_slug($SocialUser->nickname?:$SocialUser->name).'-'.uniqid(),
        'email' => $SocialUser->email,
        'avatar' => $SocialUser->avatar,
        'profile' => Hash::make('no pic'),
        'password' => Hash::make('no need for password token based'),
        // 'website' => 'add a website',
        // 'github_profile' => 'add github profile',
       'email_notifications' => 1,
       'provider_id' => [$SocialUser->id]
    ]);

    $user->assignRole('user');

    \Mail::to($user)->send(new Welcome($user));

    session()->flash('message','Welcome to '.config('app.name').' '.$user->name);

    return $user;

}

$providers = array_push($user->provider_id, $SocialUser->id);

$user->update([
    'provider_id' => $providers
]);

session()->flash('message','Welcome to '.config('app.name').' '.$user->name);

return $user;

You are best also adding this to your User model:

protected $casts = [
    'provider_id' => 'array'
];

I hope this helps

Josh
  • 1,316
  • 10
  • 26