DEV Community

HishamM1
HishamM1

Posted on

Building a Multi-Guard Reset Password API in Laravel 11

Resetting passwords in a Laravel web app is pretty easy; it's well-documented in the documentation. However, there isn't much in the documentation for an API-based application. And what if I had multiple guards? What should I do? We will explore this in today's article.

Brief

Our goal is to create a reset password functionality for a Laravel API with multiple guards. In our example, we have two guards: 'users' and 'admins', and we will implement the reset password feature for both.

1. Setup the Project

If you already have a project, skip to step 4



composer create-project laravel/laravel multi-guard-reset-password


Enter fullscreen mode Exit fullscreen mode

2. Enable API Routing

This command will install Laravel Sanctum and publish the API routes.



php artisan install:api


Enter fullscreen mode Exit fullscreen mode

3. Setup the Models

After installing the API, you'll be prompted to add HasApiTokens to our User model. Let's do that first:



<?php

namespace App\Models;

use Laravel\Sanctum\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use HasFactory, Notifiable, HasApiTokens;
    // Rest of code
}



Enter fullscreen mode Exit fullscreen mode

Next, we'll create the Admin model and migrations, which will be the same as the User.



php artisan make:model Admin -m


Enter fullscreen mode Exit fullscreen mode

This command will create the model and a migration file for the admins' table.

Admin migrations



// Previous code
    public function up(): void
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->timestamp('email_verified_at')->nullable();
            $table->string('password');
            $table->rememberToken();
            $table->timestamps();
        });
    }
// Next code


Enter fullscreen mode Exit fullscreen mode

Admin Model



<?php

namespace App\Models;

use Laravel\Sanctum\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;

class Admin extends Authenticatable
{
    use HasFactory, Notifiable, HasApiTokens;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];
    // Rest of the User model
}


Enter fullscreen mode Exit fullscreen mode

Now, all you need to do is run migrations:



php artisan migrate


Enter fullscreen mode Exit fullscreen mode

For faster tests, We can create an admin factory so we don't need create a register and login functionality



php artisan make:factory AdminFactory --model=Admin


Enter fullscreen mode Exit fullscreen mode


<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class AdminFactory extends Factory
{
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => Hash::make('password'),
            'remember_token' => Str::random(10),
        ];
    }
}


Enter fullscreen mode Exit fullscreen mode

Next, we need to call the factory in the database seeder in database/seeders/DatabaseSeeder.php.




namespace Database\Seeders;

use App\Models\User;
use App\Models\Admin;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        User::factory()->create([
            'name' => 'User',
            'email' => 'user@example.com',
        ]);

        Admin::factory()->create([
            'name' => 'Admin',
            'email' => 'admin@example.com',
        ]);
    }
}


Enter fullscreen mode Exit fullscreen mode

Then don't forget to run this command:



php artisan db:seed


Enter fullscreen mode Exit fullscreen mode

After finishing setting up models and migrations, we will start configuring the guards.

3. Configure the Guards

Navigate to the config/auth.php directory.

You'll see all the options for the existing guards, which are web and API.
The default guard is web, and the default password reset broker is users.
Let's write our guards first:



    'guards' => [
        'user' => [
            'driver' => 'sanctum',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'sanctum',
            'provider' => 'admins',
        ]
    ],


Enter fullscreen mode Exit fullscreen mode

You can keep the web and api guards, but in our case, we won't need them.
Now, let's define the providers, which specify how users are retrieved from our database:



    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ]
    ]


Enter fullscreen mode Exit fullscreen mode

Next, let's define the password reset tables for the providers:



    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 0, // default 60 but we set it to 0 for testing
        ],
        'admins' => [
            'provider' => 'admins',
            'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
            'expire' => 60,
            'throttle' => 0, // default 60 but we set it to 0 for testing
        ],
    ],


Enter fullscreen mode Exit fullscreen mode

For now, let's stick to the default password reset tokens table. However, you can create a new table by running this command:



php artisan make:migration create_admin_password_reset_tokens


Enter fullscreen mode Exit fullscreen mode

Then, define the table like this:



// Previous code
    public function up(): void
    {
        Schema::create('admin_password_reset_tokens', function (Blueprint $table) {
            $table->string('email')->primary();
            $table->string('token');
            $table->timestamp('created_at')->nullable();
        });
    }
// Next code


Enter fullscreen mode Exit fullscreen mode

Don't forget to run migrations after creating it.

Because we deleted the web guard, let's make the default guard users by adding a new AUTH_GUARD variable in the .env file.



AUTH_GUARD=user


Enter fullscreen mode Exit fullscreen mode

You can also change the password broker by setting the AUTH_PASSWORD_BROKER variable.

Next up, we will set up controllers to handle password reset requests for both users and admins.

4. Setting Up the Mail Provider

If you have already done it jump to the next step.
There are a lot of free options but the fastest is to use log which is the default but I will use Mailtrap



MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=#############
MAIL_PASSWORD=#############
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hisham@example.com"
MAIL_FROM_NAME="${APP_NAME}"


Enter fullscreen mode Exit fullscreen mode

5. Setting Up the Controllers

Let's proceed with setting up reset controllers for admins.

  1. Forgot Link Controller This controller will handle sending a reset link to the admin's email. ```powesherll

php artisan make:controller Admin/Auth/AdminForgotPasswordController -i

the `-i` will make it an invokable class because we want it to do only one thing

Now, let's define the controller:
`Admin/Auth/AdminForgotPasswordController.php`
```php


<?php

namespace App\Http\Controllers\Admin\Auth;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Password;

class AdminForgotPasswordController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        $request->validate([
            'email' => ['required', 'email'],
        ]);

        $status = Password::broker('admins')->sendResetLink(
            $request->only('email')
        );

        return $status === Password::RESET_LINK_SENT
            ? response()->json(['message' => __($status)])
            : response()->json(['message' => __($status)], 400);
    }
}


Enter fullscreen mode Exit fullscreen mode

This process involves extracting the email from the request using the Password facade and define which broker we will use which we already defined in the passwords option in config/auth.php

We then check the status returned by the sendResetLink method.
If the email is successfully sent, we return the status with a success code; otherwise, we return a 400 code.
While for security reasons, you could always return a success message regardless of whether the email exists or not, we'll stick to the current approach for now.

  1. Reset Password Controller This controller handles the resetting of passwords ```powesherll

php artisan make:controller Admin/Auth/AdminResetPasswordController -i


`Admin/Auth/AdminResetPasswordController.php`
```php


<?php

namespace App\Http\Controllers\Admin\Auth;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Admin;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;

class AdminResetPasswordController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        $request->validate([
            'token' => ['required'],
            'email' => ['required', 'email'],
            'password' => ['required', 'confirmed', 'min:8'],
        ]);

        $status = Password::broker('admins')->reset(
            $request->only('email', 'password', 'password_confirmation', 'token'),
            function (Admin $admin, string $password) {
                $admin->forceFill([
                    'password' => Hash::make($password)
                ]);

                $admin->save();
            }
        );

        return $status === Password::PASSWORD_RESET
            ? response()->json(['message' => __($status)])
            : response()->json(['message' => __($status)], 400);
    }
}


Enter fullscreen mode Exit fullscreen mode

Here, we receive four inputs from the client: token, email, password, and password_confirmation.

We then use another function from the Password facade called reset.
This function takes the provided credentials and a callback function. First, it verifies the credentials. If they are valid, it executes the callback function. In this function, the old password is replaced with the new password.
You can customize this function as needed.

Now the reset controllers for users

  1. Forgot Password Controller ```powesherll

php artisan make:controller User/Auth/UserForgotPasswordController -i

It will be the same as the admins' controller the only difference will be the broker.
```php


<?php

namespace App\Http\Controllers\User\Auth;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Password;

class UserForgotPasswordController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        // Previous Code
        $status = Password::broker('users')->sendResetLink(
            $request->only('email')
        );
        // Next code
     }
}


Enter fullscreen mode Exit fullscreen mode
  1. Reset Password Controller ```powesherll

php artisan make:controller User/Auth/UserResetPasswordController -i

It will be the same as the admins' controller the only difference will be the broker.
```php


<?php

namespace App\Http\Controllers\User\Auth;

use App\Models\User;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;

class ResetPasswordController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        // Previous code
        $status = Password::broker('users')->reset(
            $request->only('email', 'password', 'password_confirmation', 'token'),
            function (User $user, string $password) {
                $user->forceFill([
                    'password' => Hash::make($password)
                ]);
                $user->save();
            }
        );
        // Next Code
    }
}


Enter fullscreen mode Exit fullscreen mode

Congratulations You are almost finished! The next step is defining the redirect frontend URL that the email will send the client to

6. Configuring the Redirect URL

Navigate to the app/Providers/AppServiceProvider.php directory.

We want to customize the reset link based on the user type.
We'll achieve this by using the createUrlUsing method from the ResetPassword notification class.
For now, we'll hardcode the link, but ideally, it should be defined elsewhere.

Now, let's define what we want in the boot function:



    use Illuminate\Auth\Notifications\ResetPassword;
    use App\Models\User;
    use App\Models\Admin;
    // ...
    public function boot(): void
    {
        ResetPassword::createUrlUsing(function ($user, string $token) {
            return match (true) {
                $user instanceof Admin => 'http://admin.our-website/reset-password' . '?token=' . $token . '&email=' . urlencode($user->email),
                $user instanceof User => 'http://our-website/reset-password' . '?token=' . $token . '&email=' . urlencode($user->email),
                // other user types
                default => throw new \Exception("Invalid user type"),
            };
        });
    }


Enter fullscreen mode Exit fullscreen mode

All that's left is to define the routes!

7. Add the API Routes

Navigate to routes/api.php

Next, write the following routes or customize them according to your needs:



Route::prefix('admin')->group(function () {
    Route::post('/forgot-password', AdminForgotPasswordController::class);
    Route::post('/reset-password', AdminResetPasswordController::class);
});

Route::prefix('user')->group(function () {
    Route::post('/forgot-password', UserForgotPasswordController::class);
    Route::post('/reset-password', UserResetPasswordController::class);
});


Enter fullscreen mode Exit fullscreen mode

After following these steps, your API setup is complete! You can now freely test your API endpoints.

Next, let's proceed with testing the APIs using HTTPie on one user we created above.
User Forgot Password API Request
As you can see the forgot password API successfully sent the password reset link to the email address.
Reset Password Email
The reset password link that was sent is http://our-website/reset-password?token=83f932629e3489972c49897bdcd462beae29c0af8c5079fe8c553cc542030d3d&email=user%40example.com.
Upon redirection to this link, the front end will retrieve the token and email from the URL, allowing the user to set a new password.

Now let's try the reset password API
User Reset Password API Request
As we expected!

Similar results can be expected for the Admin APIs, with the only difference being the structure of the reset link.


I hope this article has been helpful to you. I've aimed to keep it as simple as possible, covering the essential details. While there are additional aspects we haven't got into, perhaps they could be explored in a future article. Thank you for reading!

Top comments (1)

Collapse
 
willybueno profile image
Willy Camargo • Edited

I did the same process, but when posting for the forgotten-password route I get the following error:
{
"message": "Route [password.reset] not defined.",
"exception": "Symfony\\Component\\Routing\\Exception\\RouteNotFoundException",
...