cult3

Adding Social Authentication to a Laravel 4 application Part 3

Jun 09, 2014

Table of contents:

  1. Define the routes
  2. Create the SessionController and stub out the methods
  3. Implementing the create method
  4. Attempt to authenticate in the store method
  5. Delete the session in the destroy method
  6. The process for re-authentication through Twitter
  7. Remove the filter from the callback method
  8. Add the authorise method to the Session controller
  9. Add the uid column to the users table
  10. Add uid to the session in the callback method of Authenticate
  11. Add the uid to the store method of Authenticate
  12. Create a UidValidator for the Registrator service
  13. Add findByUid method to SocialProviderRegistrator
  14. Check to see if the user already exists
  15. Update the user’s tokens
  16. Authenticate and redirect
  17. Check for an invite
  18. Conclusion

Over the last two weeks I’ve looked at adding social authentication to a Laravel application.

First I walked you through how to add social authentication to your application and how you can allow users to authenticate with your application without them ever having to divulge their username and password.

Next I looked at integrating social authentication within your application by creating a dedicated service to handle different types of authentication requests.

Now that we have registered new users within our applications, the final bit of the puzzle is to look at re-authentication when the user attempts to log back in to our application after being logged out.

Define the routes

The first thing I usually like to do when implementing a feature like this is to define the routes that I’m going to need. I find defining the routes forces me to think about the problem from the perspective of the user:

/**
 * Authentication
 *
 * Allow a user to log in and log out of the application
 */
Route::get("login", [
    "uses" => "SessionController@create",
    "as" => "session.create",
]);
Route::get("login/{provider}", [
    "uses" => "SessionController@authorise",
    "as" => "session.authorise",
]);
Route::post("login", [
    "uses" => "SessionController@store",
    "as" => "session.store",
]);
Route::delete("logout", [
    "uses" => "SessionController@destroy",
    "as" => "session.destroy",
]);

The first route is the normal login route for logging in with a username and password. The second route is for re-authenticating using a social authentication provider. The third route is where the normal username and password form will POST to and the fourth route is how the current session will be destroyed. You will notice that I’m calling my controller SessionController. I tend to think of authentication as creating and destroying sessions.

Create the SessionController and stub out the methods

The next thing to do is to make a rough start on the SessionController. I usually stub out the methods first, and then work through them one at a time:

class SessionController extends BaseController
{
    /**
     * Display the form to allow a user to log in
     *
     * @return View
     */
    public function create()
    {
    }

    /**
     * Accept the POST request and create a new session
     *
     * @return Redirect
     */
    public function store()
    {
    }

    /**
     * Authorise an authentication request
     *
     * @return Redirect
     */
    public function authorise($provider)
    {
    }

    /**
     * Destroy an existing session
     *
     * @return Redirect
     */
    public function destroy()
    {
    }
}

Implementing the create method

The first method I will implement is the create() method. This method will basically just display the login form to allow users to authenticate using their username and password. I’ve also included a check to ensure that the user is not currently logged in. Allowing an authenticated user to view the login form would be a bit weird:

/**
 * Display the form to allow a user to log in
 *
 * @return View
 */
public function create()
{
    if (Auth::guest()) {
        return View::make('session.create');
    }

    return Redirect::route('home.index');
}

A really basic login form would be something along these lines:

{{ Form::open(array('route' => 'session.store')) }}

<h1>Sign in</h1>

@if (Session::has('error'))
<div>{{ Session::get('error') }}</div>
@endif

<div>
{{ Form::label('email', 'Email') }}
{{ Form::text('email') }}
</div>

<div>
{{ Form::label('password', 'Password') }}
{{ Form::text('password') }}
</div>

{{ Form::submit('Sign in') }}

{{ Form::close() }}

Attempt to authenticate in the store method

The next method to implement is the store() method which will accept the POST request from the login form:

/**
 * Accept the POST request and create a new session
 *
 * @return Redirect
 */
public function store()
{
    if (Auth::attempt(['email' => Input::get('email'), 'password' => Input::get('password')])) {
        return Redirect::route('home.index');
    }

    return Redirect::route('session.create')
        ->withInput()
        ->with('error', 'Your email or password was incorrect, please try again!');
}

In this method I’m simply using Laravel’s inbuilt Auth service to authenticate the user. If the user has submitted incorrect data we can redirect back with the error property set with a message.

This message will be displayed at the top of the form in this block:

@if (Session::has('error'))
<div>{{ Session::get('error') }}</div>
@endif

Delete the session in the destroy method

Next I will create the destroy method for deleting a session:

/**
 * Destroy an existing session
 *
 * @return Redirect
 */
public function destroy()
{
    Auth::logout();

    return Redirect::route('session.create')->with('message', 'You have successfully logged out!');
}

Again in this method I’m using Laravel’s authentication service to log the user out of the application. I will also redirect to the login form with a message to notify that the session has been successfully deleted.

In the route file you will notice that I’ve specified this method to only accept the DELETE HTTP method. To submit a DELETE request to this method, you would create a form that looks something like this:

{{ Form::open(['route' => ['session.destroy'], 'method' => 'delete']) }}
<button type="submit">Logout</button>
{{ Form::close() }}

The process for re-authentication through Twitter

When a user has signed up for your application through a social authentication provider such as Twitter, that user will not have a password in order to log back in at a later date.

The process for re-authenticating an existing user using a social authentication provider is basically the same process as authenticating a user for the first time.

However, in order to re-authenticate a user, we need to store a unique id when the user first signs up, and then check to see if that id already exists. If that id does exist, we can authenticate the user instead of attempting to create a new user.

Normally this would be a really easy thing to implement because we’ve already got the bulk of the code written from the last couple of weeks. However, because I’m limiting the registration process through invitations, it makes it slightly more tricky.

This is the process I followed to add the option to re-authenticate to Cribbb.

Remove the filter from the callback method

The first thing to do is to remove the filter from the callback() method on the AuthenticateController:

/**
 * Create a new instance of the AuthenticateController
 *
 * @param Cribbb\Authenticators\Manager $manager
 * @param Cribbb\Registrators\SocialProviderRegistrator $registrator
 * @return void
 */
public function __construct(Manager $manager, SocialProviderRegistrator $registrator)
{
    $this->beforeFilter('invite', ['except' => 'callback']);
    $this->manager = $manager;
    $this->registrator = $registrator;
}

I want to reuse the callback() method for re-authentication, so someone logging back in won’t have an invitation in the session. This method doesn’t really need that filter anyway because the user has already been directed to Twitter.

Add the authorise method to the Session controller

Next I will add the authorise() method to the SessionController. As with the AuthenticateContoller, I’m injecting an instance of Cribbb\Authenticators\Manager in through the __construct() method.

/**
 * The Provider Manager instance
 *
 * @param Cribbb\Authenticators\Manager
 */
protected $manager;

/**
 * Create a new instance of the SessionController
 *
 * @param Cribbb\Authenticators\Manager
 * @return void
 */
public function __construct(Manager $manager)
{
    $this->manager = $manager;
}

/**
 * Authorise an authentication request
 *
 * @return Redirect
 */
public function authorise($provider)
{
    try {
        $provider = $this->manager->get($provider);

        $credentials = $provider->getTemporaryCredentials();

        Session::put('credentials', $credentials);
        Session::save();

        return $provider->authorize($credentials);
    } catch(Exception $e) {
        return App::abort(404);
    }
}

As I mentioned above, ideally you would just reuse the authorise() method on the AuthenticateController in this situation. So if you aren’t implementing an invitation system, feel free to do that instead.

Add the uid column to the users table

When we receive the data about the authenticated user from Twitter it will contain a column called uid. We need to save that id into the database in order to recognise when the same user tries to authenticate back into the application.

Add the following column to your users table migration:

$table->string("uid")->nullable();

Add uid to the session in the callback method of Authenticate

Now when a user is successfully redirected back to the callback() method of the AuthenticateController we need to save the uid into the session so that it can be assigned to the new user when they complete their registration:

Session::put("username", $user->nickname);
Session::put("uid", $user->uid);
Session::put("oauth_token", $token->getIdentifier());
Session::put("oauth_token_secret", $token->getSecret());
Session::save();

Add the uid to the store method of Authenticate

Now we can simply add the uid to the $data array in the store() method when we attempt to create a new user:

$data = [
    "username" => Input::get("username"),
    "email" => Input::get("email"),
    "uid" => Session::get("uid"),
    "oauth_token" => Session::get("oauth_token"),
    "oauth_token_secret" => Session::get("oauth_token_secret"),
];

$user = $this->registrator->create($data);

Remember to add the uid to the $fillable array inside your User model!

Create a UidValidator for the Registrator service

When we are authenticating a user via a social authentication provider, it is critical that we save the uid so we can re-authenticate the same user at a later date.

To ensure the uid has been set, we can create a new validator:

<?php namespace Cribbb\Registrators\Validators;

use Cribbb\Validators\Validable;
use Cribbb\Validators\LaravelValidator;

class UidValidator extends LaravelValidator implements Validable
{
    /**
     * Validation rules
     *
     * @var array
     */
    protected $rules = [
        "uid" => "required|unique:users",
    ];
}

This can then be added to the array of validators in the RegistratorsServiceProvider class:

/**
 * Register the CredentialsRegistrator service
 *
 * @return void
 */
public function registerSocialProviderRegistrator()
{
    $this->app->bind('Cribbb\Registrators\SocialProviderRegistrator', function($app) {
        return new SocialProviderRegistrator(
            $this->app->make('Cribbb\Repositories\User\UserRepository'),
            [
            new UsernameValidator($app['validator']),
            new EmailValidator($app['validator']),
            new OauthTokenValidator($app['validator']),
            new UidValidator($app['validator'])
            ]
        );
    });
}

Add findByUid method to SocialProviderRegistrator

During the callback() method of the AuthenticateController, we need a way to check to see if the user is already an existing user or not.

Add a findByUid() method to the SocialProviderRegistrator service:

/**
 * Find a user by their Uid
 *
 * @param string $uid
 * @return Illuminate\Database\Eloquent\Model
 */
public function findByUid($uid)
{
    return $this->userRepository->getBy('uid', $uid)->first();
}

Check to see if the user already exists

In the callback() method of the AuthenticateController we can now check to see if the user already exists:

$auth = $this->registrator->findByUid($user->uid);

if ($auth) {
}

Update the user’s tokens

By re-authenticating with Twitter, the user will have a different set of tokens that will give us access to their Twitter account. We need to update the tokens that we’ve got saved for the user in the database or things could get weird.

$this->registrator->updateUserTokens(
    $auth,
    $token->getIdentifier(),
    $token->getSecret()
);

Add the following method to your SocialProviderRegistrator:

/**
 * Update the user's tokens
 *
 * @param User $user
 * @param string $token
 * @param string $secret
 */
public function updateUsersTokens(User $user, $token, $secret)
{
    $user->oauth_token = $token;
    $user->oauth_token_secret = $secret;
    return $user->save();
}

Authenticate and redirect

And finally, we can authenticate the user and redirect back to the home page to abort the registration process:

Here is the full callback() method:

/**
 * Receive the callback from the authentication provider
 *
 * @return Redirect
 */
public function callback($provider)
{
    try {
        $provider = $this->manager->get($provider);

        $token = $provider->getTokenCredentials(
            Session::get('credentials'),
            Input::get('oauth_token'),
            Input::get('oauth_verifier')
        );

        $user = $provider->getUserDetails($token);

        $auth = $this->registrator->findByUid($user->uid);

        if ($auth) {
            $this->registrator->updateUserTokens($auth, $token->getIdentifier(), $token->getSecret());

            Auth::loginUsingId($auth->id);

            return Redirect::route('home.index');
        }

        Session::put('username', $user->nickname);
        Session::put('uid', $user->uid);
        Session::put('oauth_token', $token->getIdentifier());
        Session::put('oauth_token_secret', $token->getSecret());
        Session::save();

        return Redirect::route('authenticate.register');
    } catch(Exception $e) {
        return App::abort(404);
    }
}

Check for an invite

At this point, users that are re-authenticating will be authenticated and redirected to the correct page.

However, now that the callback() method is used for both registration and reauthentication, we need a way to prevent a new user from just clicking the “login” button to get access to the application:

if (!Session::has("invitition_code")) {
    return Redirect::route("invite.request");
}

Here I’m simply checking for the invitation_code in the session. If the code does not exist, we can just redirect the user to request an invitation.

Conclusion

The final piece of this puzzle is a lot more complicated than it needs to be because we’ve implemented the invitation process. If your application does not have an invitation process then creating the functionality to re-authenticate existing users will be incredibly simple.

When using social authentication to register or re-authenticate users, you can think of it as basically the exact same process. The only difference is, you must check for a uid code to see if the user already exists in your application.

This is a series of posts on building an entire Open Source application called Cribbb. All of the tutorials will be free to web, and all of the code is available on GitHub.

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.