Building a Multi-Guard Reset Password API in Laravel 11
Tags: laravel, api, backend, php
Published: 2024-03-18 18:00:15

Resetting passwords in a Laravel web app is pretty easy; it's well-documented in the [documentation](https://laravel.com/docs/11.x/passwords). 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
```powershell
composer create-project laravel/laravel multi-guard-reset-password
```
## 2. Enable API Routing
This command will install Laravel Sanctum and publish the API routes.
```powershell
php artisan install:api
```
## 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
<?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
}

  ```
Next, we'll create the Admin model and migrations, which will be the same as the User.
```powershell
php artisan make:model Admin -m
```
This command will create the model and a migration file for the admins' table.

### Admin migrations
```php
// 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
```
### Admin Model
```php
<?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
}
```
Now, all you need to do is run migrations:
```powershell
php artisan migrate
```
For faster tests, We can create an admin factory so we don't need create a register and login functionality
```powershell
php artisan make:factory AdminFactory --model=Admin
```

```php
<?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),
        ];
    }
}
```
Next, we need to call the factory in the database seeder in `database/seeders/DatabaseSeeder.php`.
```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',
        ]);
    }
}
```
Then don't forget to run this command:
```powershell
php artisan db:seed
```

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:

```php
    'guards' => [
        'user' => [
            'driver' => 'sanctum',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'sanctum',
            'provider' => 'admins',
        ]
    ],
```
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:
```php
    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ]
    ]
```
Next, let's define the password reset tables for the providers:
```php
    '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
        ],
    ],
```
For now, let's stick to the default password reset tokens table. However, you can create a new table by running this command:
```powerhsell
php artisan make:migration create_admin_password_reset_tokens
```
Then, define the table like this:
```php
// 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
```
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. 
```bash
AUTH_GUARD=user
```
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](https://mailtrap.io/home)
```bash
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}"
```
## 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);
    }
}
```
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.

2. 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);
    }
}
```
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
     }
}
```
2. 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
    }
}
```

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:
```php
    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"),
            };
        });
    }
```

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:

```php
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);
});
```
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sw7qx02htrento0mcn7r.png)
As you can see the forgot password API successfully sent the password reset link to the email address.
![Reset Password Email](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iw7nun5k27worwns3hq2.png)
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1057zrh19fymwu9gkd23.png)
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!