Jun 09, 2014
Table of contents:
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.
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.
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()
{
}
}
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() }}
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
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() }}
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.
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.
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.
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();
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();
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!
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'])
]
);
});
}
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();
}
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) {
}
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();
}
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);
}
}
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.
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.