Setting up Multi Auth in Laravel 5.2
I recently needed to add multi-auth into an existing Laravel 5.2 application. We already have Employees (App\User) logging into the app, no problem, but now we need to add Customers (App\Customer). We’re not introducing a custom guard or service provider: just the basic, out-of-the-box authentication scheme for a different model and database table.
I spent a couple hours scouring the Laravel docs, StackOverflow posts, and the Laracasts forum for a comprehensive answer on how to accomplish this. I already had it working on a different app but it felt really hacked together. I knew all the different pieces necessary to make it work but I wanted to know what the “Laravelly” way was.
We started this project with the php artisan make:auth scaffolding so all of the defaults were already there. But how do you enable authentication for the new Customer class?
First, let’s create the model with a migration. php artisan make:model Customer -m. For the out-of-the-box authentication you need email, password, and remember_token columns.
Schema::create('customers', function (Blueprint $table) { $table->increments('id'); $table->string('first_name'); $table->string('last_name'); $table->string('email')->unique(); $table->string('password', 60); $table->text('details')->nullable(); $table->rememberToken(); $table->timestamps(); $table->softDeletes(); });
Now make sure that the Customer class extends Authenticatable instead of Model. Just like the User class!
Next, we need to update the auth.php file. Leave the defaults alone and add the customer guard and the customers provider. Then you’ll need to add an entry for customers to make sure you’re handling password resets.
'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'customer' => [ 'driver' => 'session', 'provider' => 'customers', ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\User::class, ], 'customers' => [ 'driver' => 'eloquent', 'model' => App\Customer::class, ], ], 'passwords' => [ 'users' => [ 'provider' => 'users', 'email' => 'auth.emails.password', 'table' => 'password_resets', 'expire' => 60, ], 'customers' => [ 'provider' => 'customers', 'email' => 'auth.customers.emails.password', 'table' => 'password_resets', 'expire' => 60, ], ],
In the routes.php file, I took out Route::auth() in favor of being explicit. Here’s what that looks like.
Route::group(['prefix' => 'auth'], function() { // Authentication Routes... Route::get('login', 'Auth\AuthController@showLoginForm'); Route::post('login', 'Auth\AuthController@login'); Route::get('logout', 'Auth\AuthController@logout'); // Registration Routes... Route::get('register', 'Auth\AuthController@showRegistrationForm'); Route::post('register', 'Auth\AuthController@register'); // Password Reset Routes... Route::get('password/reset/{token?}', 'Auth\PasswordController@showResetForm'); Route::post('password/email', 'Auth\PasswordController@sendResetLinkEmail'); Route::post('password/reset', 'Auth\PasswordController@reset'); });
Now, as you can imagine, authentication for internal Employees and external Customers is going to diverge. For instance, our Employees can’t register for an account- they’re given one. Our customers can register accounts and they’re able to use “magic” link style authentication (like Medium or Slack).
So, this is what it looks like. The routes would be metricloop.com/auth/admin/login (employees) or metricloop.com/auth/me/login (customers), for example.
Route::group(['prefix' => 'auth'], function() { Route::group(['prefix' => 'admin'], function() { // Authentication Routes... Route::get('login', 'Auth\AuthController@showLoginForm'); Route::post('login', 'Auth\AuthController@login'); Route::get('logout', 'Auth\AuthController@logout'); // Password Reset Routes... Route::get('password/reset/{token?}', 'Auth\PasswordController@showResetForm'); Route::post('password/email', 'Auth\PasswordController@sendResetLinkEmail'); Route::post('password/reset', 'Auth\PasswordController@reset'); }); Route::group(['prefix' => 'me'], function() { // Authentication Routes... Route::get('login', 'Auth\AuthController@showLoginForm'); Route::post('login', 'Auth\AuthController@login'); Route::get('logout', 'Auth\AuthController@logout'); // Registration Routes... Route::get('register', 'Auth\AuthController@showRegistrationForm'); Route::post('register', 'Auth\AuthController@register'); // Password Reset Routes... Route::get('password/reset/{token?}', 'Auth\PasswordController@showResetForm'); Route::post('password/email', 'Auth\PasswordController@sendResetLinkEmail'); Route::post('password/reset', 'Auth\PasswordController@reset'); }); });
We’re not done yet. The problem is that both auth/admin/login and auth/me/login are hitting the same controller function. This is where you need to create a new AuthController specifically for Customers. Let’s do that.
Go ahead and copy Auth\AuthController.php into Auth\Customer\AuthController.php (make sure to update the namespace!). Go back and update the routes to reflect the new controller. Like so:
Route::group(['prefix' => 'auth'], function() { Route::group(['prefix' => 'admin'], function() { // Authentication Routes... Route::get('login', 'Auth\AuthController@showLoginForm'); Route::post('login', 'Auth\AuthController@login'); Route::get('logout', 'Auth\AuthController@logout'); // Password Reset Routes... Route::get('password/reset/{token?}', 'Auth\PasswordController@showResetForm'); Route::post('password/email', 'Auth\PasswordController@sendResetLinkEmail'); Route::post('password/reset', 'Auth\PasswordController@reset'); }); Route::group(['prefix' => 'me'], function() { // Authentication Routes... Route::get('login', 'Auth\Customer\AuthController@showLoginForm'); Route::post('login', 'Auth\Customer\AuthController@login'); Route::get('logout', 'Auth\Customer\AuthController@logout'); // Registration Routes... Route::get('register', 'Auth\Customer\AuthController@showRegistrationForm'); Route::post('register', 'Auth\Customer\AuthController@register'); // Password Reset Routes... Route::get('password/reset/{token?}', 'Auth\PasswordController@showResetForm'); Route::post('password/email', 'Auth\PasswordController@sendResetLinkEmail'); Route::post('password/reset', 'Auth\PasswordController@reset'); }); });
Great! Except that now you have two Controllers that do the exact same thing. What are the differences? First, they’re using different guards — customer vs web. Secondly, you probably want to redirect your Customers to different areas after login/logout. Thirdly, you most definitely have different login/register views for your Customers compared to your Employees or Admin users. And lastly, you have different criteria for validating and creating authenticated users (Customer vs. User).
Luckily, this is all fixed by a couple of member variables in the new Customer\AuthController class.
<?php namespace App\Http\Controllers\Auth\Customer; class AuthController extends Controller { use AuthenticatesAndRegistersUsers, ThrottlesLogins; /** * The guard to use. * * @var string */ protected $guard = 'customer'; /** * Where to redirect users after login / registration. * * @var string */ protected $redirectTo = '/me'; /** * Where to redirect users after logout. * * @var string */ protected $redirectAfterLogout = '/me'; /** * The login view. * * @var string */ protected $loginView = 'auth.customers.login'; /** * The register view. * * @var string */ protected $registerView = 'auth.customers.register'; /** * Create a new authentication controller instance. * */ public function __construct() { $this->middleware('guest', ['except' => 'logout']); } /** * Get a validator for an incoming registration request. * * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) { return Validator::make($data, [ 'first_name' => 'required|max:255', 'last_name' => 'required|max:255', 'email' => 'required|email|max:255|unique:customers', 'password' => 'required|min:6', ]); } /** * Create a new user instance after a valid registration. * * @param array $data * @return User */ protected function create(array $data) { return Customer::create([ 'first_name' => $data['first_name'], 'last_name' => $data['last_name'], 'email' => $data['email'], 'password' => bcrypt($data['password']), ]); } }
Define your validation and creation rules and you’re good to go!
Pro Tip: The out-of-the-box authentication uses logical defaults that can easily be overridden by these member variables. You can explore the AuthenticatesAndRegistersUsers and RedirectsUsers traits and see all the authentication logic hidden behind those curtains.
But what about a Customer resetting their password? If you tried it right now, you’d see the wrong view and you’d be looking into the wrong table for the email address (Hint: you need another PasswordController). Let’s do the same thing we did for the AuthController.
<?php namespace App\Http\Controllers\Auth\Customer; class PasswordController extends Controller { use ResetsPasswords; /** * @var string */ protected $redirectTo = '/me'; /** * The reset view. * * @var string */ protected $resetView = 'auth.customers.passwords.reset'; /** * The email request. * * @var string */ protected $linkRequestView = 'auth.customers.passwords.email'; /** * The broker to use. * * @var string */ protected $broker = 'customers'; /** * Create a new password controller instance. * */ public function __construct() { $this->middleware('guest'); } }
We define some member variables to tell the controller which views to load. But there’s this tricksy little thing about resetting passwords. See, the PasswordController drops you into a PasswordBroker and that is what handles the reset. And when it handles the reset, it uses the email template from the default broker when sending the reset email. This means you’re sending your Customers the same email you send your Employees!
But there’s this tricksy little thing about resetting passwords.
So, we have to tell the PasswordController which “broker” to use when attempting the reset. Make sure this variable,
protected $broker = ‘customers’;
is set in Auth\Customer\PasswordController (where customers corresponds to passwords[‘customers’] in auth.php).
And, of course, update your routes.php file.
Route::group(['prefix' => 'auth'], function() {
Route::group(['prefix' => 'admin'], function() { // Authentication Routes... Route::get('login', 'Auth\AuthController@showLoginForm'); Route::post('login', 'Auth\AuthController@login'); Route::get('logout', 'Auth\AuthController@logout'); // Password Reset Routes... Route::get('password/reset/{token?}', 'Auth\PasswordController@showResetForm'); Route::post('password/email', 'Auth\PasswordController@sendResetLinkEmail'); Route::post('password/reset', 'Auth\PasswordController@reset'); }); Route::group(['prefix' => 'me'], function() { // Authentication Routes... Route::get('login', 'Auth\Customer\AuthController@showLoginForm'); Route::post('login', 'Auth\Customer\AuthController@login'); Route::get('logout', 'Auth\Customer\AuthController@logout'); // Registration Routes... Route::get('register', 'Auth\Customer\AuthController@showRegistrationForm'); Route::post('register', 'Auth\Customer\AuthController@register'); // Password Reset Routes... Route::get('password/reset/{token?}', 'Auth\Customer\PasswordController@showResetForm'); Route::post('password/email', 'Auth\Customer\PasswordController@sendResetLinkEmail'); Route::post('password/reset', 'Auth\Customer\PasswordController@reset'); }); });
To recap, you need a new AuthController and PasswordController for each Authenticatable model with member variables telling the underlying auth logic where to go and look for things. You also have to tell the AuthController which guard to use and the PasswordController which broker to load.
The next step after this would be to implement password-optional auth for your app as the upward trend of getting rid of passwords continues to increase.
Originally published at metricloop.com on July 1, 2016.