Email verification and password resetting are two integral authentication features of modern applications. In most web apps, users usually reset their lost passwords, however, they must also verify their email address before accessing the web app for the first time.
Laravel provides handy, built-in services for sending and verifying email verification requests,and securely resetting passwords. But in this tutorial, you will learn how to create custom email verification and reset passwords in Laravel.
Prerequisites
- Composer installed globally
- Laravel 8 already installed
- A Gmail account
- A database supported by Laravel
- PHP 7.4
- Postman
Email verification and resetting passwords at a glance
A password reset option is required whenever you add login and signup functionality for user authentication. Doing so, whenever users forget their password they have the means to reset it. As part of this process, there will usually be a token in the reset link, and that token must be validated. You can see a simplistic example below.
http://www.example.org/reset_password?token=169d07470f8570afdfba09f5d9452db3d74
Laravel offers a number of different authentication implementations for creating them, such as UI auth,Breeze Auth, and Jetstream, which include login, signup, email verification, and password reset functionality.
However, if you're going to build an app without these authentication packages, then you have to manually implement password reset and email verification functionality.
Custom email verification and reset password benefits
There may be a need to customize the password reset and email verification process when building APIs because default password reset and email verification from Laravel's default authentication scaffolding is primarily targeted at web applications.
In this tutorial, you’ll learn how to do this. You won't be sending tokens embedded in links, rather you’ll be sending 6-digit pins. These are suitable for both web apps and APIs — assuming you’re building them for mobile developers to consume.
Create the Laravel application
You can create a new Laravel application via Composer or install the Laravel Installer as a global Composer dependency. We'll be using the Laravel Installer in this tutorial. To create the application using it, run the following command in the terminal.
laravel new custom_authentication
This will create a Laravel application in a new directory named custom_authentication. Next, change into the new application directory by running the following command:
cd custom_authentication
Now, start the application by running the following command:
php artisan serve
By default, the project is served on port 8000 on localhost, so you can now access the application by entering http://localhost:8000 in your browser. When you do, you should see a page similar to the image below confirming that your project is running.
As the application is working, stop it by pressing Ctrl + C.
Set up the environment variables
To configure the application's database, in .env, update the database configuration settings, which you can see in the example below, to match the details for your database.
DB_CONNECTION=
DB_HOST=
DB_PORT=
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
As you will be using Gmail's SMTP Server to email users the 6-digit PIN, you next need to update the mail environment variables to match the values below. Replace the two placeholders with your Gmail username and password respectively.
MAIL_MAILER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=465
MAIL_USERNAME=<Enter your Gmail address>
MAIL_PASSWORD=<Enter your Gmail password>
MAIL_ENCRYPTION=ssl
MAIL_FROM_ADDRESS=<Your Gmail Address>
MAIL_FROM_NAME=CustomAuth
Be careful not to use quotes(“,',) or spaces. Also, you may need to clear Laravel's cache, although it's not compulsory. To do that, run the following command in the terminal:
php artisan config:cache
You can find more information about sending mails using Gmail SMTP in this article.
To use GMAIL as the SMTP server, you need to enable Less secure app access.
Set up the model and migrations
Laravel comes with a default Model file called User.php, located in the app/Models directory, and a Migration file called 2014_10_12_000000_create_users_table.php in the database/migrations directory.
Update User.php by modifying the $fillable
array, which indicates the fillable fields, to match the code example below.
protected $fillable = [
'email',
'password',
];
Then, update the up()
method of the migration file to match the example below.
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
Set up authentication
You’ll be using Laravel Sanctum in this tutorial, which is a featherweight authentication system for SPAs (Single Page Applications), mobile applications, and token-based APIs, to restrict access to several routes.
Install it using Composer, by running the command below.
composer require laravel/sanctum
Next, use the vendor:publish
Artisan command below, to publish the new Sanctum configuration and migration files to your application's config
directory.
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
Then, in app/Http/Kernel.php, add Sanctum's middleware to your api
middleware group, by uncommenting the highlighted line in the example below.
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
Run the database migrations
With those changes made, next, run the following command to execute the database migrations and provision the database.
php artisan migrate
Add email verification support
After a successful registration, users are prompted to check their email (sent via a Mailable) for a 6-digit PIN. If the PIN is a match then their email is successfully verified and they’re redirected to the home page.
To create theMailable, app/Mail/VerifyEmail.php, and its corresponding view template, resources/views/emails/verify.blade.php, run the following Artisan command:
php artisan make:mail VerifyEmail --markdown=emails.verify
Then, open app/Mail/VerifyEmail.php and update the __construct()
method to match the code below.
public function __construct($pin)
{
$this->pin=$pin;
}
Then, add the member variable, below, to the top of the class.
public $pin;
$pin
is set as a public member variable so that it can be passed from the RegistrationController
(which will be created shortly) to VerifyEmail
's constructor.
Next, update the class' build()
function to match the code below.
public function build()
{
return $this
->subject("Email Verification")
->markdown('emails.verify');
}
And after that, update the email view template, resources/views/emails/verify.blade.php, to match the code below.
@component('mail::message')
# Email Verification
Thank you for signing up.
Your six-digit code is {{$pin}}
Thanks,<br>
{{ config('app.name') }}
@endcomponent
Set up the registration controller
Next up, you need to create a controller to register a user and to send them an email with the 6-digit PIN so that they can verify their email address. To create the controller, run the following command:
php artisan make:controller RegisterController
This will create a new file, RegisterController.php, in the app\Http\Controllers directory. Next, add the following use
statements to the top of the file.
use App\Mail\VerifyEmail;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
After that, define the register()
method by adding the code below to the class.
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
if ($validator->fails()) {
return new JsonResponse(['success' => false, 'message' => $validator->errors()], 422);
}
$user = User::create([
'email' => $request->all()['email'],
'password' => Hash::make($request->all()['password']),
]);
if ($user) {
$verify2 = DB::table('password_resets')->where([
['email', $request->all()['email']]
]);
if ($verify2->exists()) {
$verify2->delete();
}
$pin = rand(100000, 999999);
DB::table('password_resets')
->insert(
[
'email' => $request->all()['email'],
'token' => $pin
]
);
}
Mail::to($request->email)->send(new VerifyEmail($pin));
$token = $user->createToken('myapptoken')->plainTextToken;
return new JsonResponse(
[
'success' => true,
'message' => 'Successful created user. Please check your email for a 6-digit pin to verify your email.',
'token' => $token
],
201
);
}
The code starts by validating all the required registration fields before registering the user. The validation rules include:
- A valid email address that is unique.
- A password with a minimum of 8 characters.
- A
password_confirmation
field to retype your password.
If registration is successful, an email is sent with a 6-digit PIN to the registered email address for email verification and a token is returned in the response. This token is used to make any, future, authorized requests.
Next, define the controller's verifyEmail()
method by pasting the code below after the register
method.
public function verifyEmail(Request $request)
{
$validator = Validator::make($request->all(), [
'token' => ['required'],
]);
if ($validator->fails()) {
return redirect()->back()->with(['message' => $validator->errors()]);
}
$select = DB::table('password_resets')
->where('email', Auth::user()->email)
->where('token', $request->token);
if ($select->get()->isEmpty()) {
return new JsonResponse(['success' => false, 'message' => "Invalid PIN"], 400);
}
$select = DB::table('password_resets')
->where('email', Auth::user()->email)
->where('token', $request->token)
->delete();
$user = User::find(Auth::user()->id);
$user->email_verified_at = Carbon::now()->getTimestamp();
$user->save();
return new JsonResponse(['success' => true, 'message' => "Email is verified"], 200);
}
This method verifies that the 6-digit PIN, retrieved from the request, is linked to an email address reset request for the current user. If it is, then the password reset request is deleted and the user's email is marked as being verified. After that, a successful response is returned.
If a token isn't provided, or the token isn't linked to a password request for the user, then the user is redirected back to the RegistrationController
with an applicable error message.
Add resend pin functionality
If the 6-digit password reset PIN expires, the user may want to request another one. To do that, define the resendPin()
method by adding the following code to the RegisterController
after the verifyEmail()
method.
public function resendPin(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'string', 'email', 'max:255'],
]);
if ($validator->fails()) {
return new JsonResponse(['success' => false, 'message' => $validator->errors()], 422);
}
$verify = DB::table('password_resets')->where([
['email', $request->all()['email']]
]);
if ($verify->exists()) {
$verify->delete();
}
$token = random_int(100000, 999999);
$password_reset = DB::table('password_resets')->insert([
'email' => $request->all()['email'],
'token' => $token,
'created_at' => Carbon::now()
]);
if ($password_reset) {
Mail::to($request->all()['email'])->send(new VerifyEmail($token));
return new JsonResponse(
[
'success' => true,
'message' => "A verification mail has been resent"
],
200
);
}
}
The code starts off by checking that the submitted email address is valid. If it is and it's already linked to a password reset request, then the matching password reset request is deleted.
Following that, a new token is generated and, along with the email address, used to create a new password reset request. If the request is created successfully, then the token is emailed to the user. In addition, a JSON response informs the user that they will receive an email with the reset token.
Set up the Middleware
The next thing to do is to create a new middleware class that will prevent users from accessing other routes until their email address is verified. To create it, run the following command.
php artisan make:middleware VerifyEmail
The command will create a new file, VerifyEmail.php, in app/Http/Middleware/. With the file created, add the following use
statements to the top of the file.
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Auth;
Then, update the handle()
method to match the following code:
public function handle(Request $request, Closure $next)
{
if (Auth::user()->email_verified_at === null) {
return new JsonResponse(
[
'success' => false,
'message' => 'Please verify your email before you can continue'
],
401
);
}
return $next($request);
}
Next, you need to link it to the application's route middleware, by adding the highlighted line below to the $routeMiddleware
array in app\Http\Kernel.php.
'verify.api'=> \App\Http\Middleware\VerifyEmail::class,
Set up the Login Controller
Next up, you need to create a controller with login and logout functionality, by running the following command:
php artisan make:controller LoginController
This will create a file named LoginController.php
in the app\Http\Controllers directory. After it's created, add the following use
statements to the top of the file.
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
Then, define the login()
and logout()
methods by copying the following code into the body of the class.
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'string', 'email', 'max:255'],
'password' => ['required', 'string', 'min:8'],
]);
if ($validator->fails()) {
return new JsonResponse(
[
'success' => false,
'message' => $validator->errors()
],
422
);
}
$user = User::where('email', $request->all()['email'])->first();
// Check Password
if (!$user || !Hash::check($request->all()['password'], $user->password)) {
return new JsonResponse(
[
'success' => false,
'message' => 'Invalid Credentials'
],
400
);
}
$token = $user->createToken('myapptoken')->plainTextToken;
return new JsonResponse(
[
'success' => true,
'token' => $token
],
200
);
}
public function logout(Request $request)
{
auth()->user()->tokens()->delete();
return new JsonResponse(
[
'success' => true,
'message' =>'Logged Out Successfully'
],
200
);
}
The login()
method requires an email address and a password. The email address must be both valid and linked to the password before a user is successfully logged in. After a successful login, a new token is assigned to the user to make unauthorized requests.
The logout()
method deletes the user's tokens so that they won't have authorized access until they log in again to obtain a new token.
Add password reset support
Before allowing a user to change their password — especially when they are not logged in — a means of ensuring that the user is valid and owns the account must be provided.
A good way to do this is to send a confirmation PIN to the registered email address using a Mailable. If the user submits the correct PIN they're then allowed to reset their password.
Create a reset password Mailable
Create theMailable, in app/Mail/ResetPassword.php, and its corresponding view template, in resources/views/emails/password.blade.php, by running the following command.
php artisan make:mail ResetPassword --markdown=emails.password
With the files created, open app/Mail/ResetPassword.php and update the __construct()
method as follows:
public function __construct($pin)
{
$this->pin=$pin;
}
Then, define the $pin
class member variable at the top of the class.
public $pin;
Next up, update the `build()` method as follows:
public function build()
{
return $this->subject("Reset Password")->markdown('emails.password');
}
Then, update the email view template in resources/views/emails/password.blade.php as follows:
@component('mail::message')
# Reset Password
Your six-digit PIN is <h4>{{$pin}}</h4>
<p>Please do not share your One Time Pin With Anyone. You made a request to reset your password. Please discard if this wasn't you.</p>
Thanks,<br>
{{ config('app.name') }}
@endcomponent
Set up the forgot password controller
Next up, you need to create a controller to help users reset their password, emailing them a reset password PIN. To do so, run the following command:
php artisan make:controller ForgotPasswordController
It will create a new file called ForgotPasswordController.php in the app\Http\Controllers directory. With it created, add the necessary use
statements to the top of the file.
use App\Mail\ResetPassword;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Validator;
Next, define the forgotPassword()
method, by pasting the code below into the class.
public function forgotPassword(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'string', 'email', 'max:255'],
]);
if ($validator->fails()) {
return new JsonResponse(['success' => false, 'message' => $validator->errors()], 422);
}
$verify = User::where('email', $request->all()['email'])->exists();
if ($verify) {
$verify2 = DB::table('password_resets')->where([
['email', $request->all()['email']]
]);
if ($verify2->exists()) {
$verify2->delete();
}
$token = random_int(100000, 999999);
$password_reset = DB::table('password_resets')->insert([
'email' => $request->all()['email'],
'token' => $token,
'created_at' => Carbon::now()
]);
if ($password_reset) {
Mail::to($request->all()['email'])->send(new ResetPassword($token));
return new JsonResponse(
[
'success' => true,
'message' => "Please check your email for a 6 digit pin"
],
200
);
}
} else {
return new JsonResponse(
[
'success' => false,
'message' => "This email does not exist"
],
400
);
}
}
The forgotPassword()
method validates the email address sent in the request and checks if it is linked to an existing user, and if it is linked to a reset password request. If it is, then the current reset password request is replaced with another one. It then finishes up by mailing a reset password email to the user, which contains the reset password pin.
Next, add the verifyPin()
method, below, after the forgotPassword
method.
public function verifyPin(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'string', 'email', 'max:255'],
'token' => ['required'],
]);
if ($validator->fails()) {
return new JsonResponse(['success' => false, 'message' => $validator->errors()], 422);
}
$check = DB::table('password_resets')->where([
['email', $request->all()['email']],
['token', $request->all()['token']],
]);
if ($check->exists()) {
$difference = Carbon::now()->diffInSeconds($check->first()->created_at);
if ($difference > 3600) {
return new JsonResponse(['success' => false, 'message' => "Token Expired"], 400);
}
$delete = DB::table('password_resets')->where([
['email', $request->all()['email']],
['token', $request->all()['token']],
])->delete();
return new JsonResponse(
[
'success' => true,
'message' => "You can now reset your password"
],
200
);
} else {
return new JsonResponse(
[
'success' => false,
'message' => "Invalid token"
],
401
);
}
}
This method requires the user's email address and the 6-digit PIN sent to them, after the user requested to reset their password. If the pin hasn't expired and is linked to a reset password request with the users' email address, the user is allowed to reset their password. Otherwise an applicable error message is returned.
Set up the reset password controller
Next, you need to create a controller to reset a user's password. To create the controller, run the following command:
php artisan make:controller ResetPasswordController
It will create a new file named ResetPasswordController.php in the app\Http\Controllers directory. With it created, add the following use
statements to the top of the file.
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
Then, implement the resetPassword()
method which allows the user to reset their password, by adding code below to the body of the class.
public function resetPassword(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => ['required', 'string', 'email', 'max:255'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
if ($validator->fails()) {
return new JsonResponse(['success' => false, 'message' => $validator->errors()], 422);
}
$user = User::where('email',$request->email);
$user->update([
'password'=>Hash::make($request->password)
]);
$token = $user->first()->createToken('myapptoken')->plainTextToken;
return new JsonResponse(
[
'success' => true,
'message' => "Your password has been reset",
'token'=>$token
],
200
);
}
Set up the required routes
Laravel's routes are stored in routes/web.php for web apps and in routes/api.php for APIs. For this guide, you’ll be storing them in routes/api.php.
The logout route will be protected with the auth
and veriify.api
middleware, because only logged in users can log out, and only verified users can have access to other endpoints. The verify email route will be protected with the auth
middleware. The other routes don’t need users to be authenticated or verified, so don't need to be protected by any middleware.
To do this, add the route definitions below to the end of routes/api.php.
Route::post(
'/register',
[App\Http\Controllers\RegisterController::class, 'register']
)->name('register');
Route::match(
['get', 'post'],
'/login',
[App\Http\Controllers\LoginController::class, 'login']
)->name('login');
Route::post(
'/resend/email/token',
[App\Http\Controllers\RegisterController::class, 'resendPin']
)->name('resendPin');
Route::middleware('auth:sanctum')->group(function () {
Route::post(
'email/verify',
[App\Http\Controllers\RegisterController::class, 'verifyEmail']
);
Route::middleware('verify.api')->group(function () {
Route::post(
'/logout',
[App\Http\Controllers\LoginController::class, 'logout']
);
});
});
Route::post(
'/forgot-password',
[App\Http\Controllers\ForgotPasswordController::class, 'forgotPassword']
);
Route::post(
'/verify/pin',
[App\Http\Controllers\ForgotPasswordController::class, 'verifyPin']
);
Route::post(
'/reset-password',
[App\Http\Controllers\ResetPasswordController::class, 'resetPassword']
);
The verifyEmail
and logout
routes have been protected with the sanctum authentication guard, auth:sanctum
. This ensures that only authenticated users can access them.
Testing the Application
With the API created, it's time to test it and confirm that it is functioning properly. There are various ways to do so, such as using Postman or cURL, but we'll be using Postman.
Before we can do that, however, start the application by running the command below.
php artisan serve
Test the register functionality
The first thing to do is to test the register and login functionality. To do that, create a new POST request in Postman using the URL http://localhost:8000/api/register
. Then, under the Body tab:
- Choose the "form-data" encoding type
- Add a request variable,
email
, and set this to your email address. - Add a second request variable,
password
, and set this to your password. Its complexity isn't important, but it must be at least 8 characters long. - Add a third request variable,
password_confirmation
, and set it to the same value as you entered forpassword
.
With the request configured, send it by clicking Send. You should see a JSON response similar to the one below in the response body in Postman.
Test the verify email functionality
To do this, you need three things:
- The email address which you registered with.
- The 6-digit PIN which was emailed to you.
- The authentication token. You'll find this in the
token
field in the response to the login request which you just completed. This is required, since the verify email endpoint is protected.
Create a new POST request in Postman using the URL http://localhost:8000/api/email/verify
.
Then, under the Body tab:
- Choose the "form-data" encoding type
- Add a request variable,
email
, and set its value to the email address you registered with. - Add another request variable,
token
and set its value to the 6-digit PIN that was emailed to you.
Then, under the Authorization tab (it's named Auth in newer versions of Postman)::
- Change the "Type" dropdown list to "Bearer Token"
- Set the value of the "Token" field to be the token which was returned in the
token
field of the response to the login request.
Then, submit the request by clicking Send. If successful, you should see a response body similar to the example below.
{
"success": true,
"message": "Email is verified"
}
Test the login functionality
Next, let's test logging in. To do that, create a new POST request in Postman using the URL http://localhost:8000/api/login
. Then, under the Body tab:
- Choose the "raw" encoding type
- Next to the encoding type dropdown, change "TEXT" to "JSON"
- Replace the placeholders in the JSON below with your email address and password and paste it as the request's body
{
"email": "<your email address>",
"password": "<your password>"
}
Then, submit the request by clicking Send. If successful, you should see a response body similar to in the screenshot below.
Test the logout functionality
To do this, create a new POST request in Postman, using the URL http://localhost:8000/api/logout
.
Then, under the Authorization tab:
- Change the "Type" dropdown list to "Bearer Token"
- Set the value of the "Token" field to be the token which was returned in the
token
field of the response to the login request.
Then, submit the request by clicking Send. If successful, you should see a response body similar to the example below.
{
"success": true,
"message": "Email is verified"
}
Test the password reset functionality
Moving on to testing password reset, first you need to request the 6-digit PIN, then you can reset the password. The only required field is email
.
To do this, create a new POST request in Postman, using the URL http://localhost:8000/api/reset-password
. Then, under the Body tab:
- Choose the "form-data" encoding type
- Add a request variable,
email
, and set its value to the email address you registered with
Then, submit the request by clicking Send. If successful, you should see a response body similar to the example below.
{
"success": true,
"message": "Please check your email for a 6 digit pin"
}
Next, submit the PIN you were emailed for verification. To do this, create a new POST request in Postman, using the URL http://localhost:8000/api/verify/pin
. Then, under the Body tab:
- Choose the "form-data" encoding type
- Add a request variable,
email
, and set its value to the email address you registered with - Add another request variable,
token
and set its value to the 6-digit PIN that was just emailed to you, in the Reset Password email.
Then, submit the request by clicking Send. If successful, you should see a response body similar to the example below.
{
"success": true,
"message": "You can now reset your password"
}
As the PIN was verified, you can now reset your password. To do this, create a new POST request in Postman, using the URL http://localhost:8000/api/reset-password
. Then, under the Body tab:
- Choose the "form-data" encoding type
- Add a request variable,
email
, and set its value to the email address you registered with. - Add a second request variable,
password
, and set its value to your new password. - Add a third request variable,
password_confirmation
, and set its value to your new password as well.
Then, submit the request by clicking Send. If successful, you should see a response body similar to the example below.
{
"success": true,
"message": "Your password has been reset",
"token": "11|V11TAqTwLfKNsVyrJz47sOsbLuQI9CmRGZibwexj"
}
Conclusion
In this tutorial, you learned how to create custom Forgot Password and Email Verification functionality in Laravel. Check the official documentation to know more about Laravel's default email verification and reset password. The code for this project is open-source and availableon GitHub.
Funke is a tech lover with a keen interest in building and promoting sustainable tech communities especially among teenagers and young adults. She is a backend developer, who is passionate about communal growth and has published articles on many blogs including her personal blog.
When she is not writing codes or creating technical content she is rendering humanity service to her local community solving societal issues. You can reach out to her on Twitter and Github.