Laravel 7 — Socialite in Action ( Social Media Login Integration with Facebook, Twitter, LinkedIn, Google)

andyyou
6 min readJun 11, 2020

In this post, I’ll show you how to integrate those OAuth of social media step by step via Laravel Socialite. We will create a demo project which is not only a demo but also can use in production.

This post using Laravel Framework 7.15.0

It’s also a note for myself if you want to know how to implement with codebase for production. It’s the right place. Let’s getting start from create a project.

$ laravel new demo

After create the project please create a database and setup .env for it.

// Create database with command line
$ createdb demo

Add configuration, for example I am using postgresql so my .env will like as follows

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=demo
DB_USERNAME=root
DB_PASSWORD=

Install dependencies & auth

To keep things simple I will use bootstrap in this project you can change to any library or framework you want.

# Switch to project folder
$ cd demo
$ composer require laravel/ui
$ php artisan ui bootstrap
$ npm install && npm run dev
$ php artisan ui:auth# Start dev server to check everything works fine
# You should see LOGIN and REGISTER on the top right side in index
$ php artisan serve

Install Socialite

$ composer require laravel/socialite

Migration

In this step we like to modify slight columns for users table. You definitely can just edit the migration file created by default, but I often forget this kind of things so I like to teach you a way that add migrations to modify table after create a table.

First, let’s install a required dependence to support change our table.

$ composer require doctrine/dbal

Then create our migration.

$ php artisan make:migration edit_columns_in_users_table

To understand why we change table as follows, we should explain what goals we want. The requirements list as follows.

  • Support SoftDeletes means users can register, delete their account and register again but we want to keep all data which support by SoftDeletes . Hence email can NOT be unique but we still need another unique key to avoid create a duplicate email if they didn't delete their account in the database layer.
  • Log in by social media means users do NOT have to provide password so it should be nullable.
  • Support OAuth of multiple social media and if the email is the same we should merge some information into the record.
  • You can enable verify or not. The codebase should support various situation.

To ensure you can fully catch up, I post my default migration of users table as well

// Default migration
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});

And it is what we add in new migration.

public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->dropUnique(['email']);
$table->string('password')->nullable()->change();
$table->json('social')->nullable();
$table->softDeletes();
$table->unique(['email', 'deleted_at']);
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropUnique(['email', 'deleted_at']);
$table->dropSoftDeletes();
$table->dropColumn(['social']);
$table->string('password')->change();
$table->string('email')->unique()->change();
});
}

In down() method we place the rollback logic as well. In real production, it worth making sure each step can rollback well. This is most of beginner forgot. After everything is done, now, we can migrate.

$ php artisan migrate

Model

Since we add SoftDeletes and social column, we have to edit our app/User.php model. We add for $casts and setEmailAttribute function

use Illuminate\Database\Eloquent\SoftDeletes;// ...// If you want to support verify you can add implements
class User extends Authenticatable
{
use Notifiable;
use SoftDeletes;

// ...

/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
'social' => 'array',
];

// BTW: in most of case you should keep email in lowercase
public function setEmailAttribute($value)
{
$this->attributes['email'] = strtolower($value);
}
}

Get key and secret from platform

Before we move forward to the next step, you should register those services you want to provide such as Facebook, Twitter, LinkedIn, etc. Because they have different ways to get the credential, so please google it, create app, and get the key. After you get key and secret, you can set up in .env , please feel free add services you want. We save key and secret here because they are sensitive data you should not commit into version control and once they changed you can easily change them. The callback URL we pre-define here we will create later in Routing section.

FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
FACEBOOK_CALLBACK_URL=/login/facebook/callback
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=/login/google/callback
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
LINKEDIN_CALLBACK_URL=/login/linkedin/callback
TWITTER_CLIENT_ID=
TWITTER_CLIENT_SECRET=
TWITTER_CALLBACK_URL=/login/twitter/callback

Configuration

In config/services.php we should register our services. You may notice we add scopes but they don't reveal on official document. Yes it is additationl config option I think it better put them together, in fect you can also put it into .env but need more process to deal with.

// ....'ses' => [
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'facebook' => [
'client_id' => env('FACEBOOK_CLIENT_ID'),
'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
'redirect' => env('FACEBOOK_CALLBACK_URL'),
'scopes' => ['email', 'public_profile'],
],
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_CALLBACK_URL'),
'scopes' => [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'openid',
],
],
'linkedin' => [
'client_id' => env('LINKEDIN_CLIENT_ID'),
'client_secret' => env('LINKEDIN_CLIENT_SECRET'),
'redirect' => env('LINKEDIN_CALLBACK_URL'),
'scopes' => ['r_emailaddress', 'r_liteprofile'],
],
'twitter' => [
'client_id' => env('TWITTER_CLIENT_ID'),
'client_secret' => env('TWITTER_CLIENT_SECRET'),
'redirect' => env('TWITTER_CALLBACK_URL'),
'scopes' => [],
],
// ...

Routing

Remember we set up callback URL in .env, now, we need two routes, to get more information about this part you can refer to docs. Let’s modify routes/web.php

Auth::routes(); // NOTE: If you want to verify you can add ['verify' => true]Route::get('/login/{provider}', 'Auth\LoginController@redirectToProvider')
->name('social.login');
Route::get('/login/{provider}/callback', 'Auth\LoginController@handleProviderCallback')
->name('social.callback');

Controller

It is missing part in official docs and important section as well. We will start from the register part app/Http/Controllers/Auth/RegisterController.php. Because we modified table of database so we should also change validation.

protected function validator(array $data)
{
return Validator::make($data, [
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,NULL,id,deleted_at,NULL'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
}

Next we will extend app/Http/Controllers/Auth/LoginController.php to support Socialite.

namespace App\Http\Controllers\Auth;// ...
use Socialite;
use App\User;
class LoginController extends Controller
{
// ...
public function redirectToProvider(string $provider)
{
try {
$scopes = config("services.$provider.scopes") ?? [];
if (count($scopes) === 0) {
return Socialite::driver($provider)->redirect();
} else {
return Socialite::driver($provider)->scopes($scopes)->redirect();
}
} catch (\Exception $e) {
abort(404);
}
}
public function handleProviderCallback(string $provider)
{
try {
$data = Socialite::driver($provider)->user();

return $this->handleSocialUser($provider, $data);
} catch (\Exception $e) {
return redirect('login')->withErrors(['authentication_deny' => 'Login with '.ucfirst($provider).' failed. Please try again.']);
}
}
public function handleSocialUser(string $provider, object $data)
{
$user = User::where([
"social->{$provider}->id" => $data->id,
])->first();
if (!$user) {
$user = User::where([
'email' => $data->email,
])->first();
}
if (!$user) {
return $this->createUserWithSocialData($provider, $data);
}
$social = $user->social;
$social[$provider] = [
'id' => $data->id,
'token' => $data->token
];
$user->social = $social;
$user->save();
return $this->socialLogin($user);
}
public function createUserWithSocialData(string $provider, object $data)
{
try {
$user = new User;
$user->name = $data->name;
$user->email = $data->email;
$user->social = [
$provider => [
'id' => $data->id,
'token' => $data->token,
],
];
// Check support verify or not
if ($user instanceof MustVerifyEmail) {
$user->markEmailAsVerified();
}
$user->save();
return $this->socialLogin($user);
} catch (Exception $e) {
return redirect('login')->withErrors(['authentication_deny' => 'Login with '.ucfirst($provider).' failed. Please try again.']);
}
}
public function socialLogin(User $user)
{
auth()->loginUsingId($user->id);
return redirect($this->redirectTo);
}
}

View

Finally, we can add login URL to resources/views/auth/login.blade.php and complete our implementation.

@foreach(['facebook', 'twitter', 'google', 'linkedin'] as $provider)
<a class="btn btn-link" href="{{ route('social.login', ['provider' => $provider]) }}">Login with {{ ucwords($provider) }}</a>
@endforeach
@if ($errors->any())
<div class="alert alert-danger">
@foreach ($errors->all() as $error)
<div>{{ $error }}</div>
@endforeach
</div>
@endif

Further Information

You might have questions such as curious about validation rule unique:users,email,NULL,id,deleted_at,NULL. It like docs mention, after unique, the first column is the table name, the second column is the key you want to unique. The third column is for except which is usually use for update method and you can put the id to except. The rest of statement is arguments that pass into where by pair.

Though I said it could be used in production, this implementation still gets few problems that need to take care of. The migration we write $table->unique(['email', 'deleted_at']) is NOT working in database layer if you use SQL

INSERT INTO "users" (email, deleted_at, ...)
VALUES ("andy@example.com", NULL, ...);

It will insert and pass our constraints. But It’s ok because the framework prevents it and works fine. If you concern this you can google and find a lot of discussions. I will not discuss this here and just let you know it exists.

--

--