Laravel 11: Custom Password Broker Ignores My Tenant-Aware Repository

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' in config/auth.php.
  • Ran php artisan config:clear && php artisan cache:clear.
  • Logged in CustomPasswordBrokerManager::createTokenRepository(), but it never fires.
  • Confirmed 'passwords' => 'users' in config/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.