Skip to content

Instantly share code, notes, and snippets.

@leabdalla
Last active February 10, 2024 10:57
Show Gist options
  • Save leabdalla/5999421 to your computer and use it in GitHub Desktop.
Save leabdalla/5999421 to your computer and use it in GitHub Desktop.
Laravel Sentry 2 with multiple user types, finally.

Sentry is an amazing auth system. But I really need a feature: multiple user types in the same app. And I cannot separate those in groups, because they have different table columns. After 2 days burning my head, I think I found a good solution. The magic is duplicate SentryServiceProvider with new different settings.

This was tested on Laravel 4.0 and Sentry 2. If you're using other version of Sentry, my suggestion is to follow same steps from this gist but use your local files instead copying files from here.

Lets suppose we have a fresh Sentry install with default User ambient. Now we want another ambient called Admin, with new model and different settings. How to do:

1. One model, one ambient

Let's create one model called Admin for our new ambient. Just create an empty model and extend it to the Sentry user.

<?php
namespace App\Models;

use Eloquent;
use Illuminate\Auth\UserInterface;
use Illuminate\Auth\Reminders\RemindableInterface;

class Admin extends \Cartalyst\Sentry\Users\Eloquent\User implements UserInterface, RemindableInterface {

	protected $table = 'admins';

	protected $guarded = array(
	);

	protected $fillable = array(
	);

	public static $rules = array(
	);

	/**
	 * sentry methods
	 */
	public function groups(){ return $this->belongsToMany('Cartalyst\Sentry\Groups\Eloquent\Group', 'admins_groups'); }
	public function getAuthIdentifier(){ return $this->getKey(); }
	public function getAuthPassword(){ return $this->password; }
	public function getReminderEmail(){ return $this->email; }

}

2. Duplicate Sentry ServiceProvider.

Create a folder called app/lib and add in composer autoload. In this folder, create this structure:

/app/lib
    /SentryMods
        SentryAdmin.php
        SentryAdminServiceProvider.php

Now we copy contents from original Sentry files just changing some sentry.admin names.

#####SentryAdmin.php

<?php namespace Lib\SentryMods;
use Illuminate\Support\Facades\Facade;
class SentryAdmin extends Facade {
    protected static function getFacadeAccessor(){ return 'sentry.admin'; }
}

#####SentryAdminServiceProvider.php

// check attached file to this gist

3. Set configurations for the new ambient

In the file app/config/cartalyst/sentry/config.php, add this lines. Here is the magic. Here we can choose settings for the new sentry provider. You can change anything, but I've tested only changing the user model.

	'admin'=>array(
		'driver' => 'eloquent',
		'hasher' => 'native',
		'groups' => array(
			'model' => 'Cartalyst\Sentry\Groups\Eloquent\Group',
		),
		'cookie' => array(
			'key' => 'cartalyst_sentry',
	 	),
		'users' => array(
			'model' => 'App\Models\Admin',
			'login_attribute' => 'login',
		),
		'throttling' => array(
			'enabled' => true,
			'model' => 'Cartalyst\Sentry\Throttling\Eloquent\Throttle',
			'attempt_limit' => 5,
			'suspension_time' => 15,
		),
	),

4. Prepare database

I created a migration to run after the migrate of sentry package. This is called sentrymods and copies original tables for the new ambient. This migration can resolves too some adjustments for sentry tables, like adding new columns.

public function up()
{
	DB::statement('CREATE TABLE admins LIKE users');
	DB::statement('CREATE TABLE admins_groups LIKE users_groups');

	Schema::table('admins', function($table){
		$table->string('login')->nullable();
		$table->softDeletes();
	});

	Schema::table('admins_groups', function($table){
		$table->renameColumn('user_id', 'admin_id');
	});
}

5. Lets see this working.

Now you have two Sentry ambients working. Then, just use SentryAdmin when you want :) Try this little test:

print_r(Sentry::getUserProvider());
print_r(SentryAdmin::getUserProvider());

The new one prints the default sentry. It will show the default settings. The second will shows our new sentry ambient with different settings. Something like:

// Cartalyst\Sentry\Users\Eloquent\Provider
// Object (
//     [model:protected] => App\Models\Admin
//     [hasher:protected] => Cartalyst\Sentry\Hashing\NativeHasher
// )

Want a seeder file? try this

<?php

use App\Models\User;
use Lib\SentryMods\SentryAdmin;
use Lib\SentryMods\SentryAdminServiceProvider;

class SentryTableSeeder extends Seeder {

	public function run()
	{
		$group_admin = SentryAdmin::getGroupProvider()->create(array('name'=>'Admins', 'permissions'=>array('admin'=>1)));

		$user = SentryAdmin::getUserProvider()->create(array(
			'email'		=> 'admin@admin.com',
			'login'		=> 'admin',
			'password'	=> 'admin',
			'first_name'=>'Leandro', 'last_name'=>'Abdalla', 'activated'=>1,
		));
		$user->addGroup($group_admin);
	}

}
````

**More than 2 ambients?** Just follow the same steps :)

**Remember that you can use this approach to customize the Group class too.**

#### Better solution?
Let me know! I'm including this feature in many projects!

Thanks for Ben of Cartalyst staff with the initial help here: http://help.cartalyst.com/discussions/questions/110-how-to-use-multiple-configurations-of-sentry-in-the-same-laravel-4-app-one-for-public-and-one-for-admin-area
<?php namespace Lib\SentryMods;
use Cartalyst\Sentry\Cookies\IlluminateCookie;
use Cartalyst\Sentry\Groups\Eloquent\Provider as GroupProvider;
use Cartalyst\Sentry\Hashing\BcryptHasher;
use Cartalyst\Sentry\Hashing\NativeHasher;
use Cartalyst\Sentry\Hashing\Sha256Hasher;
use Cartalyst\Sentry\Sentry;
use Cartalyst\Sentry\Sessions\IlluminateSession;
use Cartalyst\Sentry\Throttling\Eloquent\Provider as ThrottleProvider;
use Cartalyst\Sentry\Users\Eloquent\Provider as UserProvider;
use Illuminate\Support\ServiceProvider;
class SentryAdminServiceProvider extends ServiceProvider {
/**
* Boot the service provider.
*
* @return void
*/
public function boot(){
$this->observeEvents();
}
/**
* Register the service provider.
*
* @return void
*/
public function register(){
$this->registerHasher();
$this->registerUserProvider();
$this->registerGroupProvider();
$this->registerThrottleProvider();
$this->registerSession();
$this->registerCookie();
$this->registerSentry();
}
/**
* Register the hasher used by Sentry.
*
* @return void
*/
protected function registerHasher(){
$this->app['sentry.admin.hasher'] = $this->app->share(function($app){
$hasher = $app['config']['cartalyst/sentry::config.admin.hasher'];
switch ($hasher){
case 'native':
return new NativeHasher;
break;
case 'bcrypt':
return new BcryptHasher;
break;
case 'sha256':
return new Sha256Hasher;
break;
}
throw new \InvalidArgumentException("Invalid hasher [$hasher] chosen for Sentry.");
});
}
/**
* Register the user provider used by Sentry.
*
* @return void
*/
protected function registerUserProvider(){
$this->app['sentry.admin.user'] = $this->app->share(function($app){
$model = $app['config']['cartalyst/sentry::config.admin.users.model'];
// We will never be accessing a user in Sentry without accessing
// the user provider first. So, we can lazily setup our user
// model's login attribute here. If you are manually using the
// attribute outside of Sentry, you will need to ensure you are
// overriding at runtime.
if (method_exists($model, 'setLoginAttribute')){
$loginAttribute = $app['config']['cartalyst/sentry::sentry.users.login_attribute'];
forward_static_call_array(
array($model, 'setLoginAttribute'),
array($loginAttribute)
);
}
return new UserProvider($app['sentry.admin.hasher'], $model);
});
}
/**
* Register the group provider used by Sentry.
*
* @return void
*/
protected function registerGroupProvider(){
$this->app['sentry.admin.group'] = $this->app->share(function($app){
$model = $app['config']['cartalyst/sentry::config.admin.groups.model'];
return new GroupProvider($model);
});
}
/**
* Register the throttle provider used by Sentry.
*
* @return void
*/
protected function registerThrottleProvider(){
$this->app['sentry.admin.throttle'] = $this->app->share(function($app){
$model = $app['config']['cartalyst/sentry::sentry.throttling.model'];
$throttleProvider = new ThrottleProvider($app['sentry.admin.user'], $model);
if ($app['config']['cartalyst/sentry::config.admin.throttling.enabled'] === false){
$throttleProvider->disable();
}
if (method_exists($model, 'setAttemptLimit')){
$attemptLimit = $app['config']['cartalyst/sentry::config.admin.throttling.attempt_limit'];
forward_static_call_array(
array($model, 'setAttemptLimit'),
array($attemptLimit)
);
}
if (method_exists($model, 'setSuspensionTime')){
$suspensionTime = $app['config']['cartalyst/sentry::config.admin.throttling.suspension_time'];
forward_static_call_array(
array($model, 'setSuspensionTime'),
array($suspensionTime)
);
}
return $throttleProvider;
});
}
/**
* Register the session driver used by Sentry.
*
* @return void
*/
protected function registerSession()
{
$this->app['sentry.admin.session'] = $this->app->share(function($app)
{
return new IlluminateSession($app['session']);
});
}
/**
* Register the cookie driver used by Sentry.
*
* @return void
*/
protected function registerCookie()
{
$this->app['sentry.admin.cookie'] = $this->app->share(function($app)
{
return new IlluminateCookie($app['cookie']);
});
}
/**
* Takes all the components of Sentry and glues them
* together to create Sentry.
*
* @return void
*/
protected function registerSentry()
{
$this->app['sentry.admin'] = $this->app->share(function($app)
{
// Once the authentication service has actually been requested by the developer
// we will set a variable in the application indicating such. This helps us
// know that we need to set any queued cookies in the after event later.
$app['sentry.admin.loaded'] = true;
return new Sentry(
$app['sentry.admin.user'],
$app['sentry.admin.group'],
$app['sentry.admin.throttle'],
$app['sentry.admin.session'],
$app['sentry.admin.cookie'],
$app['request']->getClientIp()
);
});
}
/**
* Sets up event observations required by Sentry.
*
* @return void
*/
protected function observeEvents()
{
// Set the cookie after the app runs
$app = $this->app;
$this->app->after(function($request, $response) use ($app)
{
if (isset($app['sentry.admin.loaded']) and $app['sentry.admin.loaded'] == true and ($cookie = $app['sentry.admin.cookie']->getCookie()))
{
$response->headers->setCookie($cookie);
}
});
}
}
@drsii
Copy link

drsii commented Jul 15, 2013

this is great, lets have Ben review and see about getting this added to a docs section called use cases.

@bencorlett
Copy link

This is great! Let's get this as official docs!!

@dhrrgn
Copy link

dhrrgn commented Jul 15, 2013

Just throwing in my 2 cents: That seems awfully un-maintainable in the long run. What if something changes in the Sentry core that breaks your provider? You have to keep it up to date with the core provider with every update...not very efficient.

Other than that, this is well done.

@ronaldcastillo
Copy link

If using Laravel, why not using a polymorphic relationship ? http://laravel.com/docs/eloquent#polymorphic-relations
That way you can still have different columns for different user types.

@yusidabcs
Copy link

hello there, I've try step by step but i got error : Class 'SentryAdmin' not found. If i need to register alias for it? thanks you

@leabdalla
Copy link
Author

@yusida, what is the file the error is throwing?

Don't forget to run composer dump-autoload

@leabdalla
Copy link
Author

@ronaldcastillo, can you show a working example?

@leabdalla
Copy link
Author

@dandoescode, this is really un-maintable for a long run, but for now it's working fine for me.
Let's wait better improvements from Cartalyst team in the future. So, while this not happen, I'll post here all modifications I do.

@pajcho
Copy link

pajcho commented Jul 31, 2013

This is my solution for FuelPHP implementation: https://gist.github.com/pajcho/6121291

@leabdalla
Copy link
Author

Nice work, @pajcho. I've extended the group model with the same approach and it worked fine.

Waiting for official docs with better adjusts :)

@barinali
Copy link

I can't use this. I get "Class sentry.admin does not exist" error. Where am I doing wrong?

@leabdalla
Copy link
Author

@alibarin what is the file and line of the error?

@barinali
Copy link

@leabdalla I think I fixed the problem by adding "SentryAdmin ServiceProvider" line to providers array in app.php.

@tavicu
Copy link

tavicu commented Jan 28, 2014

Hei!

Can someone help me? I can't access http://paste.laravel.com

@leabdalla
Copy link
Author

@tavicu, I've attached files to this gist. Thanks for advising!

@tavicu
Copy link

tavicu commented Feb 11, 2014

@leadbella Thanks. And also, i think at step 3 the path need to be app/config/packages/cartalyst/sentry/config.php instead of app/config/cartalyst/sentry/config.php

And i have another problem that i don't know how to solve. Please take a look over here: http://laravel.io/forum/02-11-2014-serviceprovider-in-a-folder

@lorvent
Copy link

lorvent commented Feb 15, 2014

is this working fine for laravel 4.1 and sentry 3?
Can i use this without any issues for now?

@leabdalla
Copy link
Author

@lorvent12345 I have no feedback with Sentry 3.
My suggestion is follow the same steps but using your current version files instead copying from this gist.

@whobutsb
Copy link

Hey @leabdalla,
When I'm in art tinker and try to run print_r(SentryAdmin::getUserProvider()); I get the error: PHP Fatal error: Class 'SentryAdmin' not found

I was wondering if there was anything I needed to add in my config/app.php in the Providers or the Alias sections?

In my composer.json config in the autoload section I have: app/lib.

Thanks for the help!

@whobutsb
Copy link

Alrighty I was able to hack my way through and get a fix. Here are some additions for your documentation

In my config/app.php:

  • in the providers section I added: Lib\SentryMods\SentryAdminServiceProvider
  • in the aliases section I added: 'SentryAdmin' => 'Lib\SentryMods\SentryAdmin'

Then in the lib/SentryMods/SentryAdminServiceProvider.php I had to change the registerSession function to read:

protected function registerSession()
{
    $this->app['sentry.admin.session'] = $this->app->share(function($app)
    {
        $key = $app['config']['cartalyst/sentry::admin.cookie.key'];

        return new IlluminateSession($app['session.store'], $key);
    });
}

@siegerhansma
Copy link

Just tried this with Laravel 4.1. In the SentryAdminServiceProvider this should be the RegisterCookie function:

 protected function registerCookie()
    {
        $this->app['sentry.admin.cookie'] = $this->app->share(function($app)
        {
            $key = $app['config']['cartalyst/sentry::admin.cookie.key'];

            /**
             * We'll default to using the 'request' strategy, but switch to
             * 'jar' if the Laravel version in use is 4.0.*
             */

            $strategy = 'request';

            if (preg_match('/^4\.0\.\d*$/D', $app::VERSION))
            {
                $strategy = 'jar';
            }

            return new IlluminateCookie($app['request'], $app['cookie'], $key, $strategy);

        });
    }

@luislora-es
Copy link

I have an error. 'Symfony\Component\Debug\Exception\FatalErrorException' with message 'Call to undefined method Cartalyst\Sentry\Cookies\IlluminateCookie::getCookie()' in D:\www\estacionagestion\app\libraries\SentryMods\SentryAdminServiceProvider.php:227

It's in function observeEvents, in this line:

if (isset($app['sentry.admin.loaded']) and $app['sentry.admin.loaded'] == true and ($cookie = $app['sentry.admin.cookie']->getCookie()))

Can anyone help me? I followed all steps and my code is identical.

Thanks in advance!

@leabdalla
Copy link
Author

@luislora-es, take a look at the comment of @shansma with registerCookie() function

@pkhodaveissi
Copy link

Has too many bugs in laravel 4.2, I've tried every comment bu still there are bugs like :
Call to undefined method Cartalyst\Sentry\Cookies\IlluminateCookie::getCookie()

anything?

thanks for the work

@mariapaulinar
Copy link

If anyone get error when is creating Admin model, try modify Sentry User model (vendor\cartalyst\sentry\src\Cartalyst\Sentry\Users\Eloquent\User.php) adding this:

public function getRememberToken()
{
return $this->remember_token;
}

public function setRememberToken($value)
{
$this->remember_token = $value;
}

public function getRememberTokenName()
{
return 'remember_token';
}

NOTE: add column remember_token VARCHAR(255) or text, in users table.

@rained23
Copy link

why dont we fork Sentry, make the service provider customizable from config file ?
So we just need to maintain our new repo services provider only if something broken because of a new commit made by Sentry. So everyone that really need to use this type of customization will automatically fixed too. Would that be nice @leabdalla. MultiInstanceSentry maybe ? :)

@umanda
Copy link

umanda commented Nov 8, 2014

OMG..... I have spent hours.. get fix this. @leabdalla's tutorial and codes for Laravel 4 and Sentry 2. but I have Laravel 4.2 and Sentry 2.x not 2.0. So there are several differences. but I have get it done. I have added a new GITS with my fixes.

https://gist.github.com/umanda/c4be8da584715cbb901d

Thank you very much for all and cheers
@leabdalla your idea is awsom dude !!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment