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' => App\Models\User::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 App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Auth\Passwords\PasswordBrokerManager;
use App\Extensions\CustomPasswordBrokerManager;
use Illuminate\Support\Facades\Log;
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 App\Extensions;
use Illuminate\Auth\Passwords\PasswordBrokerManager;
use Illuminate\Auth\Passwords\TokenRepositoryInterface;
use App\Repositories\TenantAwareDatabaseTokenRepository;
use App\Models\Tenant;
use Illuminate\Support\Facades\Log;
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 App\Repositories;
use Illuminate\Auth\Passwords\DatabaseTokenRepository;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Carbon;
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
Illuminate\Auth\Passwords\DatabaseTokenRepository->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
AppServiceProvider
do 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.
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' => App\Models\User::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 App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Auth\Passwords\PasswordBrokerManager;
use App\Extensions\CustomPasswordBrokerManager;
use Illuminate\Support\Facades\Log;
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 App\Extensions;
use Illuminate\Auth\Passwords\PasswordBrokerManager;
use Illuminate\Auth\Passwords\TokenRepositoryInterface;
use App\Repositories\TenantAwareDatabaseTokenRepository;
use App\Models\Tenant;
use Illuminate\Support\Facades\Log;
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 App\Repositories;
use Illuminate\Auth\Passwords\DatabaseTokenRepository;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Contracts\Hashing\Hasher;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Carbon;
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
Illuminate\Auth\Passwords\DatabaseTokenRepository->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
AppServiceProvider
do 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.
1 Answer
Reset to default 1I think the provider you want to override is auth.password
(Github):
$this->app->singleton('auth.password', function ($app) {
Log::debug('Binding CustomPasswordBrokerManager...');
return new CustomPasswordBrokerManager($app);
});