Building API-based Password Recovery in Laravel 7 for Vue/React/Angular Single Page Applications
For my current project I am building a single page Vue application that communicates with a Laravel backend using only API calls.
This means the default methods that Laravel provides for user registration and password recovery need to be overridden in several places in order to the replace web pages with JSON responses.
While this seems like it should be straightforward — and ultimately the code that needs to change is pretty simple — the defaults that Laravel provides are buried under several layers of abstraction, which makes it tricky to parse out exactly where changes need to be made.
There’s also not much documentation on this topic that is up to date with Laravel 7, so it wound up taking me a bit of trial and error to piece everything together.
So if have found yourself in a similar position and Google brought you here, then you are in luck! In this guide, I’m going to attempt to walk everything you need to do to modify Laravel 7’s defaut password recovery in order to make it work through an API interface.
Prerequisites:
For this article, I am assuming that you already know how to build frontend forms using your language of choice and how to post form data to an API endpoint using API client like axios.
I built my frontend application in Vue, but this should work exactly the same for React, Angular or any other Javascript framework, as we’ll be focusing exclusively on the backend Laravel code in this guide.
Before getting started, you should alsohave Laravel UI installed in your Laravel application and have already used it to generate Laravel’s default Auth controllers.
If not, you will need to install Laravel UI using npm and then run php artisan ui vue — auth to generate the default Auth controllers and migrations, and then run php artisan migrate to perform the migrations and generate the required database tables.
You can learn more about setting up Laravel UI from the offical Laravel 7 docs.
Laravel UI includes both user registration and password recovery functions. I don’t address user registration in this guide, but at a minimum you should have already created a few sample users in your Users database before getting started with this.
Understanding Laravel’s default password recovery
Ok, so at this point I am assuming tht you have Laravel’s default password recovery code set up and have verified that it all works through a web browser.
Let’s take a look at what Laravel has created by default. You should have an App\Http\Controllers\Auth\ directory that contains several Controllers:
Installing the default Auth also added a line to our routes/web.php that generates each the default routes required for user login/registration and password recovery:
Auth::routes();
While this one-liner is convenient if you are using the default code, we need to see out what those routes actually are and figure out which Controller actions they point to.
We can get this information by running php artisan route:list
from the command line to show all of the active routes in our Laravel application.
For our present purposes we are interested in the routes associated with the ForgetPasswordController and the ResetPasswordController.
Here is an abridged version of the routes we are mainly interested in:
The ForgotPasswordController handles the first part of the password recovery process — collecting an email address and generating a password recovery link.
The ResetPasswordController takes the recovery link and shows the user the form to enter in their new password.
Now if you open up either of these controllers and look inside you will see that they are mostly empty. That’s because these controllers both rely on specific Laravel traits which contain all of the default logic.
Specifically the ForgotPasswordController includes the Illuminate\Foundation\Auth\SendsPasswordResetEmails trait, while the ResetPasswordController includes the Illuminate\Foundation\Auth\ResetsPasswords trait.
These traits actually contain most of the default code that is used in the password recovery process.
Its important to note that in Laravel 7, the default Authentication code was moved out of Laravel core and in to Laravel UI. That means, you wont be able to find the latest code for these traits under the usual Laravel API documentation. However, you can view the Laravel UI code directly on Github.
Okay, so lets review the steps in Laravel’s password recovery process:
GET "password/reset" ForgotPasswordController@showLinkRequestForm
- Show user the Forgot Password form and collect user’s email address
From here, the user enters the email address associated with their account into the password recovery form.
Next:
POST "password/email" ForgotPasswordController@sendResetLinkEmail
We post the email address submitted by the users back to the server, then:
- Validate that the email provided exists in the Users database
- If email is valid, generate a password recovery token
- Send an email to the user with a password recovery link that contains the recovery token
- If a valid user is not found, reload page with an error messsage
Once the user receives their email and clicks the password recovery link they are taken to the Password Reset form:
GET "password/reset/{token}" ResetPasswordController@showResetForm
- Show the password reset form.
The user enters their desired password and submits the form (along with the password reset token).
POST "password/reset" ResetPasswordController@reset
We post the new desired password (from the form) and the password recovery token (from the url) back to the server, then:
- Validate the new password and validate that the token provided is valid and has not yet expired
- If valid, update the user’s password in the Users database
- If invalid, reload the form with an error messsage
Changing Laravel’s default code to allow Password Recovery via API
Our goal is to replace the user-facing forms and error messages with our own frontend, and then use API calls to send the responses to and from Laravel. However we still want to let Laravel handle all of the backend parts of generating tokens, sending emails and updating the user database.
Let’s finally dive in and look at the code changes we will need to make in order to get this all to work:
Step 1: Add Routes to routes/api.php
To get started, we’ll need to set up the routes that our forms will POST to with our user data. These routes should go into your routes/api.php file and should NOT require any user authorization to access since our users will not be logged in when they request a password reset.
Route::get(‘password/reset/{token}’, ‘ResetPasswordAPIController@showResetForm’)->name(‘password.reset’); // --> we'll delete this laterRoute::post(‘password/reset’, ‘ResetPasswordAPIController@reset’);Route::post(‘password/email’, ‘ForgotPasswordAPIController@sendResetLinkEmail’)->name(‘password.email’);
The POST routes “password/reset” and “password/email” are the two endpoints where our API calls will pass the user input collected from our frontend forms.
Since we’ll be building the user facing forms in Javascript, we will only need the POST routes in our final application. However, Laravel expects to find a named route for “password.reset” when it generates the password reset email and will throw errors if it doesn’t find it. Therefore you may find yourself dealing with cryptic errors if you delete the GET route for ‘password/reset/{token}’ before you have made changes to the password reset email (which we will get to later).
To save yourself from a few headaches, I recommend leaving this in pace for now and then removing it only after you have everything else working.
Once the routes have been added to your your routes/api.php, we can remove the Auth::routes()
line from you routes/web.php.
Step 2: Modify your controllers
The default controllers created by Laravel UI are located under Http/Controllers/Auth.
I chose to leave the default controllers intact in case I ever needed to use them in the future. So I simply copied the existing Auth/ForgotPasswordController.php and Auth/ResetPasswordController.php to new controllers named ForgotPasswordAPIController.php and ResetPasswordAPIController.php and put them under my main Controller directory.
You will also need to import several Laravel classes into your controllers, as shown below.
So far we should have:
ForgotPasswordAPIController.php
<?phpnamespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Validator;class ForgotPasswordAPIController extends Controller
{use SendsPasswordResetEmails;
ResetPasswordAPIController.php
<?phpnamespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Validator;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Foundation\Auth\ResetsPasswords;class ResetPasswordAPIController extends Controller
{use ResetsPasswords;
Be sure to update the class name of your controller, and also make sure that the namespace references accurately reflects the location where your controllers are located.
Finally, make sure to include the SendsPasswordResetEmails and ResetsPasswords traits within the body of each respective class.
Modifying the ForgotPasswordAPIController:
As you can see, the “password/reset” route calls the sendResetLinkEmail method of our ForgotPasswordAPIController. To see the actual method being executed here we need to look a the library file for Laravel UI’s SendsPasswordResetEmails trait:
public function sendResetLinkEmail(Request $request)
{$this->validateEmail($request);
$response = $this->broker()->sendResetLink(
$this->credentials($request));return $response == Password::RESET_LINK_SENT
? $this->sendResetLinkResponse($request, $response)
: $this->sendResetLinkFailedResponse($request, $response);}
We can copy this function to our ForgotPasswordAPIController but we still need to make some changes.
The problem with the default code is that $this->validateEmail()
will attempt to perform a redirect to a Laravel form in the event of a validation error. Instead, we need to capture those errors and return a JSON response.
We can do that by replacing this line with something like this :
$validator = Validator::make($request->all(), [
'email' => ['required', 'email', 'max:255' ],
]);if ($validator->fails()) {
return response()->json(['error' => $validator->errors()], 422);
}
This basically allows us to validate user input manually and then generate a JSON response containing an error message if the validation fails.
Similarly, we’ll need to override the sendResetLinkResponse and sendResetLinkFailedResponse methods to return JSON as well:
Here is the full class:
For the ResetPasswordAPIController, we need to do mostly the same. Here is the full code:
Note that there was one additional change required here:
By default, the ResetPassword method will attempt to automatically log the user into your Laravel app upon completion of a successful password reset. Since we obviously don’t want to log the user into the Laravel app, you should delete or comment out those lines.
Step 3: Testing the API endpoints
If everything has gone well to this point, you should be able to verify that everything is working in Laravel by submitting POST requests to the two POST routes we created in Step 1:
http://yourlavavelapp.com/api/password/reset
http://yourlavavelapp.com/api/password/email
I recommend using something like Postman to make sample requests to these endpoints and check the responses.
You’ll want to test out three things for each endpoint:
- Submit correct informaton and getting a success response (200).
- Submit information that causes the form to fail initial validation (e.g. “Password missing” or “Password confirmation does not match password”).
- Submit information that passes initial validation, but still fails for some other reason (e.g. “We cannot find a user with that email address”).
The different error scenarios described above each reach different points in our controllers, so you’ll want to ensure that you are getting the proper JSON error responses from each result.
The “/api/password/email” endpoint simply expects a registered email address and should be pretty easy to test:
To test out the “/api/password/reset” endpoint, we’ll need to submit a valid password recovery token along with the email, password and password confirmation fields:
In order to provide a valid recovery token, first submit an intial password recovery request to the “/api/password/email” endpoint, then check your password_resets table to retrieve the token saved there.
Step 4: Changing the Password Recovery email notification
We are almost done.
Before we finish, we also need to update the password recovery email that is sent to the user after they fill out the Forgot Password form. By default, the the email includes a password reset link that points back the Laravel app. We need to modify this link to point to to our application frontend’s password recovery page instead.
Remember this line from our routes/api.php?
Route::get('password/reset/{token}', 'ResetPasswordAPIController@showResetForm')->name('password.reset'); //we can delete this later
By default, Laravel will look for a named route with the name “password.reset” to generate the password reset link. Therefore we need to change the default email notification to point to our frontend application instead.
There are actually several ways we can do this, but the quickest and easiest way is to simply add the following code to the “boot” method of your App/Providers/AppServiceProviders.php file with the url to your frontend password reset form:
ResetPassword::createUrlUsing(function ($notifiable, $token) {
return "http://my-vue-app.com/password/reset/{$token}";
});
You can also check out this post from Stack Overflow for alternate ways of handling this in Laravel.
Whew! After completing the previous steps (and making an appropriate blood sacrifice to your diety of choice) you should hopefully have everything working!
Once everything is working, clean up the routes to get rid of anything you no longer need.
Other helpful references:
I got some help in figuring out how to make modifications to the password reset flow from this article: https://www.landdigital.agency/blog/laravel-password-reset-via-api-using-jwt-for-authentication/
I also found the following article pretty helpful for getting started with Single Page Applications on top of Laravel in general: https://blog.pusher.com/web-application-laravel-vue-part-2/