Social login with Laravel 5 using multiple providers
The code of the finished tutorial is located here: https://github.com/dexbarrett/laravel-socialite-tutorial
Contents
- Requirements
- Setting up the project
- Creating the layout
- Creating and configuring the provider applications
- Authentication and login
- Troubleshooting
Requirements
Web Server and PHP
We will need a running web server like Apache or Nginx with PHP 5.6.4 or higher. For Windows users I recommend either installing Wamp or using Laravel Homestead.
GIT and Composer
First, make sure you have have Git installed.
If you are using Ubuntu you can install Git from the terminal by running these commands:
1 2 |
sudo apt-get update sudo apt-get install git |
Windows users can install Git for Windows
If you already have Git, skip this step 😛
We also need the Composer package manager to install Laravel and other packages we need. If you don’t have it, please refer to the installation guide on the Composer’s website.
Setting up the project
First things first. We need the Laravel framework (of course), so we are going to create a new Laravel project using Composer. If you already have an existing project you can skip this first step. Run this command in your terminal to get the latest version of Laravel (5.4 at this time of writing):
composer create-project laravel/laravel social-login
However, if you want to install Laravel 5.3, for example, you need to specify the version in the command:
composer create-project laravel/laravel social-login 5.3.*
Here we just clone the laravel/laravel
github repository and install its dependencies in one step. The content will be created within a folder named social-login
Feel free to name yours different if you wish. Composer may take a while to install the dependencies, so be patient 😀
Now we need to ensure we give write permissions to the webserver. If you are on Windows or using Homestead it’s very likely you already have permissions. Otherwise you can do something like this if you are on Linux or OSX:
1 2 |
chmod o+w -R storage chmod o+w -R bootstrap/cache |
Next we need to include and install the Socialite package. Run this command on your terminal to require and install the latest version of Socialite:
composer require laravel/socialite
This version of Socialite is compatible with the latest version of Laravel, so to make it work with Laravel 5.3 you need to install Socialite 2.x:
composer require laravel/socialite ~2.0
Once Composer has finished installing Socialite, we need to add its service provider and Facade to our project. Open your editor of choice and open the file located at config/app.php
within your Laravel project folder. On that file, look for the providers
key and add the following value:
Laravel\Socialite\SocialiteServiceProvider::class
That part of the file should look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
'providers' => [ /* * Laravel Framework Service Providers... */ IlluminateAuthAuthServiceProvider::class, IlluminateBroadcastingBroadcastServiceProvider::class, IlluminateBusBusServiceProvider::class, IlluminateCacheCacheServiceProvider::class, IlluminateFoundationProvidersConsoleSupportServiceProvider::class, IlluminateCookieCookieServiceProvider::class, IlluminateDatabaseDatabaseServiceProvider::class, IlluminateEncryptionEncryptionServiceProvider::class, IlluminateFilesystemFilesystemServiceProvider::class, IlluminateFoundationProvidersFoundationServiceProvider::class, IlluminateHashingHashServiceProvider::class, IlluminateMailMailServiceProvider::class, IlluminatePaginationPaginationServiceProvider::class, IlluminatePipelinePipelineServiceProvider::class, IlluminateQueueQueueServiceProvider::class, IlluminateRedisRedisServiceProvider::class, IlluminateAuthPasswordsPasswordResetServiceProvider::class, IlluminateSessionSessionServiceProvider::class, IlluminateTranslationTranslationServiceProvider::class, IlluminateValidationValidationServiceProvider::class, IlluminateViewViewServiceProvider::class, Laravel\Socialite\SocialiteServiceProvider::class, // added line for Socialite |
On that same file, look for the aliases
key and add the following:
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
And that part should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
'aliases' => [ 'App' => IlluminateSupportFacadesApp::class, 'Artisan' => IlluminateSupportFacadesArtisan::class, 'Auth' => IlluminateSupportFacadesAuth::class, 'Blade' => IlluminateSupportFacadesBlade::class, 'Cache' => IlluminateSupportFacadesCache::class, 'Config' => IlluminateSupportFacadesConfig::class, 'Cookie' => IlluminateSupportFacadesCookie::class, 'Crypt' => IlluminateSupportFacadesCrypt::class, 'DB' => IlluminateSupportFacadesDB::class, 'Eloquent' => IlluminateDatabaseEloquentModel::class, 'Event' => IlluminateSupportFacadesEvent::class, 'File' => IlluminateSupportFacadesFile::class, 'Gate' => IlluminateSupportFacadesGate::class, 'Hash' => IlluminateSupportFacadesHash::class, 'Lang' => IlluminateSupportFacadesLang::class, 'Log' => IlluminateSupportFacadesLog::class, 'Mail' => IlluminateSupportFacadesMail::class, 'Password' => IlluminateSupportFacadesPassword::class, 'Queue' => IlluminateSupportFacadesQueue::class, 'Redirect' => IlluminateSupportFacadesRedirect::class, 'Redis' => IlluminateSupportFacadesRedis::class, 'Request' => IlluminateSupportFacadesRequest::class, 'Response' => IlluminateSupportFacadesResponse::class, 'Route' => IlluminateSupportFacadesRoute::class, 'Schema' => IlluminateSupportFacadesSchema::class, 'Session' => IlluminateSupportFacadesSession::class, 'Storage' => IlluminateSupportFacadesStorage::class, 'URL' => IlluminateSupportFacadesURL::class, 'Validator' => IlluminateSupportFacadesValidator::class, 'View' => IlluminateSupportFacadesView::class, 'Socialite' => Laravel\Socialite\Facades\Socialite::class, // added line for Socialite |
Creating the layout
We already installed Laravel and Socialite. Now, before diving into the configuration of the social providers, let’s create something we can take a look at!
I must confess I suck at design, but that’s no excuse for making your app look like it’s 1995. We will use Twitter Bootstrap, Font Awesome and Bootstrap Social Buttons to give our app a less crappy look.
Master! Master!
Let’s create a master view or layout. This will contain common markup for the different sections of the app. Each section will inherit that markup and add its own content.
Within the folder located at resources/views
create a new folder called layouts
. And, within this new folder, create a new file called master.blade.php
. This new view file should contain the following (you can double-click inside the code block sections to be able to select the content):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>@yield('page-title') - Backendtime Social Login</title> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"> <link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.0.0/bootstrap-social.min.css"> <style> body { padding-top: 73px; } .social-button { text-align: center; } ul { list-style-type: none; } li { margin-bottom: 3px; } </style> </head> <body> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/">Backendtime Social Login</a> </div> <div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav navbar-right"> <li> <a href="#">Sign In <i class="fa fa-sign-in"></i></a> </li> </ul> </div> </div> </nav> <div class="container"> @yield('page-content') </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script> </body> </html> |
This master template includes the styles and scripts for Bootstrap, Font Awesome and Bootstrap Social Buttons from a CDN. The markup is a common template that has a navbar fixed on top of the page.
The home view
Next, create a new file within resources/views
called home.blade.php
. This new view should contain the following:
1 2 3 4 5 6 7 8 |
@extends('layouts.master') @section('page-title', 'Home') @section('page-content') <div class="col-md-10"> <h3>Welcome to my awesome app!</h3> </div> @stop |
This is the home page of the app. It inherits the contents from the master template. We indicate this using the @extends
directive and specifying the path of the view, relative to the resources/views
folder. In this case, the master view is within the layouts
folder, that’s why we use that in the path. Notice too that subfolders can be indicated using “dot syntax”. Forward slashes are valid too, if you prefer to use them.
The @section
directive is used to fill a section defined elsewhere (in this case, the master template). This is similar to class inheritance in OOP.
We use the @section
directive to indicate the page-title
and page-content
sections. The content of a section can be defined inline or as a block. In the latter case it must be ended with a @stop
directive.
Now, modify the default route in the file routes/web.php
so that it points to home.blade.php
view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('home'); }); |
Go to the root of your site on the browser. I created a local site called social-login.dev so I’ll go to http://social-login.dev
The home view should appear, like this.
The login view
Create a new view file within resources/views
called login.blade.php
. This view should contain the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@extends('layouts.master') @section('page-title', 'Please login') @section('page-content') <div class="row"> <div class="col-md-9 col-md-offset-1"> <ul> <li> <a href="#" class="btn btn-block btn-lg btn-social btn-facebook social-button"> <span class="fa fa-facebook"></span> login with Facebook </a> </li> <li> <a href="#" class="btn btn-block btn-lg btn-social btn-google social-button"> <span class="fa fa-google"></span> login with Google </a> </li> <li> <a href="#" class="btn btn-block btn-lg btn-social btn-twitter social-button"> <span class="fa fa-twitter"></span> login with Twitter </a> </li> </ul> </div> </div> @stop |
This view extends from the master layout as well. The content of the page-content
section adds three buttons, one for each of the providers we’ll be using: Facebook, Google and Twitter. The markup of these buttons uses the Font Awesome and Bootstrap Social Buttons classes to give the proper look and feel.
Before adding a new route for this view, create a controller. We will use this controller to process the login logic, instead of having all that in anonymous functions.
We can create the controller using the terminal by issuing the following command:
php artisan make:controller LoginController
This will create a file within app/Http/Controllers
called LoginController.php
. Open that file and add a new method called showLoginPage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class LoginController extends Controller { public function showLoginPage() { return view('login'); } } |
Now, in routes/web.php
add a new route called login and point it to the showLoginPage of LoginController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('home'); }); Route::get('login', 'LoginController@showLoginPage'); |
Now, if you go to /login on your site you should see a page like this:
Right now, the Sign In link on the navbar points nowhere. Make a small modification to the master.blade.php
file so that it resolves to the route corresponding to the showLoginPage (the /login route):
1 2 3 4 5 6 7 |
<div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav navbar-right"> <li> <a href="{{ action('LoginController@showLoginPage') }}">Sign In <i class="fa fa-sign-in"></i></a> </li> </ul> </div> |
Now, clicking the Sign In link will navigate to the login page.
Creating and configuring the provider applications
We have the UI of the app, but it doesn’t do anything yet. In order to implement social login with Socialite, we first need to create a new application on each provider. To be able to do this, we need to have an account on each service. If you don’t have an account, create one following the corresponding link.
Create a Facebook account
Create a Google account
Create a Twitter account
Creating a Facebook application
To create a Facebook application go to https://developers.facebook.com/apps/ and login with your Facebook credentials. On the new screen that appears click the Add a New App button:
A modal dialog will appear asking you basic information such as your application name, category and an email address for contact. Fill this information accordingly and click Create App ID.
You may need need to enter a captcha after this, do it and you will be taken to your app’s admin page.
On this page, click settings located on the left menu and you will be shown the following screen:
Click the Add Platform button and select Website on the new dialog that appears.
A new section called Website will be added at the bottom:
Fill the input with the name of your host followed by: /login/facebook/callback. We haven’t defined these routes yet in Laravel, but will do later. All the provider routes will follow the same pattern: /login/provider/callback.
Finally, click the Save Changes button located at the bottom right.
Laravel provider configuration
Copy the values shown in your Facebook app settings: App ID and App Secret (for the App Secret to show click the Show button). Now edit the config/services.php
file and add a new key called facebook
to the array with the following values:
1 2 3 4 5 6 |
'facebook' => [ 'client_id' => env('FACEBOOK_CLIENT_ID'), 'client_secret' => env('FACEBOOK_CLIENT_SECRET'), 'redirect' => 'http://social-login.dev/login/facebook/callback', ], |
Now edit your .env
file and add the following lines:
1 2 |
FACEBOOK_CLIENT_ID=yourAppIDHere FACEBOOK_CLIENT_SECRET=YourAppSecretHere |
Creating a Google application
To create a Google app, go to https://console.developers.google.com/iam-admin/projects and sign in with a Google account. On this screen click the CREATE PROJECT button located on the top menu bar:
You will be presented with a new screen to give your proyect a name. This is not what will be shown to users on the authentication screen; this is just an identifier for you. The name displayed is configured later.
Click Create and your project will be created. You’ll be redirected to the API Manager section. Here, you need to enable the Google+ API. Just type Google+ on the search field and select it from the results:
Once you select it, this screen will appear. Just click the Enable button:
Once the Google+ API has been enabled, go to the Credentials section using the link located on the left menu. On this new screen, select the Oauth Consent screen tab and this will be shown:
This section allows you to customize the information that will be presented to the user when they are prompted to authorize this app. You can set things like the homepage URL and the logo image, but these are optional. The required field here is the one labeled Product name shown to users. If you don’t fill this field, you won’t be allowed to generate credentials for the app.
Click the Save button and you will be taken to the Credentials tab of this section. On the right side of this screen you will see a Credentials dialog. Click on the Create credentials dropdown and select OAuth client ID from the options:
On this new screen you need to select the application type:
Select the Web application option and the screen will expand showing the settings required for this type of app:
On this new section you can change the name field if you want, but it’s just an identifier. However, you must add the callback URL on the Authorized redirect URIs field. This is the URL where users will be taken once they accept/deny the Google OAuth app.
Add the following to the Authorized redirect URIs field: http://yourlaravelsite.domain/login/google/callback
Click Create and you will be presented with a dialog showing (finally!) the app credentials:
You can copy this values now, but they can be accessed from the credentials link on the left menu too. On the screen that appears just select the client name:
And a new screen will show the credentials:
Adding the Google credentials to Laravel
Now that you have the Google app credentials, edit the file config/services.php
and add a new key named google
with the following values:
1 2 3 4 5 6 |
'google' => [ 'client_id' => env('GOOGLE_CLIENT_ID'), 'client_secret' => env('GOOGLE_CLIENT_SECRET'), 'redirect' => 'http://social-login.dev/login/google/callback', ], |
After that, edit your .env
file and add the following lines:
1 2 |
GOOGLE_CLIENT_ID=yourGoogleAppClientID GOOGLE_CLIENT_SECRET=yourGoogleAppClientSecret |
Creating a Twitter Application
To create a Twitter application go to https://apps.twitter.com and sign in with a Twitter account. You will be redirected to the application management screen:
Click the Create New App button and the next screen will be shown:
This will ask for basic information for the app like the name and description. These fields are required but the one you must fill in is the one for the callback URL. Add the following URL, replacing the hostname with your laravel hostname: http://yourlaravelsite.domain/login/twitter/callback
Once you filled the information, tick the Yes, I agree checkbox and click the Create your twitter application button.
You will be redirected to the new app’s management page:
By default, Twitter won’t give your app access to the user’s email, but this permission is easy to get. First, go to the Settings tab on your app’s management page and look for the Privacy Policy URL and Terms of Service URL fields. You need to fill these or Twitter won’t let your app get the email permission. For now, just enter your app’s domain. Once your app is live you need to create these pages and update the urls:
Click the Update Settings button at the bottom and now go to the Permissions Tab.
Unless your app needs to post tweets on behalf of the user, change the permissions to Read only. Then go to the Additional Permissions section below and check the box that says Request email addresses from users. Click the Update Settings button to save the changes.
If you didn’t copy your app’s Consumer Key and Consumer Secret before, go to the Keys and Access Tokens tab and copy these values. Now, edit the config/services.php
file and add a new key called twitter
to the array with the following:
1 2 3 4 5 6 |
'twitter' => [ 'client_id' => env('TWITTER_CLIENT_ID'), 'client_secret' => env('TWITTER_CLIENT_SECRET'), 'redirect' => 'http://social-login.dev/login/twitter/callback', ], |
Then, edit the .env
file and add these lines:
1 2 |
TWITTER_CLIENT_ID=YourTwitterAppConsumerKey TWITTER_CLIENT_SECRET=YourTwitterAppConsumerSecret |
Authentication and login
We have the provider configuration set. Now we need to create the code… not so fast; let’s create some database migrations first!
Laravel already includes a user migration. Locate the file in database/migrations
called something like 2014_10_12_000000_create_users_table.php
and edit it so that it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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('email')->unique(); $table->rememberToken(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } } |
Now we need a new migration to store information related to social providers. Run the following command in your terminal to create it:
php artisan make:migration create_social_login_profiles_table --create=social_login_profiles
This will create a new file within database/migrations
called something like 2016_06_03_204539_create_social_login_profiles_table.php
Edit the new migration file and make it look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class CreateSocialLoginProfilesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('social_login_profiles', function (Blueprint $table) { $table->increments('id'); $table->integer('user_id')->unsigned() ->references('id')->on('users'); $table->string('facebook_id')->nullable(); $table->string('google_id')->nullable(); $table->string('twitter_id')->nullable(); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('social_login_profiles'); } } |
Connect to your local MySQL server through the terminal or using a client like MySQL Workbench, HeidiSQL (Windows only) or Sequel Pro (Mac only).
Once you are connected, create a new database called social_login
Now we have to update the .env
and add the database connection details. Locate the following lines on that file and edit accordingly:
1 2 3 4 5 6 |
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=social_login DB_USERNAME=root DB_PASSWORD=yourRootPassword |
Now execute this command to run the migrations and create the tables:
php artisan migrate
Coding Time
Login with the social providers consists of these steps:
- In our web application, the user selects to login with one of the providers and gets redirected to the provider’s authentication page
- The user accepts/denies the provider app and is taken back to our web app’s callback URL
- Our web application handles the success/failure of the authentication
Let’s start by creating a new model that will represent the social login providers. To do this, run the following artisan command:
php artisan make:model SocialLoginProfile
Now, edit the User
model located at app/User.php
to create a relationship method pointing to the SocialLoginProfile
model. We’ll also modify the $fillable
array to specify which model fields can be mass-assigned when creating a new model instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?php namespace App; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; class User extends Authenticatable { use Notifiable; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = ['email']; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; public function socialProfile() { return $this->hasOne(SocialLoginProfile::class); } } |
Now we will create a new class that will handle the social authentication. We do this so we don’t have all the code inside the LoginController
.
Create a new file within the app
directory and call it LoginUser.php
. It should contain the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?php namespace App; use App\Exceptions\SocialAuthException; use Exception; use Socialite; class LoginUser { public function authenticate($provider) { return Socialite::driver($provider)->redirect(); } public function login($provider) { try { $socialUserInfo = Socialite::driver($provider)->user(); $user = User::firstOrCreate(['email' => $socialUserInfo->getEmail()]); $socialProfile = $user->socialProfile ?: new SocialLoginProfile; $providerField = "{$provider}_id"; $socialProfile->{$providerField} = $socialUserInfo->getId(); $user->socialProfile()->save($socialProfile); auth()->login($user); } catch (Exception $e) { throw new SocialAuthException("failed to authenticate with $provider"); } } } |
What does this class do?
We first import the classes we are going to need. We import the Socialite facade and a custom exception class (that we’ll create shortly).
Then we have two methods. The authenticate method makes use of the driver method of Socialite, which takes a string representing the social provider we want to use. It’s important to note that this method expects a value matching a supported provider. So, if we want to authenticate with facebook we should pass the string facebook
; for Google we pass google
and so on. In this case we define a parameter called $provider
which we will pass to the Socialite’s driver method and then chain the redirect method which will take the user to the appropiate authentication page.
The login method has more logic as you can see. It accepts a $provider
parameter that we’ll pass to Socialite as well.
We begin by defining a try…catch block. We do this because if Socialite fails to properly authenticate the user, it will throw an exception. We will catch a general Exception
because Socialite can throw different types of exceptions, but no matter which one it throws, we’ll handle it as a failed authentication. Still, we will throw a custom SocialAuthException
that will be intercepted by our LoginController
.
The first thing we do inside the try
block is to retreive an instance of the corresponding provider using the driver method and chaining the user method. The user method returns an instance of LaravelSocialiteOneUser
or LaravelSocialiteTwoUser
depending on the provider. This User
class encapsulates user data obtained from the provider such as email, nickname, name and avatar.
Once we have the user data, we use the User model’s firstOrCreate method to look for a record in the users table that matches the email obtained from the provider. If a record is found, it will return the corresponding User
model; otherwise it will create a new record on the users table and return a User model as well.
Then we use the socialProfile
dynamic property in the User
model to check if the relationship exists. We use a the ternary shortcut operator to test this: if the property socialProfile
returns a non-null value, that will be assigned to $socialProfile
; otherwise, a new instance of the SocialLoginProfile
will be created and assigned to $socialProfile
. It’s important to note that the property name is socialProfile
which is different to the model name SocialLoginProfile
. This is because the property name is based on the method declared in the User
model, and SocialLoginProfile
is the actual model where the method socialProfile()
in app/User.php
points to.
We do this to see if this is the first time a user logs into the application and create the appropiate records in the database.
We then update the SocialLoginProfile
model instance and dinamically set the field corresponding to the provider the user used to authenticate. We set its value equal to the ID obtained from the provider and save it to the database.
Finally, we use the auth()
helper method to obtain access to the Laravel’s authentication class and we use its login method, which accepts a User
model as a parameter and logs it into our application.
No we have the core of the social login process ready. But we need some more work to do to wire things up. First, let’s create the custom exception class that we mentioned before.
Create a new file within the app/Exceptions
folder and call it SocialAuthException.php
. It will contain the following:
1 2 3 4 5 6 7 8 9 10 |
<?php namespace App\Exceptions; use Exception; class SocialAuthException extends Exception { } |
Now let’s move into view territory. We need to create a page that will simulate the dashboard.
Create a new file within resources/views
and call it dashboard.blade.php
. It should contain the following:
1 2 3 4 5 6 7 8 |
@extends('layouts.master') @section('page-title', 'Dashboard') @section('page-content') <div class="col-md-10"> <h3>This is the dashboard</h3> </div> @stop |
Simple stuff. Just a view extending the master layout, with a header.
Now let’s make a new route to show this new view. Edit the routes/web.php
file and add the following:
1 |
Route::get('dashboard', 'LoginController@showDashBoard'); |
Now edit the app/Http/Controllers/LoginController.php
file and add the corresponding method:
1 2 3 4 |
public function showDashboard() { return view('dashboard'); } |
If you navigate to http://yourLaravelSite.domain/dashboard you should see the dashboard view:
But there’s a small problem: this view should be available to logged users only. Let’s fix that!
First edit the file at app/Exceptions/Handler.php
and change the unauthenticated method so that it points to the route mapped to LoginController@showLoginPage
. This is where not-logged users will be redirected when trying to access a protected page:
1 2 3 4 5 6 7 8 |
protected function unauthenticated($request, AuthenticationException $exception) { if ($request->expectsJson()) { return response()->json(['error' => 'Unauthenticated.'], 401); } return redirect()->guest(action('LoginController@showLoginPage')); } |
Now let’s protect the dashboard page using this middleware. Edit the routes/web
file and modify the dashboard route like this:
1 2 |
Route::get('dashboard', 'LoginController@showDashBoard') ->middleware(['auth']); |
Now, if we you go to http://yourLaravelSite.domain/dashboard it will redirect you to the login page.
We already have the LoginUser
, class but our LoginController
doesn’t know about it. Let’s modify our app/Http/Controllers/LoginController.php
file and make it use the LoginUser
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
<?php namespace App\Http\Controllers; use App\LoginUser; use Illuminate\Http\Request; use App\Exceptions\SocialAuthException; class LoginController extends Controller { protected $loginUser; public function __construct(LoginUser $loginUser) { $this->loginUser = $loginUser; } public function showLoginPage() { return view('login'); } public function showDashboard() { return view('dashboard'); } public function auth($provider) { return $this->loginUser->authenticate($provider); } public function login($provider) { try { $this->loginUser->login($provider); return redirect()->action('LoginController@showDashBoard'); } catch (SocialAuthException $e) { return redirect()->action('LoginController@showLoginPage') ->with('flash-message', $e->getMessage()); } } public function logout() { auth()->logout(); return redirect()->to('/'); } } |
What we do first is to import the LoginUser
and SocialAuthException
classes. Then we define the constructor method which takes the LoginUser
class as a parameter and stores the reference into a class property called $loginUser
. When the controller runs, Laravel will automatically inject an instance of the LoginUser
class and it will be available in the class property.
Then we have a method called auth which simply calls the authenticate method of the LoginUser
class. This is where we redirect the user to the provider’s authentication page.
We have another method, called login, which uses the LoginUser
class as well. This method will run when the user comes back from the provider’s authentication page.
Here we have a try…catch block. If all goes well, the user will be logged in and redirected to the dashboard page. But if something goes wrong, the SocialAuthException
will be thrown and the catch
block will be executed. The catch
block redirects the user to the login page and injects a message into the session informing that the authentication failed.
We also have a logout method. This simply uses Laravel’s authentication to logout the user and redirects to the home page.
Bear with me; we’re almost done.
The controller now uses the LoginUser
class, but we haven’t created the corresponding routes. So, edit the routes/web.php
file and add this:
1 2 3 4 5 6 7 8 |
Route::get('logout', 'LoginController@logout'); Route::get('login/{provider}', 'LoginController@auth') ->where(['provider' => 'facebook|google|twitter']); Route::get('login/{provider}/callback', 'LoginController@login') ->where(['provider' => 'facebook|google|twitter']); |
Here we add a route to the logout method of the LoginController, and other two routes that point to the auth and login methods, respectively.
These last two routes accept a parameter called provider
. This dynamic value will contain the provider that we want to use, and this value will be passed to the controller method mapped to the route. We also add some constraints to those two routes. We use a regex saying that the provider
parameter can only contain one of the following strings: facebook
, google
or twitter
. Any other value will not match the route pattern and will result in a 404 error.
Now let’s update our login page buttons so they point to their corresponding provider. Edit the resources/views/login.blade.php
file and change accordingly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@extends('layouts.master') @section('page-title', 'Please login') @section('page-content') <div class="row"> <div class="col-md-8 col-md-offset-1"> <ul> <li> <a href="{{ action('LoginController@auth', ['provider' => 'facebook']) }}" class="btn btn-block btn-lg btn-social btn-facebook social-button"> <span class="fa fa-facebook"></span> login with Facebook </a> </li> <li> <a href="{{ action('LoginController@auth', ['provider' => 'google']) }}" class="btn btn-block btn-lg btn-social btn-google social-button"> <span class="fa fa-google"></span> login with Google </a> </li> <li> <a href="{{ action('LoginController@auth', ['provider' => 'twitter']) }}" class="btn btn-block btn-lg btn-social btn-twitter social-button"> <span class="fa fa-twitter"></span> login with Twitter </a> </li> </ul> </div> </div> @stop |
We use the action()
helper to point to the route mapped to the auth method of the LoginController. But since this route requires a dynamic parameter, we need to pass it too. We do this by using an array as a second parameter in the action()
helper, using a key that matches the route parameter and the corresponding provider as the value.
Remember that the LoginController will redirect the user to the login page if the authentication fails. It will store a message in the session too. We need to update our login view to display this message.
First, we will need a partial view. A view that only will contain the markup that display the error messages.
Create a new folder within resources/views
called partials. Inside this folder create a new file called flash-messages.blade.php
which should have the folloing content:
1 2 3 4 5 6 |
@if(session()->has('flash-message')) <div class="alert alert-danger text-center" role="alert"> {{ session()->get('flash-message') }} </div> @endif |
This view uses the session()
helper to access data stored in the session. The has method returns true
if a value with the key flash-message exists. If it does, it will show the markup and will access the value using the has method of the Session class.
Now we will update the login page to include this partial view.
Edit the resources/views/login.blade.php
file and update accordingly:
1 2 3 4 5 6 7 8 |
@extends('layouts.master') @section('page-title', 'Please login') @section('page-content') <div class="row"> <div class="col-md-7 col-md-offset-2"> @include('partials.flash-messages') </div> <div class="col-md-9 col-md-offset-1"> |
We use the Blade’s include()
directive to insert the content of the partial view we created before.
And talking about partials, we’ll need another one. Why? We are going to create a small dropdown on the navbar that displays the email of the logged in user and contains a link to go to the dashboard and another link to sign out.
Create a new file at resources/views/partials
called user-menu.blade.php
. It should contain this:
1 2 3 4 5 6 7 8 9 |
<li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">logged as {{ auth()->user()->email }} <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="{{ action('LoginController@showDashBoard') }}">Dashboard</a></li> <li role="separator" class="divider"></li> <li><a href="{{ action('LoginController@logout') }}">Sign out</a></li> </ul> </li> |
This partial uses the user method of Laravel’s authentication class. This method returns an instance of the user model if the user is logged in. So, this dropdown should only appear if the user is logged in, right? Right!
To do this, we need to edit the resources/views/layouts/master.blade.php
file and modify it like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav navbar-right"> @if(auth()->check()) @include('partials.user-menu') @else <li> <a href="{{ action('LoginController@showLoginPage') }}"> Sign In <i class="fa fa-sign-in"></i> </a> </li> @endif </ul> </div> |
Once again, we use the auth()
helper to use Laravel’s authentication goodies. The check method returns true
if the user is logged in. So, if the user is logged in, it includes the contents of the partial view that contains the menu markup. Otherwise, it display the link that takes to the login page.
And we’re done with the changes. Let’s try this out!
For example, click on login with Google. The authentication page will appear:
If you click on Deny, you’ll be redirected back to the login page and an error message will be shown:
But now, click again login with Google and allow access to the application. You’ll be logged into the web app and redirected to the dashboard:
If you click the Sign out link from the dropdown, you will be logged out and redirected to the home page.
Aaaaand pretty much that’s it! We’re finally done. I really hope this helped you if you had trouble implementining Socialite in your Laravel application.
Next, I included a troubleshooting section with common problems that you may encounter and the solutions.
The code of the finished tutorial is here https://github.com/dexbarrett/laravel-socialite-tutorial
Troubleshooting
- You get an error like this cURL error 60: SSL certificate: unable to get local issuer certificate
This is most likely to happen if you are on Windows. To fix this problem first download and extract this cert file and place in a proper location. For example, if you are using WAMP, place it in C:\wamp\bin\php\yourVersionOfPHP\extras\ssl
.
Now, open your php.ini
file and look for the line that says curl.cainfo
and point it to the path of your extracted cacert.pem
file. Remove the semicolon at the beginning of the line to uncomment it.
curl.cainfo = "C:\wamp\bin\php\yourPHPVersionHere\extras\ssl\cacert.pem"
Make sure the php.ini
file you edit is the one that the web server uses and not the one for the command line. At least in WAMP there are two of them. In WAMP, this php.ini
is normally located at C:\wamp\bin\apache\apacheVersionHere\bin
.
Save the file and restart Apache.
- When you attempt to authenticate, you receive an error saying something like there’s an URL mismatch or some voodoo like that
This is likely to happen when the hostname you use for development is different to the hostname of the live site. You can use the same host for both development and production to avoid this. But if you don’t, make sure to update the callback URLs on each provider. For example, for the tutorial I used a local site named http://social-login.dev
. But if I wanted to deploy this site I would probably use a different hostname. Maybe something like http://backendtime-social-demo.com
. So I should go to each provider’s app settings and change the callback URLs so that they start with http://backendtime-social-demo.com
.
In the particular case of Facebook, when you plan to deploy, you first need to make your Facebook app public. Log into https://developers.facebook.com/apps/ and select your app. Once you are in its admin area, go to the section named App Review on the left menu:
On this area there will a section like this:
Just click the switch button and a dialog will appear. Click Confirm and your app will be in live mode.
This is not required for development, like I said. Just don’t forget to do this when you are about to first deploy to production or your users won’t be able to sign in with Facebook.
What do you think of the tutorial? Is there any other Laravel/PHP topic you’d like to learn about? Leave a comment and let me know.