Multiple Authentication in Laravel
You may not need this, if you can manage multiple types of user via roles and permissions it will be much the simpler route to follow.
In my case I wanted user with different: properties, relationships, routes, password rules, timeouts, and more.
It was worth the pain of setting up two authenticatable models.
Laravel is very flexible and well documented, but the further you stray from what most people do - the less obvious it is and a few of these steps took me a while to figure out.
It seems I’m not the only one to find this process tricky, there are quite a few blogs posts about it
Create a new Authenticatable Model
This is similar to the standard User model, I called mine Account
and they are being used for a Single Sign On process so related routes etc are named sso.
artisan make:migration create_accounts_table --create=accounts
The schema should look like
public function up()
{
Schema::create('accounts', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
You can have more fields if your user has more properties.
app/Models/Account.php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\belongsTo;
use Illuminate\Database\Eloquent\Relations\belongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class Account extends Authenticatable
{
use Notifiable, SoftDeletes;
protected $hidden = ['password', 'email', 'token', 'token_expire_at', 'remember_token', 'deleted_at', 'email_verified_at'];
}
You don’t have to hide all these fields - but it’s good to keep anything sensitive excluded from API calls by default.
The important things are it has to extend User as Authenticatable
and use Notifiable.
Add Guard
config/auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// add this
'sso' => [
'driver' => 'session',
'provider' => 'accounts',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// add this
'accounts' => [
'driver' => 'eloquent',
'model' => App\Models\Account::class,
'table' => 'accounts',
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
// add this
'accounts' => [
'provider' => 'accounts',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
This creates a new sso
guard which uses sessions and passwords based on the accounts
table instead of the users table.
At one point in debugging before I had this all hooked up properly I could see things almost working but I was seeing queries for my account running against the users table.
This setup is a little harder to get right because some misconfigurations just result in an inability to login with no fatal error.
Add Routes
First create a new middleware group - this allows you to put the controls you need in place for this user.
In my case these users will be accessing the Laravel instance exclusively via an API driven by a JavaScript frontend, so I copied the web middleware but removed the Interia specific parts.
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\HandleInertiaRequests::class,
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
],
// added this sso section
'sso' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
Now I can can create my route file referencing the authentication guard I just defined
routes/sso.php
<?php
use App\Http\Controllers\AccountController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth:sso')->prefix('/{site:code}/auth/')->group(function () {
Route::post('logout', [AccountController::class, 'logout']);
Route::get('bookmarks', [AccountController::class, 'bookmarks']);
Route::post('bookmarks', [AccountController::class, 'saveBookmarks']);
Route::delete('bookmarks', [AccountController::class, 'deleteBookmarks']);
});
Route::prefix('/{site:code}/auth/')->group(function () {
Route::post('signup', [AccountController::class, 'register']);
Route::post('login', [AccountController::class, 'login']);
Route::post('get_reset_link', [AccountController::class, 'sendResetPasswordLink']);
Route::post('reset', [AccountController::class, 'resetPassword']);
});
In my case I have a site prefix - this is used for multisite login.
The logout and bookmark routes are protected, while the signup and login routes are available to anonymous users.
Register this route file in
app/Providers/RouteServiceProvider.php
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
// add this
Route::middleware('sso')
->prefix('sso')
->group(base_path('routes/sso.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
Now all my new sso routes are protected by the middleware which adds things like session and XSRF protection.
Account Controller
<?php
namespace App\Http\Controllers;
use App\Http\Requests\AccountSignupRequest;
use App\Mail\AccountPasswordReset;
use App\Models\Account;
use App\Models\Site;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
class AccountController extends Controller
{
public function register(Site $site, AccountSignupRequest $request)
{
$account = new Account;
$account->email = $request->email;
$account->firstname = $request->firstname;
$account->lastname = $request->lastname;
$account->password = Hash::make($request->password);
$account->last_login = now();
$account->save();
$account->refresh();
$account->sites()->attach($site);
This bit is important !
To get the right kind of authenticatable from the Auth facade we have to specify the custom guard we made if you don’t specify one here then Laravel uses the default guard which will get the wrong kind of Authenticatable - probably User
.
// be sure to specify the guard
Auth::guard('sso')->login($account);
return response()->json([
'status' => 'success',
'message' => 'Account created successfully',
'account' => $account,
]);
}
public function login(Site $site, Request $request)
{
$request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
]);
$credentials = $request->only('email', 'password');
$ok = Auth::guard('sso')->attempt($credentials);
if (! $ok) {
return response()->json([
'status' => 'error',
'message' => 'Unauthorized',
], 401);
}
$user = Auth::guard('sso')->user();
return response()->json([
'status' => 'success',
'user' => $user,
]);
}
public function logout()
{
Auth::logout();
return response()->json([
'status' => 'success',
'message' => 'Successfully logged out',
]);
}
public function bookmarks(Site $site)
{
$account = Auth::user();
$bookmarks = $this->getBookmarks($site, $account);
return response()->json($bookmarks);
}
public function sendResetPasswordLink(Site $site, Request $request)
{
$request->validate(['email' => 'required|email']);
$status = Password::broker('accounts')->sendResetLink(
$request->only('email'),
function ($user, $token) use ($site) {
Mail::to($user->email, "{$user->firstname} {$user->lastname}")->send(
new AccountPasswordReset($user, $site, $token));
return Password::RESET_LINK_SENT;
},
);
return response()->json($status);
}
public function resetPassword(Site $site, Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|min:8|confirmed',
]);
$status = Password::broker('accounts')->reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (Account $user, string $password) {
$user->forceFill([
'password' => Hash::make($password),
])->setRememberToken(Str::random(60));
$user->save();
event(new PasswordReset($user));
}
);
return response()->json($status);
}
}
the AccountSignupRequest validation looks like
public function rules(): array
{
return [
'firstname' => ['required', 'string', 'max:255'],
'lastname' => 'required|string|max:255',
'email' => ['required', 'email:dns', Rule::unique('accounts')],
'password' => 'min:8',
];
}
It’s really not that different to a regular user registration
But you have to specify the guard.
See also why I have used a closure above to send a password reset email with additional data
Useful resources
- How to implement multiple authentication in Laravel
- How to Create Multi Authentication with Multiple Table in Laravel
- Laravel Multi Auth using different tables [part 1: User authentication]
- Laravel Multi Auth using different tables [part 2: Admin authentication]
- Laravel: Using Different Table and Guard for Login
- Laravel 11 Multi authentication using guard Tutorial | Admin & user auth system in laravel
- Password Brokers: Reset Passwords on Multiple Tables in Laravel
I also dug into API docs and even source code a little.
It’s worth checking out the Authenticatable interface and the PasswordBroker class
Caveat
This code isn’t live yet - it seems to pass initial testing but let me know if you spot bugs or if you see a better way of doing this.