Search code examples
phplaravellaravel-authentication

Password reset in Laravel by email or mobile


By default Laravel 5.5's password reset system works on email, but I need to add support for a mobile number (verify by OTP and generate a token and redirect to password reset page). I am doing all this part and I have created a mobile column on password_resets table.

But the problem is \Illuminate\Auth\Passwords\DatabaseTokenRepository && \Illuminate\Auth\Passwords\TokenRepositoryInterface ON exist method and it doesn't seem configurable.

public function exists(CanResetPasswordContract $user, $token)
{
    $record = (array) $this->getTable()->where(
        'email', $user->getEmailForPasswordReset()
    )->first();

    return $record &&
           ! $this->tokenExpired($record['created_at']) &&
             $this->hasher->check($token, $record['token']);
}

I need to override that method. There's so much inheritance going on. What classes do I need to extend and how to override that method.


Solution

  • If you want to override behaviour of \Illuminate\Auth\Passwords\DatabaseTokenRepository methods, you will have to build your own token repository, overriding methods in the existing repository that currently just check for the 'email' column in the database. Ensure you've created a migration to add the appropriate column:

    <?php
    
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    
    return new class extends Migration
    {
        public function up(): void
        {
            $tname = config("auth.passwords.users.table");
            Schema::table($tname, fn (Blueprint $t) => $t->string("mobile", 16));
        }
    
        public function down(): void
        {
            $tname = config("auth.passwords.users.table");
            Schema::table($tname, fn (Blueprint $t) => $t->dropColumn("mobile"));
        }
    }
    

    Then create your custom repository:

    app/Auth/DatabaseTokenRepository.php

    <?php
    
    namespace App\Auth;
    
    use Illuminate\Auth\Passwords\DatabaseTokenRepository as DatabaseTokenRepositoryBase;
    use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
    use Illuminate\Support\Carbon;
    
    class DatabaseTokenRepository extends DatabaseTokenRepositoryBase;
    {
        //
        // Override these methods to use mobile as well as email
        //
        public function create(CanResetPasswordContract $user)
        {
            $email = $user->getEmailForPasswordReset();
            $mobile = $user->getMobileForPasswordReset();
            $this->deleteExisting($user);
            $token = $this->createNewToken();
            $this->getTable()->insert($this->getPayload($email, $mobile, $token));
            return $token;
        }
    
        protected function deleteExisting(CanResetPasswordContract $user)
        {
            return $this->getTable()
                ->where("email", $user->getEmailForPasswordReset())
                ->orWhere("mobile", $user->getMobileForPasswordReset())
                ->delete();
        }
    
        protected function getPayload($email, $mobile, $token): array
        {
            return [
                "email" => $email,
                "mobile" => $mobile,
                "token" => $this->hasher->make($token),
                "created_at" => new Carbon(),
            ];
        }
    
        public function exists(CanResetPasswordContract $user, $token)
        {
            $record = (array) $this->getTable()
                ->where("email", $user->getEmailForPasswordReset())
                ->orWhere("mobile", $user->getMobileForPasswordReset())
                ->first();
            return $record &&
                   ! $this->tokenExpired($record["created_at"]) &&
                     $this->hasher->check($token, $record["token"]);
        }
    
        public function recentlyCreatedToken(CanResetPasswordContract $user)
        {
            $record = (array) $this->getTable()
                ->where("email", $user->getEmailForPasswordReset())
                ->orWhere("mobile", $user->getMobileForPasswordReset())
                ->first();
    
            return $record && $this->tokenRecentlyCreated($record['created_at']);
        }
    }
    

    Now you will need to use this custom token repository instead of the default one. So you have to override another class.

    app/Auth/PasswordBrokerManager.php

    <?php
    namespace App\Auth;
    
    use Illuminate\Support\Str;
    use Illuminate\Auth\Passwords\PasswordBrokerManager as PasswordBrokerManagerBase;
    
    class PasswordBrokerManager extends PasswordBrokerManagerBase
    {
        protected function createTokenRepository(array $config)
        {
            $key = $this->app['config']['app.key'];
            if (Str::startsWith($key, 'base64:')) {
                $key = base64_decode(substr($key, 7));
            }
            $connection = $config['connection'] ?? null;
            // return an instance of your new repository
            // it's in the same namespace, no need to alias it
            return new DatabaseTokenRepository(
                $this->app['db']->connection($connection),
                $this->app['hash'],
                $config['table'],
                $key,
                $config['expire']
            );
        }
    }
    

    Now you have created a custom broker to use your custom repository. You need a new service provider to make use of it.

    app/Providers/PasswordResetServiceProvider.php

    <?php
    namespace App\Providers;
    
    use App\Auth\PasswordBrokerManager;
    use Illuminate\Auth\Passwords\PasswordResetServiceProvider as PasswordResetServiceProviderBase;
    
    class PasswordResetServiceProvider extends PasswordResetServiceProviderBase
    {
        protected function registerPasswordBroker()
        {
            $this->app->singleton('auth.password', function ($app) {
                // reference your new broker
                // the rest of the method code is unchanged
                return new PasswordBrokerManager($app);
            });
            $this->app->bind('auth.password.broker', function ($app) {
                return $app->make('auth.password')->broker();
            });
        }
    }
    

    Next, replace default password reset service provider with the custom one in your application config: app/config/app.php

    <?php
    
    return [
        "providers" => [
            ...
            // Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
            App\Providers\PasswordResetServiceProvider::class,
            ...
        ],
    ];
    

    And finally, define the getMobileForPasswordReset() method on your user model:

    <?php
    
    namespace App;
    
    use Illuminate\Foundation\Auth\User as Authenticatable;
    ...
    
    class User extends Authenticatable
    {
        ...
        public method getMobileForPasswordReset()
        {
            return $this->mobile;
        }
    }