Most non-basic multi-user applications need some roles and permission levels. If you ever used WordPress, you must have noticed that they have a super admin, admin, editor, author, etc. Simplifying the development and integration of a permission system is what Cartalyst’s Sentinel package is trying to accomplish. The package provides an API for dealing with users, groups, permissions, etc. In this article, we’ll use it to create a small demo app.
Environment Setup
For our sample application in this tutorial, we will be using the Slim micro-framework and Vagrant. You can check out the final demo on Github to see the result. Let’s start by first requiring the necessary packages:
composer require slim/slim:~2.0
composer require twig/twig:~1.*
composer require cartalyst/sentinel:2.0.*
Sentinel suggests installing Illuminate Eloquent, Illuminate Events, Symfony Http Foundation and ircmaxell password-compat so let’s add those to the project.
composer require illuminate/database illuminate/events symfony/http-foundation ircmaxell/password-compat
Because working with users, groups and permissions requires some database interaction, we need to create our database with the necessary tables. If you are using Laravel you can install the database using the migrate command.
php artisan vendor:publish --provider="Cartalyst\Sentinel\Laravel\SentinelServiceProvider"
php artisan migrate
If not, you’ll have to do it manually using the following steps. You can read more about database installation in the documentation.
- Open the
vendor/cartalyst/sentinel/schema/mysql.sql
, adduse DATABASENAME;
at the top of the file and save. - Feed your file to MySQL using the command
mysql -u root -p < vendor/cartalyst/sentinel/schema/mysql.sql
and enter the password if necessary, or just import the file using your favorite SQL editor / inspector (like PhpMyAdmin, Sequel Pro, etc).
Finally, let’s bootstrap our app with a public/index.php
file:
<?php
require_once __DIR__.'/../vendor/autoload.php';
$app = new \Slim\Slim();
//register bindings
include_once __DIR__.'/../app/bootstrap/container.php';
include_once __DIR__.'/../app/routes.php';
$app->run();
Container Bindings
Inside our app/bootstrap/container.php
we will define our container bindings.
$app->container->twigLoader = new Twig_Loader_Filesystem(__DIR__.'/../views');
$app->container->twig = new Twig_Environment($app->container->twigLoader, array(
'cache' => false,
));
The $app
variable is a Slim
container. Since we chose to use Eloquent for our database interactions we need to specify the connection configuration.
// app/bootstrap/container.php
$capsule = new \Illuminate\Database\Capsule\Manager();
$capsule->addConnection([
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'capsule',
'username' => 'root',
'password' => 'root',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
]);
$capsule->bootEloquent();
After booting Eloquent, the only part left is to bind Sentinel to the container.
// app/bootstrap/container.php
$app->container->sentinel = (new \Cartalyst\Sentinel\Native\Facades\Sentinel())->getSentinel();
If you prefer to use static calls, you can use it directly. (Sentinel::authenticate($credentials)
)
Creating Roles
The first step is to define our permission groups. We have the admin
role which gives the user the permission to create, update and delete users. On the other hand, the user
role only gives the permission to update users.
$app->container->sentinel->getRoleRepository()->createModel()->create(array(
'name' => 'Admin',
'slug' => 'admin',
'permissions' => array(
'user.create' => true,
'user.update' => true,
'user.delete' => true
),
));
$app->container->sentinel->getRoleRepository()->createModel()->create(array(
'name' => 'User',
'slug' => 'user',
'permissions' => array(
'user.update' => true
),
));
This code is temporary, and can be placed at the bottom of index.php
, run once, and then removed. Its only purpose is to create the roles in the database.
Sign up Page
We have two options when creating the sign up page. We can either use email confirmation, or directly set the user as validated.
// app/routes.php
$app->get('/', function () use ($app) {
$app->twig->display('home.html.twig');
});
Our home page contains a form and a set of fields for user details (first name, last name, email, password and password confirmation). Check the source code of the view files here.
Then, let’s define the POST
home route.
// app/routes.php
$app->post('/', function () use ($app) {
// we leave validation for another time
$data = $app->request->post();
$role = $app->container->sentinel->findRoleByName('User');
if ($app->container->sentinel->findByCredentials([
'login' => $data['email'],
])) {
echo 'User already exists with this email.';
return;
}
$user = $app->container->sentinel->create([
'first_name' => $data['firstname'],
'last_name' => $data['lastname'],
'email' => $data['email'],
'password' => $data['password'],
'permissions' => [
'user.delete' => 0,
],
]);
// attach the user to the role
$role->users()->attach($user);
// create a new activation for the registered user
$activation = (new Cartalyst\Sentinel\Activations\IlluminateActivationRepository)->create($user);
mail($data['email'], "Activate your account", "Click on the link below \n <a href='http://vaprobash.dev/user/activate?code={$activation->code}&login={$user->id}'>Activate your account</a>");
echo "Please check your email to complete your account registration.";
});
After getting the request data and resolving Sentinel from the container, we select the role that we want to assign to the new user. You can query roles by name, slug or by ID. More about that in the documentation. Next, we test to see if the new user is already registered, then we create a new user using the request data, assign it the User
role and we create a new activation. The activation process is totally optional, we will talk more about it later. We skipped the validation process here to keep the function simple.
Permissions
The User
role gives users the permission to update. But, what if we wanted to add or override some permissions?
$user = $app->container->sentinel->create([
'first_name' => $data['firstname'],
'last_name' => $data['lastname'],
'email' => $data['email'],
'password' => $data['password'],
'permissions' => [
'user.update' => false,
'user.create' => true
],
]);
Sentinel has two methods for working with permissions: standard or strict. For the standard method, you can give the user the ability to create new users directly ('user.create': true
), but when you want to deny a permission to the role, you just need to give it false
on the user’s permissions. The strict method will negate the permission if is set to false
anywhere. You can read more about permissions in the documentation.
User Activation
After saving the user to the database, you’ll need to send the activation link to the user’s email.
We have two methods to implement the activation process. The first will send the code and the user ID via email, and the second one will only send the activation code.
// app/routes.php
$app->post('/', function () use ($app) {
//...
$activation = (new Cartalyst\Sentinel\Activations\IlluminateActivationRepository)->create($user);
mail($data['email'], "Activate your account", "Click on the link below \n <a href='http://vaprobash.dev/user/activate?code={$activation->code}&login={$user->id}'>Activate your account</a>");
echo "Please check your email to complete your account registration.";
});
The create
method on the IlluminateActivationRepository
will return a database record containing the activation code. Now, we need to create a new route to catch the account validation request.
// app/routes.php
$app->get('/user/activate', function () use ($app) {
$code = $app->request->get('code');
$login = $app->request->get('login');
$activation = new Cartalyst\Sentinel\Activations\IlluminateActivationRepository;
if (!($user = $app->sentinel->findById($login)))
{
echo "User not found!";
return;
}
if (!$activation->complete($user, $code))
{
if ($activation->completed($user))
{
echo 'User is already activated. Try to log in.';
return;
}
echo "Activation error!";
return;
}
echo 'Your account has been activated. Log into your account.';
return;
});
The first step is to test whether or not the user exists. Then, we attempt to complete the activation. In case of error, we check to see if the user has been already activated.
// app/routes.php
$app->get('/user/activate', function () use ($app) {
$code = $app->request->get('code');
$activationRepository = new Cartalyst\Sentinel\Activations\IlluminateActivationRepository;
$activation = Cartalyst\Sentinel\Activations\EloquentActivation::where("code", $code)->first();
if (!$activation)
{
echo "Activation error!";
return;
}
$user = $app->container->sentinel->findById($activation->user_id);
if (!$user)
{
echo "User not found!";
return;
}
if (!$activationRepository->complete($user, $code))
{
if ($activationRepository->completed($user))
{
echo 'User is already activated. Try to log in.';
return;
}
echo "Activation error!";
return;
}
echo 'Your account has been activated. Log in to your account.';
return;
});
The first step is to retrieve the activation record using the code
parameter, then we grab the user using the user ID from the activation record, and the last part is similar to the first method. You can read more about activation in the documentation.
I prefer using the second method for the activation process and I always avoid sending unnecessary details to the user.
Login Page
The login page contains an email field, password, and the user can choose to be remembered.
// app/routes.php
$app->get('/login', function () use ($app) {
$app->twig->display('login.html.twig');
});
After getting the user email and password we can pass them to the authenticate
method. It may throw an exception depending on the error (ThrottlingException
, NotActivatedException
, etc). You can read more about the authenticate
method in the documentation. However, you may have a remember me
field to automatically log the user in the future. The authenticateAndRemember
method is similar to the authenticate
method and sets the remember me parameter to true
by default.
// app/routes.php
$app->post('/login', function () use ($app) {
$data = $app->request->post();
$remember = isset($data['remember']) && $data['remember'] == 'on' ? true : false;
try {
if (!$app->container->sentinel->authenticate([
'email' => $data['email'],
'password' => $data['password'],
], $remember)) {
echo 'Invalid email or password.';
return;
} else {
echo "You're logged in";
return;
}
} catch (Cartalyst\Sentinel\Checkpoints\ThrottlingException $ex) {
echo "Too many attempts!";
return;
} catch (Cartalyst\Sentinel\Checkpoints\NotActivatedException $ex){
echo "Please activate your account before trying to log in";
return;
}
});
The authenticate
method will return the user on success or false on failure, but the important part is that it can throw an exception depending on the enabled checkpoints, which are like middleware that intercept the login process and decide if you can continue or not. In this case, we may have a ThrottlingException
in case of abusive login attempts or a NotActivatedException
if the user has not been activated yet. You can configure the enabled checkpoints inside vendor/cartalyst/sentinel/src/config
in the checkpoints
section.
Logout
Logging out the current user is really straightforward.
// app/routes.php
$app->get('/logout', function () use ($app) {
$app->container->sentinel->logout();
echo 'Logged out successfully.';
});
Admin Page
Our admin page doesn’t contain any view content. We are just going to test if the user is connected, get the logged in user and then test if he has the required permissions to access the page.
// app/routes.php
$app->get('/admin/users', function () use ($app) {
$loggedUser = $app->container->sentinel->check();
if (!$loggedUser) {
echo 'You need to be logged in to access this page.';
return;
}
if (!$loggedUser->hasAccess('user.*')) {
echo "You don't have the permission to access this page.";
return;
}
echo 'Welcome to the admin page.';
});
The check
method returns the logged in user or false
. If we have a user, we call the hasAccess
method with the required permissions. The *
alias selects any permission. You can use user.update
or user.delete
depending on the required permission to access the resource.
Wrapping Up
The Sentinel package takes out the pain of user permission and role management. It also offers the ability to add in password reminders, password hashers, etc. Be sure to check the documentation for more details, look at the final product on Github, and if you have any questions or comments, don’t hesitate to post them below.
Frequently Asked Questions (FAQs) about User Authorization with Sentinel
How do I install Sentinel in Laravel?
To install Sentinel in Laravel, you need to follow these steps:
1. First, you need to install Laravel via Composer. If you haven’t installed Composer, you can download it from the official website.
2. Once Laravel is installed, you can install Sentinel via Composer by running the following command in your terminal: composer require cartalyst/sentinel "2.0.*"
3. After the installation, you need to publish the configuration file by running the command: php artisan vendor:publish --provider="Cartalyst\Sentinel\Laravel\SentinelServiceProvider"
4. Finally, you need to run the migrations by executing the command: php artisan migrate
Remember to replace “2.0.*” with the version of Sentinel you want to install.
How do I use Sentinel for user authentication?
Sentinel provides a simple and easy-to-use API for user authentication. Here’s a basic example of how you can use Sentinel for user authentication:
1. First, you need to include the Sentinel class in your controller: use Cartalyst\Sentinel\Laravel\Facades\Sentinel;
2. Then, you can authenticate a user by calling the authenticate
method and passing an array with the user’s credentials:$credentials = [
'email' => 'john.doe@example.com',
'password' => 'password',
];
$user = Sentinel::authenticate($credentials);
If the credentials are valid, the authenticate
method will return the authenticated user. Otherwise, it will return false
.
How do I handle user roles and permissions with Sentinel?
Sentinel provides a powerful and flexible system for handling user roles and permissions. You can create roles and assign permissions to them, and then assign these roles to users.
To create a role, you can use the Role
facade’s create
method:$role = Sentinel::getRoleRepository()->createModel()->create([
'name' => 'Subscriber',
'slug' => 'subscriber',
]);
To assign a role to a user, you can use the assignRole
method:$user = Sentinel::findById(1);
$role = Sentinel::findRoleByName('Subscriber');
$role->users()->attach($user);
To check if a user has a certain role, you can use the inRole
method:if ($user->inRole('subscriber')) {
// The user has the 'subscriber' role
}
To assign permissions to a role, you can use the permissions
property:$role->permissions = [
'user.create' => true,
'user.update' => false,
'user.delete' => false,
];
$role->save();
To check if a user has a certain permission, you can use the hasAccess
method:if ($user->hasAccess('user.create')) {
// The user has the 'user.create' permission
}
How do I handle user activation with Sentinel?
Sentinel provides a simple API for handling user activation. When a user registers, Sentinel can automatically create an activation code for the user. You can then send this activation code to the user’s email address, and the user can activate their account by clicking on a link in the email.
Here’s a basic example of how you can create an activation code for a user:$user = Sentinel::register([
'email' => 'john.doe@example.com',
'password' => 'password',
]);
$activation = Activation::create($user);
To activate a user, you can use the complete
method and pass the user and the activation code:$activation = Activation::complete($user, $code);
If the activation is successful, the complete
method will return true
. Otherwise, it will return false
.
How do I handle user reminders with Sentinel?
Sentinel provides a simple API for handling user reminders, such as password reset reminders. You can create a reminder for a user, and then send the reminder code to the user’s email address. The user can then reset their password by entering the reminder code.
Here’s a basic example of how you can create a reminder for a user:$user = Sentinel::findByCredentials([
'login' => 'john.doe@example.com',
]);
$reminder = Reminder::create($user);
To complete a reminder, you can use the complete
method and pass the user, the reminder code, and the new password:$reminder = Reminder::complete($user, $code, $newPassword);
If the reminder is completed successfully, the complete
method will return true
. Otherwise, it will return false
.
How do I handle user throttling with Sentinel?
Sentinel provides a simple API for handling user throttling, which can help protect your application against brute-force attacks. You can configure the throttling settings in the Sentinel configuration file.
Here’s a basic example of how you can check if a user is throttled:$user = Sentinel::findByCredentials([
'login' => 'john.doe@example.com',
]);
$throttle = Throttle::findByUser($user);
if ($throttle->isBanned()) {
// The user is banned
}
if ($throttle->isSuspended()) {
// The user is suspended
}
To ban a user, you can use the ban
method:$throttle->ban();
To unban a user, you can use the unban
method:$throttle->unban();
To suspend a user, you can use the suspend
method and pass the suspension time in minutes:$throttle->suspend(15);
To unsuspend a user, you can use the unsuspend
method:$throttle->unsuspend();
How do I handle user events with Sentinel?
Sentinel provides a simple API for handling user events, such as login, logout, registration, activation, reminder, and throttling events. You can listen for these events and execute custom code when they occur.
Here’s a basic example of how you can listen for the sentinel.user.registered
event:Event::listen('sentinel.user.registered', function ($user, $activation) {
// Send the activation code to the user's email address
});
You can also listen for multiple events at once:Event::listen(['sentinel.user.registered', 'sentinel.user.activated'], function ($user) {
// Send a welcome email to the user
});
How do I customize the Sentinel configuration?
You can customize the Sentinel configuration by editing the config/cartalyst.sentinel.php
file. This file contains various configuration options for Sentinel, such as the models used by Sentinel, the hashing strategy, the session and cookie settings, the activation settings, the reminder settings, and the throttling settings.
Here’s a basic example of how you can change the hashing strategy:'hashing' => [
'strategy' => 'bcrypt',
'bcrypt' => [
'rounds' => 10,
],
'native' => [
'salt' => 'abc123',
],
'argon2' => [
'memory' => 2048,
'threads' => 2,
'time' => 2,
],
],
Remember to clear the configuration cache by running the php artisan config:clear
command after making changes to the configuration file.
How do I use Sentinel with multiple authentication systems?
Sentinel provides a simple API for handling multiple authentication systems. You can create multiple instances of Sentinel, each with its own configuration, and use them independently.
Here’s a basic example of how you can create a new instance of Sentinel:$sentinel = new Sentinel(
new IlluminateSession($app['session.store']),
new IlluminateCookie($app['request'], $app['cookie']),
new EloquentUser(),
new EloquentRole(),
new EloquentActivation(),
new EloquentReminder(),
new EloquentThrottle(),
$app['request']->getClientIp()
);
You can then use this instance to authenticate users, handle roles and permissions, handle activations and reminders, and handle throttling.
How do I extend Sentinel with custom functionality?
Sentinel is built on top of the Laravel framework, which means you can easily extend it with custom functionality. You can create your own models, repositories, and services, and use them with Sentinel.
Here’s a basic example of how you can create a custom user model:use Cartalyst\Sentinel\Users\EloquentUser as SentinelUser;
class User extends SentinelUser
{
// Add your custom methods here
}
You can then tell Sentinel to use your custom user model by changing the users.model
configuration option:'users' => [
'model' => 'App\User',
],
Remember to clear the configuration cache by running the php artisan config:clear
command after making changes to the configuration file.
Younes is a freelance web developer, technical writer and a blogger from Morocco. He's worked with JAVA, J2EE, JavaScript, etc., but his language of choice is PHP. You can learn more about him on his website.