Damen's avatar
Level 3

Authentication for multi-tenant application in Laravel 5.4

For my application I'm trying to completely separate the goings on of tenants. Usernames only need to be unique for the company (not the entire application), companies have their own login routes, create their own users etc. Below I have outlined an example of what I'm thinking for the routes.

Route::get('/register', RegisterController@show');
Route::post('/register', 'RegisterController@store');

These routes are for the registration of companies and the first administrator. Once validated the user is redirected to the company's own login route (outlined below), where they and the accounts they create later can login.

Route::get('/{company}/login', 'LoginController@show');
Route::post('/{company}/login', 'LoginController@login');

These are a company's login routes. I eventually want a company's routes to be a subdomain (e.g. '{company}.example.com/login'), but that is a different topic.

I think this get's the idea across. The problem I am facing is not at all related to the above routes. Getting those set up was pretty easy. The issue right now is the authentication of users trying to log in and access private routes for a company. Users should only have access to the company/team they are a part of. I'm wondering what the easiest and most elegant way of approaching this problem. I don't want to completely redo Laravel's auth system, I want to extend it to suit my needs.

Through my research I think what I need to do is create a custom Guard, but the documentation doesn't do a great job of explaining what a Guard really is. Of course I am open to different approaches. I really think this type of system is general enough to benefit the SaaS community, and I'll probably open source it if I get it working nicely.

0 likes
2 replies
martinbean's avatar
Level 80

@Damen You don’t need to create a new guard or modify Laravel’s authentication at all. All you really need to do is add a “where” clause to your login query.

Using Laravel’s LoginController, you can do this by overriding the credentials() method. This specifies the array that should be passed to Auth::attempt(). You can modify it to include the current company by doing something like:

protected function credentials(Request $request)
{
    return $request->only($this->username(), 'password') + [
        'tenant_id' => app(Tenant::class)->getKey(),
    ];
}

This means you can also use the in-built login routes:

Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('login', 'Auth\LoginController@login');
Route::post('logout', 'Auth\LoginController@logout')->name('logout');

How you set the tenant in the container is up to you. If you’re placing tenants’ routes in a group, then you could do something like:

Route::group(['domain' => '{tenant}.example.com'], function ($tenant) {
    // Set tenant in container
    app(Tenant::model)->instance($tenant);

    // Authentication
    Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
    Route::post('login', 'Auth\LoginController@login');
    Route::post('logout', 'Auth\LoginController@logout')->name('logout');
});
1 like
Damen's avatar
Level 3

@martinbean Wow that is perfect. Much simpler than what I thought I had to do. Thank you!

Please or to participate in this conversation.