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
2. Enable API Routing
This command will install Laravel Sanctum and publish the API routes.
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
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.
php artisan make:model Admin -m
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
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
}
Now, all you need to do is run migrations:
php artisan migrate
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
<?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
.
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:
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:
'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:
'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:
'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:
php artisan make:migration create_admin_password_reset_tokens
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
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
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}"
5. Setting Up the Controllers
Let's proceed with setting up reset controllers for admins.
- 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.
- 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
- 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
}
}
- 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:
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:
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.
As you can see the forgot password API successfully sent the password reset link to the email address.
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
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)
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",
...