Framework: Resolve TokenRepositoryInterface from Container rather than new DatabaseTokenRepository(...)

Created on 26 Aug 2016  Â·  5Comments  Â·  Source: laravel/framework

I've been looking over a simple way of making a couple of minor amends to the Auth\Passwords in a project and have spotted that PasswordBrokerManager createTokenRepository() news a DatabaseTokenRepository() instance rather than resolving the \Illuminate\Auth\Passwords\TokenRepositoryInterface (which would be expected from the docblock).

Likewise the PasswordBrokerManager resolve() method new's an \Illuminate\Contracts\Auth\PasswordBroker instance (which is is in the IOC as auth.password).

I was looking at overriding a couple of methods and rebinding a new implementation of TokenRepositoryInterface before realising that this wasn't always used (only when type hinted in the construct).

What I'm asking is there a specific reason we are newing instances of contract implemenations which if were resolved, could be overridden? And if there is no reason, would a PR to change this behaviour be accepted.

I'd rather not create all the boilerplate of a new PasswordResetServiceProvider for what would be a couple of methods to override.

Most helpful comment

This is probably irrelevant once 5.5 is released because of #17499 but I wanted to modify the Reset Password functionality so it would not generate a new token each time a user clicks the link. If a user is impatient and clicks the Reset Password more than once, then the first email they receive is now invalid because the token was replaced. Instead, I just want the 2nd email to contain the same token as the first email did.

I subclassed DatabaseTokenRepository so it will only create a new token if a non-expired one doesn't already exist.

Here's what I had to do for this to work:

config/app.php: - comment out the PasswordResetServiceProvider and replace it with my subclass:

//Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
App\Auth\Passwords\MLPasswordResetServiceProvider::class,

MLPasswordResetServiceProvider.php - which registers my MLPasswordBrokerManager in the container under auth.password:

class MLPasswordResetServiceProvider extends PasswordResetServiceProvider
{
    /**
     * Register the password broker instance.
     *
     * @return void
     */
    protected function registerPasswordBroker()
    {
        $this->app->singleton('auth.password', function ($app) {
            return new MLPasswordBrokerManager($app);
        });

        $this->app->bind('auth.password.broker', function ($app) {
            return $app->make('auth.password')->broker();
        });
    }
}

MLPasswordBrokerManager.php - this class returns my MLDatabaseTokenRepository subclass:

class MLPasswordBrokerManager extends PasswordBrokerManager
{

    /**
     * Create a token repository instance based on the given configuration.
     *
     * @param  array $config
     * @return \Illuminate\Auth\Passwords\TokenRepositoryInterface
     */
    protected function createTokenRepository(array $config)
    {
        $key = $this->app['config']['app.key'];

        if (Str::startsWith($key, 'base64:')) {
            $key = base64_decode(substr($key, 7));
        }

        $connection = isset($config['connection']) ? $config['connection'] : null;

        return new MLDatabaseTokenRepository(
            $this->app['db']->connection($connection),
            $this->app['hash'],
            $config['table'],
            $key,
            $config['expire']
        );
    }
}

MLDatabaseTokenRepository.php - only creates a new token if a valid one does not already exist for the user:

class MLDatabaseTokenRepository extends DatabaseTokenRepository
{

    /**
     * Create a new token record.
     *
     * @param  \Illuminate\Contracts\Auth\CanResetPassword $user
     * @return string
     */
    public function create(CanResetPasswordContract $user)
    {
        $email = $user->getEmailForPasswordReset();

        // If the user already has a valid token, send it instead of creating a new one.
        // This will prevent the impatient user from being frustrated if they clicked
        // Reset Password more than once.
        $record = (array)$this->getTable()
            ->where('email', $email)
            ->first();
        $exists = $record && !$this->tokenExpired($record['created_at']);
        if ($exists) {
            return $record['token'];
        }

        $this->deleteExisting($user);

        // We will create a new, random token for the user so that we can e-mail them
        // a safe link to the password reset form. Then we will insert a record in
        // the database so that we can verify the token within the actual reset.
        $token = $this->createNewToken();

        $this->getTable()->insert($this->getPayload($email, $token));

        return $token;
    }
}

All 5 comments

Funny, I just ended up in the exact same place looking for a way to do username based password resets.

The column email seems to be hard coded in DatabaseTokenRepository making this a bit more difficult than I was hoping for.

Ended up there initially because need to change the hash length (nothing
too difficult we thought!) And then discovered an awful lot which is quite
concrete!

On 2 Sep 2016 20:22, "Dave" [email protected] wrote:

Funny, I just ended up in the exact same place looking for a way to do
username based password resets.

The column email seems to be hard coded in DatabaseTokenRepository making
this a bit more difficult than I was hoping for.

—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
https://github.com/laravel/framework/issues/15071#issuecomment-244465609,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AB0I4_z-41gNMfhrgVdFVieg3udmnYO0ks5qmHeSgaJpZM4JuIrM
.

Please feel free to open a PR with the suggested changes, I think you have a point here.

Closing this since it's not actually a bug :) please ping me if you think otherwise.

This is probably irrelevant once 5.5 is released because of #17499 but I wanted to modify the Reset Password functionality so it would not generate a new token each time a user clicks the link. If a user is impatient and clicks the Reset Password more than once, then the first email they receive is now invalid because the token was replaced. Instead, I just want the 2nd email to contain the same token as the first email did.

I subclassed DatabaseTokenRepository so it will only create a new token if a non-expired one doesn't already exist.

Here's what I had to do for this to work:

config/app.php: - comment out the PasswordResetServiceProvider and replace it with my subclass:

//Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
App\Auth\Passwords\MLPasswordResetServiceProvider::class,

MLPasswordResetServiceProvider.php - which registers my MLPasswordBrokerManager in the container under auth.password:

class MLPasswordResetServiceProvider extends PasswordResetServiceProvider
{
    /**
     * Register the password broker instance.
     *
     * @return void
     */
    protected function registerPasswordBroker()
    {
        $this->app->singleton('auth.password', function ($app) {
            return new MLPasswordBrokerManager($app);
        });

        $this->app->bind('auth.password.broker', function ($app) {
            return $app->make('auth.password')->broker();
        });
    }
}

MLPasswordBrokerManager.php - this class returns my MLDatabaseTokenRepository subclass:

class MLPasswordBrokerManager extends PasswordBrokerManager
{

    /**
     * Create a token repository instance based on the given configuration.
     *
     * @param  array $config
     * @return \Illuminate\Auth\Passwords\TokenRepositoryInterface
     */
    protected function createTokenRepository(array $config)
    {
        $key = $this->app['config']['app.key'];

        if (Str::startsWith($key, 'base64:')) {
            $key = base64_decode(substr($key, 7));
        }

        $connection = isset($config['connection']) ? $config['connection'] : null;

        return new MLDatabaseTokenRepository(
            $this->app['db']->connection($connection),
            $this->app['hash'],
            $config['table'],
            $key,
            $config['expire']
        );
    }
}

MLDatabaseTokenRepository.php - only creates a new token if a valid one does not already exist for the user:

class MLDatabaseTokenRepository extends DatabaseTokenRepository
{

    /**
     * Create a new token record.
     *
     * @param  \Illuminate\Contracts\Auth\CanResetPassword $user
     * @return string
     */
    public function create(CanResetPasswordContract $user)
    {
        $email = $user->getEmailForPasswordReset();

        // If the user already has a valid token, send it instead of creating a new one.
        // This will prevent the impatient user from being frustrated if they clicked
        // Reset Password more than once.
        $record = (array)$this->getTable()
            ->where('email', $email)
            ->first();
        $exists = $record && !$this->tokenExpired($record['created_at']);
        if ($exists) {
            return $record['token'];
        }

        $this->deleteExisting($user);

        // We will create a new, random token for the user so that we can e-mail them
        // a safe link to the password reset form. Then we will insert a record in
        // the database so that we can verify the token within the actual reset.
        $token = $this->createNewToken();

        $this->getTable()->insert($this->getPayload($email, $token));

        return $token;
    }
}

Hi,
Resolving TokenRepositoryInterface instance should permits to easily implements token management with Sentinel Reminder.
L5.4 has new DatabaseTokenRepository() hard coded.
Cheers.

Was this page helpful?
0 / 5 - 0 ratings