OAuth login using Facebook, Google, Twitter and Github with Laravel Socialite

These days majority of web apps delegate authentication to other providers like Facebook, Google, Twitter, Github and many others. By using OAuth we don’t have to worry about storing user credentials on our server and gives user flexibility to use the same account to authorize on multiple platforms.

Socialite from laravel makes it very easy to add multiple providers for authentication, in this post lets build an Auth system on top of Laravel’s default Authentication to support login and signup using Facebook, Google, Twitter and Github.

social login screen

Source Code

Create Laravel App

Create a new laravel app, I am using Laravel 5.4, once it’s done pull the socialite using

composer require laravel/socialite

Now add this into providers and aliases array in config/app.php file.

'providers' => [
    ...
    Laravel\Socialite\SocialiteServiceProvider::class,
]

....
'aliases' => [
    ....
    'Socialite' => Laravel\Socialite\Facades\Socialite::class,
]

Tweak Users Migration

Now let’s add some fields in our user’s migration provided by laravel to store avatar and provider info along with access_token.

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->string('email')->unique();
        $table->string('password');

        $table->string('avatar')->nullable();
        $table->string('provider', 20)->nullable();
        $table->string('provider_id')->nullable();
        $table->string('access_token')->nullable();

        $table->rememberToken();
        $table->timestamps();
    });
}

After this run php artisan migrate and open app/User.php model to add these fields in fillable property so we can mass assign them later.

protected $fillable = [
     'name', 'email', 'password',
     'avatar', 'provider_id', 'provider',
     'access_token'
];

Creating OAuth App

You will also need to add credentials for the OAuth services your application utilizes. Open your config/services.php configuration file, and you should use the key facebooktwitterlinkedingooglegithub or bitbucket, depending on the providers your application requires. For example, if you use GitHub it will be like this

'github' => [
    'client_id' => env('GH_ID'),
    'client_secret' => env('GH_SECRET'),
    'redirect' => env('APP_URL') . '/oauth/github/callback',
],

And in your .env file keep the actual client_id and secret for each providers.

GH_ID=your-github-app-id
GH_SECRET=your-github-app-secret

You can obtain credentials by visiting following developer areas to setup all the providers in services.

Make sure to set APP_URL=http://localhost:8000 in .env file and it must match the providers redirect URL. Later in production you can change this to your actual app url. 

Visit providers website dev section and create Developer applications then fill the below details for each providers in config/services.php.

Github

https://github.com/settings/developers

'github' => [
    'client_id'     => env('GH_ID'),
    'client_secret' => env('GH_SECRET'),
    'redirect'      => env('APP_URL') . '/oauth/github/callback',
]

Facebook

https://developers.facebook.com

'facebook' => [
    'client_id'     => env('FB_ID'),
    'client_secret' => env('FB_SECRET'),
    'redirect'      => env('APP_URL') . '/oauth/facebook/callback',
]

Twitter

https://apps.twitter.com

'twitter' => [
    'client_id'     => env('TW_ID'),
    'client_secret' => env('TW_SECRET'),
    'redirect'      => env('APP_URL') . '/oauth/twitter/callback',
],

Google

https://console.developers.google.com

'google' => [
    'client_id'     => env('GL_ID'),
    'client_secret' => env('GL_SECRET'),
    'redirect'      => env('APP_URL') . '/oauth/google/callback',
],

To start just add Github first, if you can make one work rest will be very easy, you just need client_id and secret for all the providers you wanted to support in config/services.php configuration file.

Setup Routes for Social Auth

We will need three routes, one for showing the list of buttons for providers, another for Redirect to the provider for authentication and last one to handle the callback from provider once authorized.

// Social Auth
Route::get('auth/social', 'Auth\SocialAuthController@show')->name('social.login');
Route::get('oauth/{driver}', 'Auth\SocialAuthController@redirectToProvider')->name('social.oauth');
Route::get('oauth/{driver}/callback', 'Auth\SocialAuthController@handleProviderCallback')->name('social.callback');

As you can see we need a SocialAuthController to connect with routes. Run php artisan make:controller SocialAuthController It will give you a controller inside app/Http/Controllers/ just move it in Auth folder and add below code to complete the implementation of authentication.

SocialAuthController

This controller is responsible to show the login buttons and perform authentication, if it is a new user, we will create a user and login, and if user email id is already present in db we will just update the provider info.

namespace App\Http\Controllers\Auth;

use App\User;
use Exception;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;

class SocialAuthController extends Controller
{

    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * List of providers configured in config/services acts as whitelist
     *
     * @var array
     */
    protected $providers = [
        'github',
        'facebook',
        'google',
        'twitter'
    ];

    /**
     * Show the social login page
     *
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function show()
    {
        return view('auth.social');
    }

    /**
     * Redirect to provider for authentication
     *
     * @param $driver
     * @return mixed
     */
    public function redirectToProvider($driver)
    {
        if( ! $this->isProviderAllowed($driver) ) {
            return $this->sendFailedResponse("{$driver} is not currently supported");
        }

        try {
            return Socialite::driver($driver)->redirect();
        } catch (Exception $e) {
            // You should show something simple fail message
            return $this->sendFailedResponse($e->getMessage());
        }
    }

    /**
     * Handle response of authentication redirect callback
     *
     * @param $driver
     * @return \Illuminate\Http\RedirectResponse
     */
    public function handleProviderCallback( $driver )
    {
        try {
            $user = Socialite::driver($driver)->user();
        } catch (Exception $e) {
            return $this->sendFailedResponse($e->getMessage());
        }

        // check for email in returned user
        return empty( $user->email )
            ? $this->sendFailedResponse("No email id returned from {$driver} provider.")
            : $this->loginOrCreateAccount($user, $driver);
    }

    /**
     * Send a successful response
     *
     * @return \Illuminate\Http\RedirectResponse
     */
    protected function sendSuccessResponse()
    {
        return redirect()->intended('home');
    }

    /**
     * Send a failed response with a msg
     *
     * @param null $msg
     * @return \Illuminate\Http\RedirectResponse
     */
    protected function sendFailedResponse($msg = null)
    {
        return redirect()->route('social.login')
            ->withErrors(['msg' => $msg ?: 'Unable to login, try with another provider to login.']);
    }

    protected function loginOrCreateAccount($providerUser, $driver)
    {
        // check for already has account
        $user = User::where('email', $providerUser->getEmail())->first();

        // if user already found
        if( $user ) {
            // update the avatar and provider that might have changed
            $user->update([
                'avatar' => $providerUser->avatar,
                'provider' => $driver,
                'provider_id' => $providerUser->id,
                'access_token' => $providerUser->token
            ]);
        } else {
            // create a new user
            $user = User::create([
                'name' => $providerUser->getName(),
                'email' => $providerUser->getEmail(),
                'avatar' => $providerUser->getAvatar(),
                'provider' => $driver,
                'provider_id' => $providerUser->getId(),
                'access_token' => $providerUser->token,
                // user can use reset password to create a password
                'password' => ''
            ]);
        }

        // login the user
        Auth::login($user, true);

        return $this->sendSuccessResponse();
    }

    /**
     * Check for provider allowed and services configured
     *
     * @param $driver
     * @return bool
     */
    private function isProviderAllowed($driver)
    {
        return in_array($driver, $this->providers) && config()->has("services.{$driver}");
    }
}

Now lets create the view for login screen.

Create Social View

Lets add all the buttons for providers we are planning to support in our resources/views/auth/social.blade.php.

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-4 col-md-offset-4">
            @if ($errors->has('msg'))
                <div class="alert alert-warning">
                    {{ $errors->first('msg') }}
                    <button type="button" data-dismiss="alert" aria-label="Close" class="close"><span aria-hidden="true">×</span></button>
                </div>
            @endif

            <div class="panel panel-default">
                <div class="panel-heading text-center">Social Login </div>

                <div class="panel-body">
                    <p class="lead text-center">Authenticate using your social network account from one of following providers</p>
                    <a href="{{ route('social.oauth', 'facebook') }}" class="btn btn-primary btn-block">
                        Login with Facebook
                    </a>
                    <a href="{{ route('social.oauth', 'twitter') }}" class="btn btn-info btn-block">
                        Login with Twitter
                    </a>
                    <a href="{{ route('social.oauth', 'google') }}" class="btn btn-danger btn-block">
                        Login with Google
                    </a>
                    <a href="{{ route('social.oauth', 'github') }}" class="btn btn-default btn-block">
                        Login with Github
                    </a>
                    <hr>
                    <a href="{{ route('login') }}" class="btn btn-default btn-block">
                        Login with Email
                    </a>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

How it works?

When a user clicks a button login with Github redirectToProvider() method gets called. Which takes care of sending the user to the OAuth provider login screen.

Before redirecting the user, you may also add additional “scopes” on the request using the scopes method. This method will merge all existing scopes with the ones you supply, for example if you wanted to access more info of a Facebook user you can pass additional scopes:

return Socialite::driver($driver)
    ->scopes(['public_profile', 'user_friends'])
    ->redirect();

You can overwrite all existing scopes using the setScopes method:

return Socialite::driver('github')
        ->setScopes(['scope1', 'scope2'])
        ->redirect();

Back to flow, once user is authenticated provider will make a request to callback url which will call handleProviderCallback() method. In this Socialite::driver($driver)->user(); method will read the incoming request and retrieve the user’s information from the provider.

$providerUser = Socialite::driver($driver)->user();

// All Providers will return these fields 
$providerUser->getId();
$providerUser->getNickname();
$providerUser->getName();
$providerUser->getEmail();
$providerUser->getAvatar();

// Additional details specific to providers as array
$providerUser->user

Next we have a check if an email id was returned fro provider user, since we will be storing it in our users table we will call loginOrCreateAccount() method which will simply create and login a user if not already exists, and if it was already present we will update the avatar and provider_id and provider with update access_token. We need this access token to fetch any other information from providers API.

Stateless Authentication

The stateless method may be used to disable session state verification. This is useful when adding social authentication to an API:

return Socialite::driver('google')->stateless()->user();

We have stored the access_token for user in users table, so if we need to fetch users details anywhere in our app you can get the details about user by calling $user = Socialite::driver('github')->userFromToken($token).

Use access_token for API calls

To access other resource from providers API you will need to authenticate user with scope, it will be different in each provider. For example in github if you wanted to access all the emails of a user, you will need to pass user:email scope like this:

return Socialite::driver($driver)->scopes(['user:email'])->redirect();

Access Github user repository

In order to access authenticated user repository you can use Guzzle HTTP Client to make calls to the endpoint, Guzzle is already installed with Socialite as dependency so you don’t have to pull it.

use GuzzleHttp\Client;

public function getUserGithubRepos() {
    // current logged in user
    $me = auth()->user();

    $client = new Client();
    $response = $client->get('https://api.github.com/user/repos?access_token=' . $me->access_token, [
        'headers' => [
            'Accept' => 'application/json',
        ],
    ]);

    return $response->getBody();
}

You can refer to API docs for more information about how to access certain endpoints for a provider like Facebook, Google Plus, Twitter etc.

As always you can find full source code for this on github and you can use it as a starter kit for your next social auth project. I hope you found it useful, let me know in the comments if you have anything to say.

Source Code