I’m implementing multi-tenancy in my Laravel 11 application, so I need a tenant_id column in the password_reset_tokens table. I’ve created a custom PasswordBrokerManager and a custom TenantAwareDatabaseTokenRepository, but Laravel still uses the default DatabaseTokenRepository—so the tenant_id never gets inserted.
1. config/auth.php
return [
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => AppModelsUser::class,
],
],
'passwords' => [
'users' => [
'driver' => 'custom', // I made sure to set this
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
];
I ran php artisan config:clear && php artisan cache:clear and verified with dd(config('auth.passwords.users')) that it shows "driver" => "custom".
2. AppServiceProvider
<?php
namespace AppProviders;
use IlluminateSupportServiceProvider;
use IlluminateAuthPasswordsPasswordBrokerManager;
use AppExtensionsCustomPasswordBrokerManager;
use IlluminateSupportFacadesLog;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
Log::debug('AppServiceProvider: register() is being called...');
// Bind my custom password broker manager
$this->app->singleton(PasswordBrokerManager::class, function ($app) {
Log::debug('Binding CustomPasswordBrokerManager...');
return new CustomPasswordBrokerManager($app);
});
}
public function boot(): void
{
Log::debug('AppServiceProvider: boot() is being called...');
}
}
The logs for AppServiceProvider show up, but when I trigger a password reset, my custom code is ignored.
3. CustomPasswordBrokerManager
<?php
namespace AppExtensions;
use IlluminateAuthPasswordsPasswordBrokerManager;
use IlluminateAuthPasswordsTokenRepositoryInterface;
use AppRepositoriesTenantAwareDatabaseTokenRepository;
use AppModelsTenant;
use IlluminateSupportFacadesLog;
use RuntimeException;
class CustomPasswordBrokerManager extends PasswordBrokerManager
{
public function __construct($app)
{
Log::debug('CustomPasswordBrokerManager: constructor called!');
parent::__construct($app);
}
protected function createTokenRepository(array $config): TokenRepositoryInterface
{
Log::debug('CustomPasswordBrokerManager: createTokenRepository called! driver=' . ($config['driver'] ?? ''));
// 1) DB connection
$connection = $this->app['db']->connection($config['connection'] ?? null);
// 2) Table name
$table = $config['table'];
// 3) Laravel app key
$hashKey = $this->app['config']['app.key'];
// 4) Expiry & throttle
$expire = $config['expire'];
$throttle = $config['throttle'] ?? 60;
// 5) Current tenant
$tenant = Tenant::current();
if (! $tenant || ! $tenant->id) {
throw new RuntimeException('No tenant found. Tenant ID cannot be null.');
}
// 6) Return custom repository
return new TenantAwareDatabaseTokenRepository(
$connection,
$this->app['hash'],
$table,
$hashKey,
$expire,
$throttle,
$tenant->id
);
}
}
I never see the “createTokenRepository called!” log in my laravel.log, meaning Laravel won’t enter this code.
4. TenantAwareDatabaseTokenRepository
<?php
namespace AppRepositories;
use IlluminateAuthPasswordsDatabaseTokenRepository;
use IlluminateContractsAuthCanResetPassword as CanResetPasswordContract;
use IlluminateContractsHashingHasher;
use IlluminateDatabaseConnectionInterface;
use IlluminateSupportCarbon;
class TenantAwareDatabaseTokenRepository extends DatabaseTokenRepository
{
protected mixed $tenantId;
public function __construct(
ConnectionInterface $connection,
Hasher $hasher,
string $table,
string $hashKey,
int $expires = 60,
int $throttle = 60,
mixed $tenantId = null
) {
parent::__construct($connection, $hasher, $table, $hashKey, $expires, $throttle);
$this->tenantId = $tenantId;
}
protected function getPayload($email, $token): array
{
return [
'email' => $email,
'tenant_id' => $this->tenantId,
'token' => $this->getHasher()->make($token),
'created_at' => Carbon::now(),
];
}
}
This class includes tenant_id in the insert, but Laravel keeps using the default DatabaseTokenRepository.
5. Error & Stack Trace
When I request a password reset, I get:
SQLSTATE[HY000]: General error: 1364 Field 'tenant_id' doesn't have a default value
IlluminateAuthPasswordsDatabaseTokenRepository->create()
It shows DatabaseTokenRepository is being called, not TenantAwareDatabaseTokenRepository.
6. Controller Snippet
public function store(Request $request): Responsable
{
$request->validate(['email' => 'required|email']);
$status = $this->broker()->sendResetLink($request->only('email'));
// ...
}
protected function broker(): PasswordBroker
{
return Password::broker(config('fortify.passwords'));
// config('fortify.passwords') is "users"
}
7. Things I Tried
- Double-checked
'driver' => 'custom'inconfig/auth.php. - Ran
php artisan config:clear && php artisan cache:clear. - Logged in
CustomPasswordBrokerManager::createTokenRepository(), but it never fires. - Confirmed
'passwords' => 'users'inconfig/fortify.php. - Verified logs in
AppServiceProviderdo show up.
8. Environment
- Laravel: 11.x
- PHP: 8.3
- MySQL/MariaDB
- Jetstream + Fortify
Question
How do I ensure Laravel actually uses my CustomPasswordBrokerManager and TenantAwareDatabaseTokenRepository instead of defaulting to DatabaseTokenRepository? Any help is appreciated.