Password reset in Laravel by email or mobile
Asked Answered
C

1

7

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.

Coatbridge answered 16/11, 2019 at 7:30 Comment(5)
does your user not have an email address? or you are saying you only reference the user by a phone number on the password_resets tableDomash
I don't know if touching those classes is the way to go. I would introduce a new guard for authenticating using phone number, and override the method that sends the notification on the User model, documentationFibrous
@Domash - yes our user has email address, but is optional. Mobile number is mandatory. So we add mobile column on password reset tableCoatbridge
@nakov, i think this thread is helpful - #56589529. i m following this, but i think i miss some thingsCoatbridge
@HasanHafizPasha you can comment on the post there and find if the guy that implemented that could help you more.Fibrous
C
8

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;
    }
}
Caravan answered 16/11, 2019 at 16:32 Comment(1)
Brilliant! Been trying to figure out how exactly to do this for several days before finally finding your answer.Formal

© 2022 - 2024 — McMap. All rights reserved.