Header Ads

Step by Step Laravel Social Login Authentication



Are you looking for a simple and easy to maintain social login authentication code for your Laravel application? Well, this step by step tutorial might help you to solve that problem. Let's get started!

Laravel Socialite Package Installation

First, we need to install the Laravel Socialite package:
composer require laravel/socialite

After installing the Laravel Socialite package, add the Laravel\Socialite\SocialiteServiceProvider in your config/app.php configuration file:
'providers' => [
    // Other service providers...

    Laravel\Socialite\SocialiteServiceProvider::class,
],

In the same file config/app.php, add the Socialite facade to the aliases array.
'Socialite' => Laravel\Socialite\Facades\Socialite::class,

Next, you need to add your credentials for the OAuth services your application wish to utilize. These credentials should be placed in your config/services.php configuration file, and should use the key facebook, twitter, linkedin, google, github or bitbucket, depending on the providers your application requires:
'facebook' => [
    'client_id' => env('FACEBOOK_CLIENT_ID'),
    'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
    'redirect' => env('FACEBOOK_CLIENT_CALLBACK')
],
Pro tip: Always store your credentials in .env file for security purposes and access it using Laravel env() helper function.
Your .env file might look like this:
FACEBOOK_CLIENT_ID=your_facebook_app_id
FACEBOOK_CLIENT_SECRET=your_facebook_app_secret
FACEBOOK_CLIENT_CALLBACK=http://yourdomain.com/auth/facebook/callback

Below is some of the links on how to get those client ids and secrets, and add those callback urls.

Database Migrations

Now that Laravel Socialite is set, next we have to add a column in the users table. There are two ways to achieve this, first is to update our users table migration file directly and run migrate:refresh artisan command:
$table->json('settings')->nullable();
php artisan migrate:refresh
We are using json type column here because a user might using the same email in their Facebook and Twitter for example. In this way we can associate those different accounts in one account in our system by checking their Facebook or Twitter id.

Second is to create a new migration file and run migrate artisan command:
php artisan make:migration add_settings_to_users_table --table=users
$table->json('settings')->nullable();
php artisan migrate
This is usually useful when you don't want to delete your existing table in your database.

Next, make sure to cast the settings column to array in your User model:
/**
 *  The attributes that should be casted to native types.
 * 
 *  @var array
 */
protected $casts = [
    'settings' => 'array'
];

Routes

Next step is to add a routes to routes/web.php that will redirect to the provider that we are going to support (e.g., Facebook, Twitter, GitHub, etc.) and a callback url that will handle the data that is being returned by the third party provider.
Route::get('auth/{driver}', ['as' => 'socialAuth', 'uses' => 'Auth\SocialController@redirectToProvider']);
Route::get('auth/{driver}/callback', ['as' => 'socialAuthCallback', 'uses' => 'Auth\SocialController@handleProviderCallback']);

Now, your button links might look like this:
<a href="/auth/facebook">Connect with Facebook</a>
<a href="/auth/twitter">Connect with Twitter</a>
<a href="/auth/google">Connect with Google</a>

The Controller

Next step is to create the Auth\SocialController using the make:controller artisan command.
php artisan make:controller Auth/SocialController
The above command will generate a SocialController.php file to app/Http/Controllers/Auth directory.

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Requests;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Social\FacebookServiceProvider;
use GuzzleHttp\Exception\ClientException;
use Laravel\Socialite\Two\InvalidStateException;
use League\OAuth1\Client\Credentials\CredentialsException;

class SocialController extends Controller
{
    protected $providers = [
        'facebook' => FacebookServiceProvider::class,
    ];

    /**
     *  Create a new controller instance
     * 
     * @return  void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     *  Redirect the user to provider authentication page
     * 
     *  @param  string $driver
     *  @return \Illuminate\Http\Response
     */
    public function redirectToProvider($driver)
    {
        return (new $this->providers[$driver])->redirect();        
    }

    /**
     *  Handle provider response
     * 
     *  @param  string $driver
     *  @return \Illuminate\Http\Response
     */
    public function handleProviderCallback($driver)
    {   
        try {
            return (new $this->providers[$driver])->handle();
        } catch (InvalidStateException $e) {
            return $this->redirectToProvider($driver);
        } catch (ClientException $e) {
            return $this->redirectToProvider($driver);
        } catch (CredentialsException $e) {
            return $this->redirectToProvider($driver);
        }
    }
}
Above is the complete code of SocialControler.php. It's quite straight forward, isn't it? We just have to create a protected property $providers array that holds the implementing classes for each third party provider that we're going to support. This approach somehow adheres the Single Responsibility Principle (SRP) in the SOLID object-oriented design.

Next, we make sure the user accessing these entry points is a guest user, and in Laravel you just need a one line of code in the constructor for that, yes, you guessed it right, the built-in middleware method.

Then in the redirectToProvider method we return the new instance of the class in the $providers property and call a redirect method from that class.

Lastly in the handleProviderCallback method we try to execute the handle method from the same class in the $providers property and catch some exceptions and call the redirectToProvider method in the SocialController class, basically, if an exception occur the user will be redirected back to the provider (e.g., Facebook, Twitter, GitHub, etc.).

The Last Step

Now that our controller is ready, it's now time to create those classes in $providers array property. But first let's create an abstract class that can be extended to those classes. Depending on your folder structures, I usually create a new folder for things like this. So let's create a Social directory inside the app directory and create a PHP file called AbstractServiceProvider.php.
<?php

namespace App\Social;

use App\User;
use Laravel\Socialite\Facades\Socialite;

abstract class AbstractServiceProvider
{
    protected $provider;

    /**
     *  Create a new SocialServiceProvider instance
     */
    public function __construct()
    {
        $this->provider = Socialite::driver(
            str_replace(
                'serviceprovider', '', strtolower((new \ReflectionClass($this))->getShortName())
            )
        );
    }

    /**
     *  Logged in the user
     * 
     *  @param  \App\User $user
     *  @return \Illuminate\Http\Response
     */
    protected function login($user)
    {
        auth()->login($user);

        return redirect()->intended('/');
    }

    /**
     *  Register the user
     * 
     *  @param  array $user
     *  @return User $user
     */
    protected function register(array $user)
    {
        $password = bcrypt(str_random(10));
        
        $newUser = User::create(array_merge($user, ['password' => $password]));        

        return $newUser;
    }

    /**
     *  Redirect the user to provider authentication page
     * 
     *  @return \Illuminate\Http\Response
     */
    public function redirect()
    {
        return $this->provider->redirect();
    }

    /**
     *  Handle data returned by the provider
     * 
     *  @return \Illuminate\Http\Response
     */
    abstract public function handle();         
}
In the constructor method we assign the socialite instance to the $provider property by using the Socialite facade and call the driver method, which will accept a string.

The (new \ReflectionClass($this))->getShortName() will return the name of the class that extends the AbstractServiceProvider class.

We also have an abstract handle method that should be implemented by the child class or the class that will extend AbstractServiceProvider class.

After creating the abstract class, we can now start making those wrapper classes. In the same folder where the abstract class is located create a FacebookServiceProvider.php file.
<?php

namespace App\Social;

use App\User;

class FacebookServiceProvider extends AbstractServiceProvider
{
   /**
     *  Handle Facebook response
     * 
     *  @return Illuminate\Http\Response
     */
    public function handle()
    {
        $user = $this->provider->fields([
                    'first_name', 
                    'last_name', 
                    'email', 
                    'gender', 
                    'verified',                    
                ])->user();

        $existingUser = User::whereEmail($user->email)->orWhere('settings->facebook_id', $user->id)->first();

        if ($existingUser) {
            $settings = $existingUser->settings;

            if (! isset($settings['facebook_id'])) {
                $settings['facebook_id'] = $user->id;
                $existingUser->settings = $settings;
                $existingUser->save();
            }

            return $this->login($existingUser);
        }

        $newUser = $this->register([
            'first_name' => $user->user['first_name'],
            'last_name' => $user->user['last_name'],
            'email' => $user->email,
            'gender' => ucfirst($user->user['gender']),
            'settings' => [
                'facebook_id' => $user->id,                
            ]
        ]);        

        return $this->login($newUser);
    }       
}
What we're doing here is we check if the user is an existing user, if so we check their Facebook id if it's already in the settings column, if not we will add it and log in them directly. Otherwise we will register them first before logging in.

Conclusion

That's all there is to it, the idea here is to make more easy adding functionality in the future with minimal changes in the files, in fact the only file you will probably change when adding new functionality is the Auth/SocialController.php, like for example, if you wish to support Twitter or Google, all you have to do is add a key-value pair in the $providers property array in SocialController class. After that create a TwitterServiceProvider.php file in app/Social directory, extends the AbstractServiceProvider class and implement the handle method.

Hope you'll find this tutorial helpful. If you have any questions please don't hesitate to write it down in the comment section below and I'll try to answer it in the best of my ability. Thanks!

No comments