This title was written for Laravel version 4. If you're looking for material on the latest version of Laravel, then please check out Code Smart.
I know that I normally start chapters with a witty little story, but I'm a bit short on time this week, so here's the plan. If we all work together and use our imagination, we can pretend that I wrote a really funny tale about Ian Landsman, two trained sea lions, a bottle of advocaat and the Rome metro system. Yep. That should do it.
So what are we going to learn about in this chapter? Well we all have secret stuff don't we? Dark secrets that you don't want anyone to know? Oh, just me? Well if you were to have these dark secrets, you would want a secure way of protecting them from prying eyes. In this chapter we are going to learn how we can use Laravel's authentication system to ensure that only authenticated users are able to access our restricted content.
Right then! Let's get started. Before we begin, we are going to need a fresh Laravel project, with the database configured correctly. You remember how to set up the database right? If not, why not have a browse over the database chapter once more?
Before we begin using the authentication system, we need to decide where we will store our sessions. A session is a little text payload with an associated browser cookie that will allow PHP and Laravel to remember your user between requests. Let's take a look inside the app/config/session.php
file for more information.
/*
|--------------------------------------------------------------------------
| Default Session Driver
|--------------------------------------------------------------------------
|
| This option controls the default session "driver" that will be used on
| requests. By default, we will use the lightweight native driver but
| you may specify any of the other wonderful drivers provided here.
|
| Supported: "native", "cookie", "database", "apc",
| "memcached", "redis", "array"
|
*/
'driver' => 'database',
As you can see, Laravel offers us a number of ways to store our session. You can use whichever storage method is most suitable to your application, but for now we are going to be using the 'database' option to store our sessions within a database table.
There are a number of additional configuration options within this config file, including the length of the session, the name of the session cookie, the name of the sessions table and more. We will be leaving all of these settings as their default values for this example, but feel free to have a browse and see what further configuration is available.
Now that we have decided on the database as our method of session storage, we need to create a table to store the session information in. Once again, Laravel swoops down out of the sky in a colourful leotard. Ready and willing to save the day. "Simply use the artisan session:table
command to deploy your session table." says Laravel in a deep and booming manly voice.
"Thanks Laravel!". Let's try it out.
$ php artisan session:table
Migration created successfully!
The artisan session:table
command has created a migration that will create our sesion table within the database. We've already learned about migrations, so let's run the migrate
command to run the generated migration.
$ php artisan migrate
Migrated: 2013_12_07_231153_create_session_table
If we examine our database, we see that our session table has been created. Wonderful! Next we need to create some users that will be used to login to our application.
By default, Laravel ships with a User
Eloquent model with app/models/User.php
. Let's take a look at that.
<?php
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;
class User extends Eloquent implements UserInterface, RemindableInterface {
/**
* The database table used by the model.
*
* @var string
*/
protected $table = 'users';
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = array('password');
/**
* Get the unique identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifier()
{
return $this->getKey();
}
/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword()
{
return $this->password;
}
/**
* Get the e-mail address where password reminders are sent.
*
* @return string
*/
public function getReminderEmail()
{
return $this->email;
}
}
Woah! Lot's of handy stuff in here. Let's take a closer look at each section.
<?php
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;
class User extends Eloquent implements UserInterface, RemindableInterface {
Our User
model implements both UserInterface
and RemindableInterface
. What are these for? Well the UserInterface
let's Laravel know that the model contains all methods needed for it to authenticate with Laravel's own authentication system. The RemindableInterface
allows Laravel to retrieve the users email address or other contact information to allow for the functionality of sending password reminder emails.
We know what the $table
property does from our time with the Eloquent ORM chapter, so what's this next property?
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = array('password');
The fields that are contained within the $hidden
array will be excluded from any output that is generated using the toJson()
and __toString()
methods. We may not want our user's password to display within these results. You are of course welcome to remove this property if you like. I'm going to leave it there.
Let's take a look at the methods that are defined on the model in more detail. We will begin with the getAuthIdentifier()
method.
<?php
/**
* Get the unique identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifier()
{
return $this->getKey();
}
This method allows us to specify a model property that will act as a unique identifier for the user. By default it is set to the primary key using the $this->getKey()
method inherited from the base model, however, we can change this to any unique property that we like. Let's leave it as the key for now.
/**
* Get the password for the user.
*
* @return string
*/
public function getAuthPassword()
{
return $this->password;
}
The getAuthPassword()
method is used to identify the property that is used for the users password within the model. The default value is the password
column, but once again we are free to change this to suit our needs.
Lastly, we have the getReminderEmail()
method.
/**
* Get the e-mail address where password reminders are sent.
*
* @return string
*/
public function getReminderEmail()
{
return $this->email;
}
This method is used to identify the model property that will contain the users email address. This can later be used if we wish to use Laravel's inbuilt password reminder mechanism.
Let's create a migration for our User
model shall we? You remember how to use the Schema builder and migrations system, right? It goes a little something like this...
$ php artisan migrate:make create_user_table
Created Migration: 2013_12_07_233727_create_user_table
Generating optimized class loader
Now let's fill in the generated migration. We will keep this simple with a 'username', 'password' and 'email' field. The password field will be 60 characters long in order to support the full length of our bcrypt encrypted password. More on that later!
<?php
use Illuminate\Database\Migrations\Migration;
class CreateUserTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function($table)
{
$table->increments('id');
$table->string('username', 128)->unique();
$table->string('password', 60);
$table->string('email', 320)->unique();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('users');
}
}
I've also added some unique()
constraints on 'username' and 'email' as we don't wish to have duplicates. Also, it never hurts to add an auto-incremental primary key, and model timestamps. Let's run our migrations again.
$ php artisan migrate
Migrated: 2013_12_07_233727_create_user_table
Now to need to provide a means for adding users to the system. Hurray! We need a form. All web developers like forms... right? We will skip the validation for our registration form in this circumstance to speed up the chapter and approach the feature in isolation, but if you want to implement validation then why not take a look at the Validation chapter and try it? Consider it homework!
Let's start by creating a view for our registration form. We'll keep it really simple. Now, it's true that I do a fair bit of front end development as well as this stuff, so I know it doesn't make sense to wrap our form elements in <p>
tags. However, I don't want to add a style-sheet to this example project, so let's just use them to have our form fields display in a block
style.
Here's our simple form view that I have saved in app/views/create_user_form.blade.php
.
<html>
<head>
<title>Create User</title>
</head>
<body>
<form action="{{ url('user') }}" method="post">
<p><label for="username">Username:</label></p>
<p><input type="text" name="username" placeholder="Username" /></p>
<p><label for="email">Email:</label></p>
<p><input type="text" name="email" placeholder="Email" /></p>
<p><label for="password">Password:</label></p>
<p><input type="password" name="password" placeholder="Password" /></p>
<p><input type="submit" value="Create" /></p>
</form>
</body>
</html>
A work of art, I'm sure you'll agree! I feel comfortable writing HTML myself, so I rarely use the form builder component of Laravel. If you want to, then go ahead. I'm not gonna stop you! Laravel allows us the freedom of being able to make our own decisions, and I encourage you to take advantage of this freedom.
We have created a form with the three fields that our User
model needs, and a 'Create' submit button. I've left out the csrf
token for simplicity, and have used the url()
helper to target the form to our POST /user
route. Let's go ahead and create the required routes.
<?php
// app/routes.php
Route::get('/', function()
{
return View::make('hello');
});
Route::get('/user', function()
{
return View::make('create_user_form');
});
Route::post('/user', function()
{
$user = new User;
$user->username = Input::get('username');
$user->password = Hash::make(Input::get('password'));
$user->email = Input::get('email');
$user->save();
return Response::make('User created! Hurray!');
});
We have left the framework's /
welcome route within our routes.php
file. Next we have added a GET /user
route which will simply be used to show the create new user form.
Finally we have a POST /user
route that creates a new User
instance. Sets the values that we retrieve from our create user form, and persists the user to the database.
Note that we have used the Hash::make()
method to encrypt the users password with the bcrypt
encryption mechanism that Laravel uses for it's authentication system. It's worth noting that you could use Hash::make()
on a value multiple times and receive different string results each time. Don't panic! This is normal behaviour. There's some clever mathematics involved and I assure you that your password can still be authenticated against.
We return a simple text response from the route to let us know that the act was successful.
Great! Go ahead and visit /user
to create a new user. If you haven't pointed your local webserver to the public directory then you might want to use the php artisan serve
command to quickly start the PHP inbuilt webserver.
User created! Hurray!
Finally we can secure our secret content. Oh wait, we're forgetting something. The secret content. Right then... secret content... hmm. Okay, the secret content will be a list of my celebrity crushes. Don't tell my other half! We need to make sure this is really secure.
First let's create a GET /crush
route to show a view.
<?php
// app/routes.php
Route::get('/crush', function()
{
return View::make('crush');
});
Right then, now we need to create the crush view. Let's go ahead and create a HTML list of my celebrity crushes within app/views/crush.blade.php
.
<ul>
<li>Keira Knightley</li>
<li>Emma Watson</li>
</ul>
Here we have two list entries for Keira Knightley and Emma Watson. Two very fine ladies. Well, Emma Watson is fine now, but I don't suggest drooling over her, and then watching the first Harry Potter movie. You will feel VERY bad. Don't do that. Quite a traumatic experience. shudder
Anyway, moving on...
Now that we have our top secret data, we need to protect it. Let's start with the most basic form of authentication. HTTP basic auth. Basic auth is an authentication system that is a collaboration between the browser and the webserver. It will allow us to create an authentication mechanism without the need for a custom login form. It's the perfect way to start.
To enforce authentication within our system we need to protect
a route. We can do this by using a route filter. Let's use the auth.basic
filter to protect our GET /crush
route from prying eyes.
Here's our updated route.
Route::get('/crush', array(
'before' => 'auth.basic',
function()
{
return View::make('crush');
}
));
We have changed the GET /crush
route to provide an array as a second parameter so that we might attach additional information to the route. We include a before
route filter called auth.basic
. This is an inbuilt filter method which you can find within app/filters.php
. Including this filter is all that we need to do to protect this route using HTTP basic authentication.
Let's give it a go. First we need to visit the GET /crush
URL in our browser. Our web browser will display its own HTTP basic login form. This may look different depending on the browser you are using.
By default, the HTTP basic authentication mechanism will use the User
email address as the username. So let's go ahead and enter the email address and password that we entered into the create user form.
We are presented with our list of crushes. This list is now protected with basic auth, and will only be presented if we enter the correct credentials. We will continue to be granted access to this page until our browser's session expires.
How about logging out? Well we can create a GET /logout
route for this purpose. Let's take a closer look.
Route::get('/logout', function()
{
Auth::logout();
return Response::make('You are now logged out. :(');
});
The Auth::logout()
method will destroy out authentication session. If you visit the GET /logout
route and then return to GET /crush
you will notice that the prompt for login is once again presented.
Logging in with our email address is fine, but what if we wish to authenticate using the username
field instead? Sure! No problem. Simply alter the auth.basic
filter within app/filters.php
as follows.
Route::filter('auth.basic', function()
{
return Auth::basic('username');
});
We have supplied a string as the first parameter to the Auth::basic()
method to specify which field to use as the username for the basic login process. In our example we are specifying that HTTP basic auth use the username
field of the User
model.
Let's try again, visit the GET /logout
route to destroy your session. Now visit GET /crush
one more. This time use your username
and password
to login.
Great, once again we see our crushes.
While HTTP basic auth is very quick to get setup with, we do have to rely on the browsers own login form. Unfortunately we have no control over how it appears, and it is a little inflexible. Let's try using Laravel's own auth system and create a login form.
First we will need a form view to enter our login credentials within app/views/login_form.blade.php
.
<html>
<head>
<title>Login</title>
</head>
<body>
<form action="{{ url('login') }}" method="post">
<p><label for="username">Username:</label></p>
<p><input type="text" name="username" placeholder="Username" /></p>
<p><label for="password">Password:</label></p>
<p><input type="password" name="password" placeholder="Password" /></p>
<p><input type="submit" value="Login" /></p>
<p><input type="checkbox" name="remember" /> <label for="remember">Remember me.</label></p>
</form>
</body>
</html>
Here we have our login form. It's very similar to our create user form, only this time we have only the username
and password
fields. The form will target the POST /login
URI. We also have a 'Remember me.' checkbox to state that the password can be remembered for the next login attempt. I'm sure you have seen a system such as this before. You have been using the internet, right? :)
Let's create a route to display our login form. We will call it GET /login
. I know. Creative, right?
<?php
// app/routes.php
Route::get('/login', function()
{
return View::make('login_form');
});
Right then, let's change our super secret GET /crush
route to use the auth
filter instead of auth.basic
. Like this.
<?php
// app/routes.php
Route::get('/crush', array(
'before' => 'auth',
function()
{
return View::make('crush');
}
));
Now let's logout and visit /crush
once more.
What happened there? We were redirected to our newly created login form. Well let's take a look at the auth
filter within app/filters.php
to find out more.
Route::filter('auth', function()
{
if (Auth::guest()) return Redirect::guest('login');
});
Aha, I see. The auth
filter uses the Auth::guest()
method to determine whether the user is a guest, and is not logged into the system. If this is the case then the filter will use the Redirect::guest()
method to serve a Redirect
response to the /login
form.
Woah! That's what we called our login form route? Isn't that convenient? It's almost like I plan this stuff before hand.
Now that we have our login form, we need a route to handle the form's submission. Let's create the POST /login
route.
<?php
// app/routes.php
Route::post('/login', function()
{
$credentials = Input::only('username', 'password');
if (Auth::attempt($credentials)) {
return Redirect::intended('/');
}
return Redirect::to('login');
});
In order to authenticate with the system, we must first build a credentials array. We pull the values for username
and password
from the request data. We don't have to worry about hashing the password, Laravel will do that for us.
It's worth noting that we can add other constraints to the credentials array. For example, we may wish to add 'enabled' => true
to ensure that the account is active.
Once the credentials array has been constructed, we pass it to the Auth::attempt()
method. This method will return a boolean result that is used to indicate if the login attempt was successful or not. If the authentication attempt was successful, then our session will have been created, and we are now logged into the system.
You'll notice the Redirect::intended()
method in the above example. What's all that about? Well, let's imagine that you are trying to find a second hand car online. Why? Well because the new ones all look the same and boring. Anyway, your buddy Dayle sends you a direct link to a car you might be interested in. An old WRX Impreza or something. Well unfortunately the site requires that you login to be able to see cars. You click the link that Dayle sent you and you are directed to the login form.
Once you have logged into the site, you are redirected to the home page. Well that's not useful is it? You wanted to see the car. Now you have to click the link over again? Well the Redirect::intended()
method can be used to avoid this sithation.
The Redirect::intended()
method will redirect our users to the route that they were attempting to access before they hit our authentication filter. Once the authentication process is complete, we send them back to where they wanted to go. The first parameter of the method is a default route, to ensure that our users are delivered to an appropriate page if they have hit the login route directly.
Let's test our newly constructed authentication system shall we? First hit the GET /logout
route to ensure that we are logged out of the system. Next hit the GET /crush
route to be redirected to our login form. Fill and submit the form and hopefully you will arrive at the GET /crush
page. Well done you!
Well then, let's move on to.. oh wait. We forgot the 'Remember me.' functionality! Well I suppose I could go back and edit it, but you know what, let's just fix it here?
Let's attack our POST /login
route once again.
<?php
// app/routes.php
Route::post('/login', function()
{
$credentials = Input::only('username', 'password');
$remember = Input::has('remember');
if (Auth::attempt($credentials, $remember)) {
return Redirect::intended('/');
}
return Redirect::to('login');
});
We've added a line to retrieve a boolean value representing our remember me check-box. Next we pass the $remember
value as a second parameter to the Auth::attempt()
method. Now if we check the box on the login form, it will remember our username and password for next time that we want to login to the site. Right then. We can move on now.
Once we are logged in to the system, it might be useful to be able to identify our currently logged in user. Don't worry! Taylor already thought about this one too. The Auth::user()
method will return an Eloquent ORM instance used to represent the currently logged in user. This means that we can use all of the goodness baked into Eloquent. We can access the user's relationships or perhaps something more simple like its username
property.
$user = Auth::user();
echo $user->username;
Right! Ladies and gentlemen. We have one final trick to share with you today. Have you ever seen a web application that allow for the administrators to 'emulate' another user. "Switch to Derek" for example. No? Well trust me, there are applications like that our there in the wild. Mostly CMS type things.
Anyway, I realize that I'm getting a little tired and my writing is getting a bit weird, so I'm gonna finish on a high note and show you how you can achieve the same functionality in your own applications.
$user = User::find(5);
Auth::login($user);
// Now we're Derek!
Well there we have it! Simply pass an instance of an Eloquent model for the user that we wish to emulate to the Auth::login()
method to login that user. Super useful!
My books are available online for free to encourage learning. However, if you'd like for me to keep writing, then please consider buying a digital copy over at Leanpub.com.
It's available in PDF, ePub, and Kindle format, and contains a bunch of extras that you won't find on the site. I have a full-time job, and I write my books in my spare time. Please consider buying a copy so that I can continue to write new books from the comfort of my sofa!