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