Add OAuth to Sentinel 2

by   PHP Laravel Sentinel


Thursday, 12 May 2016



Link to Github repo

This post goes about the integration of the Laravel Socialite package to Sentinel 2 for OAuth Authentication. It allows visitors to register and sign in to your application via their own Google, Facebook, LinkedIn or Github account. This is kind of a long article because I will go through the whole installation process, but the code in itself isn't that sophisticated at all, so don't get intimidated by the tiny vertical scroll bar you see on the right side!

sign in page

Context

I assume you have a running Laravel app with Sentinel installed. To get a gist of the various steps needed to get up and running with Laravel socialite (untied from Sentinel), have a look at this external blog post. If you'd like getting to the same starting point as me, I suggest you install this fresh Sentinel repo. Sentinel is an open-source authorization and authentication framework that has all the tools in place for managing an authorization-based application. However, it doesn't come with OAuth functionality natively and instead propose an add-on package called Sentinel-Social. Now let me be clear on that: I strongly encourage anyone who considers building a serious web app to get an official package release with all the increased security it brings. That being said, it often helps to get around things by yourself in order to gain a good understanding of how it all works out.

Please note:
  • indicates that you can safely copy the entire code block to your local file.
  • indicates that this is an excerpt of a longer code block, so copy only what's inside the -- marks and place it to its due location in your local file.

Install Socialite

First get Socialite installed. In your project's main directory run

composer require laravel/socialite

And update your providers and aliases arrays

config/app.php

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

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

Register your app

google developer

We then need to register our application to the proper authentication provider. Let's do so with Google as an example, but the process is similar for other providers. Go to the Google Developers website and follow the guidelines to create a new project1. You should provide info like your homepage url (can be http://localhost/[your_app] if you work locally) and most importantly your callback url. This url may look like this: http://localhost/[your_app]/login/callback/google, but you are free to opt for something more eccentric as long as you remain consistent throughout the whole configuration process. For each provider, Socialite will have you reporting 3 different values to the config/services page:

  1. Your callback url
  2. Your Client ID
  3. Your Client Secret

Of course, for security reasons, we are not going to paste those values directly there. Instead, we will put reference to environment variables and update our .env file.

config/services.php

'google' => [
    'client_id' => getenv('GOOGLE_CLIENT_ID'),
    'client_secret' => getenv('GOOGLE_CLIENT_SECRET'),
    'redirect' => getenv('GOOGLE_URL'),
],
'facebook' => [
    'client_id' => getenv('FACEBOOK_CLIENT_ID'),
    'client_secret' => getenv('FACEBOOK_CLIENT_SECRET'),
    'redirect' => getenv('FACEBOOK_URL'),
],
'linkedin' => [
    'client_id' => getenv('LINKEDIN_CLIENT_ID'),
    'client_secret' => getenv('LINKEDIN_CLIENT_SECRET'),
    'redirect' => getenv('LINKEDIN_URL'),
],
'github' => [
    'client_id' => getenv('GITHUB_CLIENT_ID'),
    'client_secret' => getenv('GITHUB_CLIENT_SECRET'),
    'redirect' => getenv('GITHUB_URL'),
]

.env

GOOGLE_CLIENT_ID=[YOUR_GOOGLE_CLIENT_ID]
GOOGLE_CLIENT_SECRET=[YOUR_GOOGLE_CLIENT_SECRET]
GOOGLE_URL=[YOUR_GOOGLE_CALLBACK_URL]

FACEBOOK_CLIENT_ID=[YOUR_FACEBOOK_CLIENT_ID]
FACEBOOK_CLIENT_SECRET=[YOUR_FACEBOOK_CLIENT_SECRET]
FACEBOOK_URL=[YOUR_FACEBOOK_CALLBACK_URL]

LINKEDIN_CLIENT_ID=[YOUR_LINKEDIN_CLIENT_ID]
LINKEDIN_CLIENT_SECRET=[YOUR_LINKEDIN_CLIENT_SECRET]
LINKEDIN_URL=[YOUR_LINKEDIN_CALLBACK_URL]

GITHUB_CLIENT_ID=[YOUR_GITHUB_CLIENT_ID]
GITHUB_CLIENT_SECRET=[YOUR_GITUB_CLIENT_SECRET]
GITHUB_URL=[YOUR_GITHUB_CALLBACK_URL]

Update the database

Next step is to update our database. Upon successful connection, we will retrieve a whole bunch of user-specific data from the authentication provider (except user password naturally), but we'll only keep the id attributed to the user by the authentication provider. This value will serve as reference for future connections (i.e. so that we know whether the user has already registred).

So run

php artisan make:migration --table users alter_users_table_add_oauth_id

to update users table and add these fields in the newly created migration.

2016_01_21_210953_alter_users_table_add_oauth_id

public function up()
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('google_id');
        $table->string('facebook_id');
        $table->string('linkedin_id');
        $table->string('github_id');
        $table->string('avatar');
    });
}

public function down()
{
    Schema::table('users', function (Blueprint $table) {
        $table->dropColumn('google_id');
        $table->dropColumn('facebook_id');
        $table->dropColumn('linkedin_id');
        $table->dropColumn('github_id');
        $table->dropColumn('avatar');
    });
}

Finally run php artisan migrate. Note that we also register users avatar.

Define Routes

Let's add routes for redirecting users to the OAuth provider and retrieve the callback.

Routes.php

Route::get('login/{provider}', array('as' => 'redirect', 'uses' => 'AuthController@redirectToProvider'));
Route::get('login/callback/{provider}', array('as' => 'callback', 'uses' => 'AuthController@handleProviderCallback'));

Following the first route, we provide a redirection link in the login form. It can be a clickable image for convenience.

login.blade.php

<a href="login/google"><img src="images/oauth/google.png" alt="" width="150"></a>

Do the same for all providers you'd like to add.

Build the controller

Routes are calling the AuthController class which consists of 3 methods:

  1. The redirectToProvider method will use the Socialite facade to redirect user to the proper auth provider.
  2. The handleProviderCallback method will try authenticate user by calling the findOrCreateUser method.
  3. The findOrCreateUser method checks if user exists and whether she has already used this provider in the past, performing registration if not.

touch AuthController.php

AuthController.php

<?php namespace App\Http\Controllers;

use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Cartalyst\Sentinel\Checkpoints\NotActivatedException;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use App\Http\Controllers\Controller;
use App\User;

use Socialite;
use Validator;
use Redirect;
use Response;
use Sentinel;
use Session;

class AuthController extends Controller
{
    public function redirectToProvider($provider=null)
    {
        if(!config("services.$provider")) Response::make("Not Found", 404); //just to handle providers that doesn't exist
        return Socialite::driver($provider)->redirect();
    }

    /* 4 cases:
        1.  User register for the first time with Oauth
        2.  User sign in with Oauth, but is already registered
        3.  User sign in with Oauth, but has already used another Oauth service
        4.  User sign with an already used Oauth service
    */
    public function handleProviderCallback($provider=null)
    {
        try {
            $user = Socialite::driver($provider)->user();
        } catch (Exception $e) { // Cannot retrieve OAuth user data
            return Redirect::to('login');
        }

        $password = str_random(10);
        $OAuthUser = $this->findOrCreateUser($user, $provider, $password);

        try {
            $user = Sentinel::findById($OAuthUser->id);

            if (Sentinel::authenticateOauth($user)) {
                Sentinel::login($user);
                return Redirect::route('home')->with('success', 'Welcome <b>' . $user->email . '!</b>');
            } else {
                return Redirect::route('login')->with('error', 'Cannot authenticate.');
            }
        } catch (NotActivatedException $e) {

            return Redirect::route('login')->with('error', $e->getMessage());
        }
    }


    private function findOrCreateUser($user, $provider, $password)
    {
        if ($userExist = User::where('email', '=', $user->email)->first()) {
            if ($userProvider = User::where($provider . '_id', '=', $user->id)->first()) { // User is already registered with this oauth service
                return $userProvider;
            } else { // User exists but has never used this service provider before
                // Update user with new provider_id
                $new_provider = $provider . '_id';
                $userExist->$new_provider = $user->id;
                $userExist->save();  

                return $userExist;                
            }
        } else {// Register and activate new user and proceed to authentication. Return password.
            $credentials = [
                'email' => $user->email,
                'password' => $password,
                $provider . '_id' => $user->id,
                'avatar' => $user->avatar
            ];

            $user = Sentinel::register($credentials, true);
            if ($user) {
                $role = Sentinel::findRoleBySlug('user');

                $role->users()->attach($user);
            }

            Session::flash('warning', "You successfully signed in via OAuth <span class='fa fa-smile-o'></span>.<br/>Your default attributed password: <b>$password</b><br/>Take a note of your password now, as you won't be able to access it anymore. You can always sign in with your favorite OAuth service tough.");
            return $user;
        }
    }
}

Define authenticate method

For the case when the user is new to the site and has first to be registered, we perform a registration process by calling a new method called authenticateOauth in the Sentinel file, located inside the vendor/cartalyst/sentinel/src folder.

Sentinel.php

public function authenticateOauth($user, $remember = false, $login = true)
{
    $response = $this->fireEvent('sentinel.authenticating', $user, true);

    if ($response === false) {
        return false;
    }

    if (! $this->cycleCheckpoints('login', $user)) {
        return false;
    }

    if ($login === true) {
        $method = $remember === true ? 'loginAndRemember' : 'login';

        if (! $user = $this->{$method}($user)) {
            return false;
        }
    }

    $this->fireEvent('sentinel.authenticated', $user);

    return $this->user = $user;
}

End Result

There you are. Go to the login page, click the Google link, enter your credentials and you should be redirected to the homepage as an authauticated user with your new account freshly created.

successfull login


  1. Developer links for the 3 other providers: Facebook, Linkedin, Github ↩︎

Blog Search

About

Hi there! My name is Jean-Marc Kleger and I'm a web developer. Welcome to my blog where a share some tips on how to deal with a selection of challenges encountered in my day-to-day coding workflow. Most articles are related to their own Github repo so that you can quickly experiment the code. Don't hesitate also to make use of the comment section at the end of each post to share your knowledge on the topic. Enjoy the visit!