Laravel 9 provides a built-in authentication system for securing your application. There's always a restricted area in your application that requires you to grant access to allocated users. To do so, you need to identify the user trying to access this area and check whether they're allowed access. This is called Authentication and is a general component in Laravel and web applications on the internet.
Once you've identified the user and allowed access, you need to check what actions this user can perform. For instance, perhaps the user can create new records but not delete existing ones. This is also called Authorization and is a standard practice in Laravel and most web applications.
Authentication in Laravel
How does Laravel handle authentication and authorization? The answer is in this first article in a series on building MVC applications in Laravel.
Laravel uses guards to determine how a user is authenticated for each request. Guards essentially serve as the gatekeeper for your application, granting or denying access accordingly. Laravel provides several built-in guards, including.
- Session Guard: This guard uses session data to authenticate users. A session is created when a user logs in and their session ID is stored in a cookie. On subsequent requests, the session guard retrieves the session ID from the cookie, retrieves the corresponding session data, and uses it to authenticate the user.
- Token Guard: This guard uses API tokens to authenticate users. Instead of storing user data in a session, a token guard stores a unique API token in a database table and sends it to the client in an HTTP header. On subsequent requests, the token guard retrieves the token from the header to authenticate the user.
With both guards, the user provides the credentials as an email address and password.
In the Session Guard, Laravel matches the provided credentials with the records stored in the database. If there's a match, it stores the User ID inside the Session Data. The next time the user visits the application, Laravel checks whether an Auth Cookie holds a Session ID. If one is found, Laravel extracts the User ID from the Session Data and authenticates the user accordingly.
In the Token Guard, Laravel matches the provided credentials with the records stored in the database. Laravel generates a token representing the authenticated user's session if a match exists. This token is usually stored in a JSON Web Token (JWT) and contains the following information:
- iss (Issuer): Identifies the issuing entity of the token, usually the name or identifier of the Laravel application.
- sub (Subject): Identifies the token's subject, which is the user who's being authenticated. This field typically contains an identifier for the user, such as their unique ID in the database.
- iat (Issued At): When the token was issued.
- exp (Expiration Time): When the token expires.
In addition to these standard fields, you can add custom claims to the token to store additional information about the user, such as their permissions, roles, etc.
Laravel then returns this token to the front-end. It can store the token inside Local Storage or a cookie (recommended). On subsequent requests to the server, the client passes the JWT in the HTTP headers, and the server uses the information in the JWT to identify and authenticate the user for that request.
Login, Logout, and Auth Middleware
In this section, let's explore how Laravel handles authenticating users using the User model, Login View, Login Controller, Logout Controller, Routes, and Auth middleware.
I started by creating a new Laravel app. There are several methods for creating a new Laravel app. I chose the Docker-based one using the Laravel Sail package. You can read more about Laravel installation by following this URL (https://laravel.com/docs/10.x/installation).
Choose the method that best suits you. Before you start, make sure you have Docker running on your computer.
I'm using a MacBook Pro. I start by running this command:
curl -s \
"https://laravel.build/laravel-authentication"\
| bash
This creates a new Laravel application on your computer under the directory named laravel-authentication.
After the installer finishes, run the following commands:
- Build up the Docker container.
./vendor/bin/sail up
- Install the NPM packages.
./vendor/bin/sail npm install
- Serve the application.
./vendor/bin/sail run dev
The application is now accessible at http://localhost
. Open the URL in the browser, and you'll see the same view as in Figure 1.
Next, let's install the Laravel Breeze starter kit. The Laravel team provides this starter kit for scaffolding Laravel authentication and Profile management. This is my ultimate choice when starting a new Laravel project. Trust me: It saves you a lot of time! You can read more about Laravel Breeze here (https://laravel.com/docs/10.x/starter-kits#laravel-breeze).
Laravel Breeze Installation
Laravel Breeze comes in four flavors:
- Breeze & Blade
- Breeze & React
- Breeze & Vue
- Breeze & Next.js/API
I'll use Breeze and Vue for this article.
Run the following commands to install Laravel Breeze, VueJS, and InertiaJS together.
./vendor/bin/sail composer require \laravel/breeze -dev
./vendor/bin/sail artisan breeze:install vue
./vendor/bin/sail artisan migrate
./vendor/bin/sail npm install
./vendor/bin/sail npm run dev
By installing Laravel Breeze, you've installed a ton of new things in your application. Let me list them briefly, but you can always check this repository commit titled (install Laravel Breeze package) to check every new file and configuration added by the package.
In addition to Breeze Starter Kit, Laravel also offers the JetStream Starter Kit, which is a more advanced option.
More importantly, Laravel Breeze does the following:
- Adds Vue, InertiaJS, and Tailwindcss
- Configures InertiaJS
- Configures Tailwindcss
- Scaffolds a front-end application using Vue/InertiaJS
- Adds Laravel Authentication controllers, views, and routes:
- Login
- Logout
- Register new user
- Forgot password
- Reset password
- Email verification
- Profile management
- Auth routes
Let's explore each of these sections in more detail.
Users in Laravel
Laravel represents a user in the application through the User model (app\Models\User.php
). It backs up this model in the database by storing user information inside the users' database table. Figure 2. Shows the users' table structure.
When I first created the Laravel application, it automatically added a database/factories/UserFactory.php
. Laravel uses the factory class to generate test data for the application. Listing 1 has the entire UserFactory.php
file.
Listing 1: UserFactory.php file
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
public function definition()
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10.../igi',
'remember_token' => Str::random(10),
];
}
public function unverified()
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
Notice how Laravel hashes the password and never stores plain text passwords. When the user tries to log in, it hashes the incoming password and compares it to the one stored in the database.
In Laravel, the Factory and Seeder classes work together to generate test data for your application.
- Factory: A factory class defines the data structure you want to generate. You can specify the attributes and values for each field in the model. The factory class uses Faker, a library for generating fake data, to fill in the values for each field.
- Seeder: A seeder class calls the factory class and generates the data. You can specify how many records you want to create and what data type to generate. The seeder class inserts the data into the database.
Locate the database/seeders/DatabaseSeeder.php
file, and inside the run()
method, add the following line of code:
\App\Models\User::factory(1)->create();
This line inserts a new record into the users' database table.
Run this command to seed the database with a testing user record:
./vender/bin/sail db:seed
You can use factories and seeders to feed any testing data you want in your application. Generally, it's a best practice to provide factories and seeders to test your application locally.
User Registration
Laravel Breeze places the Register view inside the resources/js/Pages/Auth/Register.vue
Vue component. Listing 2 shows a simplified version of the Register.vue
component.
Listing 2: Register.vue component
<script setup>
const form = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
terms: false,
});
const submit = () => {
form.post(route('register'), {
onFinish: () => form.reset(
'password',
'password_confirmation'
),
});
};
</script>
<template>
<form @submit.prevent="submit">
<div>
<TextInput id="name"
type="text"
v-model="form.name"
required />
<InputError :message="form.errors.name" />
</div>
<div>
<TextInput id="email"
type="email"
v-model="form.email"
required />
<InputError :message="form.errors.email" />
</div>
<div>
<TextInput id="password"
type="password"
v-model="form.password"
required/>
<InputError :message="form.errors.password" />
</div>
<div>
<TextInput id="password_confirmation"
type="password"
v-model="form.password_confirmation"
required />
<InputError :message="form.errors.password_confirmation" />
</div>
<div>
<PrimaryButton>
Register
</PrimaryButton>
</div>
</form>
</template>
The script section uses the useForm
composable that InertiaJS offers to create a form object with five fields: name, email, password, password_confirmation, and terms.
The submit function is then defined to submit the form data to the server. The form is posted to the Register POST route with the form.post()
method. The onFinish()
option is set, which specifies a function to be executed after the request has finished. In this case, the function form.reset('password', 'password_confirmation')
is called, which resets the password and password_confirmation fields in the form.
The template section defines the Register form. It's defined with a submit.prevent()
event listener, which prevents the default form submission behavior and calls the submit function instead. The form contains five input fields bound to a corresponding field in the form state object using the v-model
directive. The ID
attribute specifies a unique identifier for each field. Finally, the PrimaryButton component displays a button for submitting the form.
Let's check the Register routes Laravel Breeze defines inside the routes/auth.php
file.
Route::get('register', [RegisteredUserController::class, 'create']
)->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
The first route is a GET route for the /register
URL, and it maps to the create()
method of the RegisteredUserController
class. The name()
method is used to specify a name for the route, which can be used later in the application to generate URLs or to refer to the route by name.
The second route is a POST route for the /register
URL, and it maps to the store()
method of the RegisteredUserController
class. This route is intended to receive form submissions from the login form.
Laravel Breeze stores all Auth routes in a separate file located at
routes/auth.php
Listing 3 shows the source code for the store()
method Laravel Breeze defines on the RegisteredUserController
class. This code authenticates the user into the application.
Listing 3: RegisteredUserController store() method
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => 'required|string|max:255',
'email' => [
'required',
'string',
'email',
'max:255',
'unique:'.User::class
],
'password' => [
'required',
'confirmed',
Rules\Password::defaults()
],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(RouteServiceProvider::HOME);
}
The store()
method takes a Request
object as an argument, which contains the user input data. The method first validates the user input data using the validate()
method on the $request
object. The validation rules specify that the name field is required and must be a string with a maximum length of 255 characters. The email field is also required. It must be a string, must be a valid email address, and must be unique in the users' table. The password field is required and must be confirmed. The Confirmed validation rule expects a matching field of password_confirmation to exist in the $request
object. Finally, the Rules\Password::defaults()
method is used to specify default validation rules for password fields in Laravel.
You can read more about Laravel Validation here (https://laravel.com/docs/10.x/validation).
If the validation passes, a new user is created by calling the create
method on the User model and passing an array of data that includes the user's name, email, and hashed password. The Hash::make()
method is used to hash the password.
An event Registered
is then fired with the newly created user as its argument. Locate the app\Providers\EventServiceProvider.php
file and notice the $listen
variable.
protected $listen = [
Registered::class => [SendEmailVerificationNotification::class,],
];
Laravel adds an event listener for the Registered event. The SendEmailVerificationNotification
class sends an email verification to the registered user if the email verification feature is enabled. I'll look at the email verification feature in the coming sections.
Back to the store()
method, the Auth::login()
method is called to log the user in, and finally, the method returns a redirect to the home page using the redirect
method and passing the RouteServiceProvider::HOME
constant as its argument.
User Login
Laravel Breeze places the Login view inside the resources/js/Pages/Auth/Login.vue
Vue component. Listing 4 shows a simplified version of the Login.vue
component.
Listing 4: Login.vue component
<script setup>
const form = useForm({
email: '',
password: '',
remember: false,
});
const submit = () => {
form.post(route('login'), {
onFinish: () => form.reset('password'),
});
};
</script>
<template>
<form @submit.prevent="submit">
<div>
<TextInput
id="email"
type="email"
v-model="form.email"
required
/>
<InputError :message="form.errors.email" />
</div>
<div>
<TextInput
id="password"
type="password"
v-model="form.password"
required
/>
<InputError :message="form.errors.password" />
</div>
<div>
<Checkbox name="remember" v-model:checked="form.remember" />
<span class="ml-2 text-sm text-gray-600">Remember me</span>
</div>
<PrimaryButton>
Log in
</PrimaryButton>
</form>
</template>
The script section uses the useForm
composable that InertiaJS offers to create a form object with the properties email, password, and remember me. The properties are initialized with the default values of an empty string, an empty string, and a false
string, respectively.
The submit function is then defined to submit the form data to the server. The form is posted to the Login POST route with the form.post()
method and the onFinish()
option used to reset the password field after completing the form submission.
The template section defines the Login form's structure, which comprises text inputs for the email and password fields, an error message component for displaying validation errors, a checkbox for remembering the user, and a primary button for submitting the form. The form is bound to the submit event using the @submit.prevent
Vue directive, and the submit function defined in the script section is called when the form is submitted. The text inputs and the checkbox are bound to the properties of the form object using the v-model
Vue directive. The error message components display the error messages from the form object using the message prop.
Let's check the Login routes Laravel Breeze defines inside the routes/auth.php
file.
Route::get('login', [AuthenticatedSessionController::class, 'create']
)->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store');
The first route is a GET route for the /login URL, and it maps to the create()
method of the AuthenticatedSessionController
class. The name()
method is used to specify a name for the route, which can be used later in the application to generate URLs or to refer to the route by name.
The second route is a POST route for the /login URL, and it maps to the store()
method of the AuthenticatedSessionController
class. This route is intended to receive form submissions from the login form.
Listing 5 shows the source code for the store()
method Laravel Breeze defines on the AuthenticatedSessionController
class. This code authenticates the user into the application.
Listing 5: AuthenticatedSessionController store() method
public function store(LoginRequest $request):
RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
The store()
method takes a LoginRequest
FormRequest object as an argument and returns a RedirectResponse
object. The method starts by calling the authenticate()
method on the LoginRequest
object, which is used to validate the incoming login request and verify that the user-provided credentials are valid.
The next line calls the regenerate()
method on the session
object, which regenerates the user's session ID to increase the security of the session.
Finally, the method returns a redirect
response to the intended destination. Laravel tracks the intended destination when the user tries to visit a private page before logging in to the application. If there is no such intended destination, Laravel redirects the user to the URL specified in the constant RouteServicePovider::HOME
, which is, in this case, the /dashboard
URL.
Let's inspect the LoginRequest::authentiate()
method closely. Listing 6 shows the entire source code for this method.
Listing 6: LoginRequest::authenticate() method
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (
!Auth::attempt(
$this->only('email', 'password'),
$this->boolean('remember')
)
) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages(
['email' => trans('auth.failed'),]);
}
RateLimiter::clear($this->throttleKey());
}
The authenticate()
function accepts no parameters and returns nothing (void method).
The first line calls the ensureIsNotRateLimited()
method, which checks if the user is rate limited (e.g., if they've tried to log in too many times in a short period). If the user is rate limited, the method throws an exception.
The second line uses the Auth facade's attempt()
method to attempt to log the user in with the email and password provided in the request. The method also accepts a Boolean argument, determining whether the user's session should be remembered.
If the authentication attempt fails, the method calls the hit()
method on the RateLimiter
class to increase the user's failed attempts. The method then throws a ValidationException
with a message indicating that the authentication has failed.
If the authentication attempt is successful, the method calls the clear()
method on the RateLimiter
class to clear the number of failed attempts for the user.
That's how Laravel Breeze validates the user's credentials and attempts a login to the application.
You must have noticed that the attempt()
method accepts as a third parameter whether it should remember the user's login session. If Laravel resolves this parameter to a Boolean True
value, it generates a remember-me token.
In Laravel, the remember token is a random string generated and stored in the database when the user checks the “Remember Me” checkbox during login. This token is then used to identify the user on subsequent visits.
Typically, the remember me token is a hash of a combination of the user's ID, a random value, and the current timestamp. This hash is designed to be unique and secure, so the resulting hash will be different even if two users have the same combination of ID and timestamp. The hashed token is then stored in the database, linked to the user's account, and used to identify the user on subsequent visits.
Generating a remember token involves the following steps:
- When a user logs into the application and selects the “Remember me” option, Laravel generates a unique token using a secure random number generator.
- The token is combined with additional information, such as the user's ID and a time stamp, to create a signed token that the application can verify.
- The signed token is stored in the user's session, cookie, and database record. This allows the application to maintain the user's login state and easily verify the user's authentication status on subsequent requests.
In this way, the remember token serves as a secure and convenient way for Laravel to remember the user's identity and allow them to remain logged in across multiple visits.
This section summarizes the entire process that Laravel uses to log users into the application.
Email Verification
Laravel supports email verification at the core. Email verification is a process you use to confirm the validity and authenticity of an email address provided by a user during registration.
To enable email verification in your application, ensure that the User model implements the Illuminate\Contracts\Auth\MustVerifyEmail
contract. That's all!
Assuming that you've enabled email verification in your application, right after a successful registration process, Laravel fires the Registered
event, as you've seen in the “User Registration” section previously.
Laravel hooks into the Registered
event inside the AppServiceProvider::class
by listening to this event and fires the SendEmailVerificationNotification
event listener.
protected $listen = [
Registered::class => [SendEmailVerificationNotification::class, ],
];
Inside this listener, Laravel sends the user a verification email to the email address provided. The email contains a URL generated by URL::temporarySignedRoute()
method with a default expiration date of 60 seconds and a hashed string of the User ID and User Email.
The user must click this URL, and the Laravel application validates and checks the parameters and verifies the user in the database.
Now that you know how the email verification process works, let's discover all aspects of enabling and making this feature usable in Laravel.
Start by navigating to the app/Models/User.php
model file. The User model must implement the MustVerifyEmail
contract as follows:
class User extends
Authenticatable implements MustVerifyEmail
Next, let's revisit the SendEmailVerificationNotification::class
and see how Laravel prepares and sends the verification email. Listing 7 shows the class implementation.
Listing 7: SendEmailNotification class
class SendEmailVerificationNotification
{
public function handle(Registered $event)
{
if ($event->user instanceof MustVerifyEmail &&
!$event->user->hasVerifiedEmail()
)
{$event->user->sendEmailVerificationNotification(); }
}
}
The handle()
method of the class sends the user an email only when:
- The User model implements the
MustVerifyEmail
contract. - The user is not yet verified.
Let's inspect the sendEmailVerificationNotification()
method Laravel defines inside the Illuminate\Auth\MustVerifyEmail.php
trait. Listing 8 shows the related code in this trait.
Listing 8: MustVerifyEmail trait
<?php
namespace Illuminate\Auth;
use Illuminate\Auth\Notifications\VerifyEmail;
trait MustVerifyEmail
{
// ...
public function sendEmailVerificationNotification()
{
$this->notify(new VerifyEmail);
}
// ...
}
Laravel applies this trait on the User model. Hence, the sendEmailVerificationNotification()
method is accessible on the User model instance.
Laravel issues a new notification using the VerifyEmail
notification class inside the method. Once again, because Laravel uses the Notifiable trait on the User model, it's safe to call the $this->notify()
method to send out a notification.
The VerifyEmail::class
is just a Laravel notification found in the Illuminate\Auth\Notifications\VerifyEmail.php
file.
I'll dedicate a full article to using Laravel Notifications in the future. For now, it's enough to understand that this VerifyEmail notification prepares an email with the temporary signed route and sends it to the user. Listing 9 shows the code to prepare the signed route.
Listing 9: Temporary signed route code
URL::temporarySignedRoute('verification.verify', Carbon::now()->addMinutes(
Config::get('auth.verification.expire', 60)
),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
The method takes three parameters:
- The name of the route, which is
verification.verify
- The expiration time for the signed route, which is set to the current time plus the number of minutes specified in the
auth.verification.expire
configuration setting (with a default value of 60 minutes) - An array of parameters passed to the route. In this case, the array contains two parameters: id, the User ID, and hash, is a hash of the user's email address.
This is an example of a temporary signed route in Laravel:
email/3/374a976d6e8288d2633ed8ad94b6d4bdd8487d50?expirs=1676289142
&signature=b2fd6e7f2def3e2cfeb9aedab9b58c840e8263a20975bc85d1ecdc78d6441d3
Figure 3 shows the email sent to the user to verify the email address.
You can read more about the signed URLs in Laravel here (https://laravel.com/docs/10.x/urls#signed-urls).
Email verification is an optional feature. It's recommended to use it, though.
Once the user clicks the Verify Email Address button, Laravel loads the route verification.verify
in the browser. This is handled by the VerifyEmailController::class __invoke()
method, as shown in Listing 10.
Listing 10: VerifyEmailController __invoke() method
class VerifyEmailController extends Controller
{
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail())
{
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
}
if ($request->user()->markEmailAsVerified())
{
event(new Verified($request->user()));
}
return redirect()->intended(RouteServiceProvider::HOME.'?verified=1');
}
}
The code in Listing 10 checks if the user is already verified otherwise, it verifies the user by calling the markEmailAsVerified()
method and finally redirects the user to the intended route, if it exists; otherwise, it redirects the user to the route defined in the RouteServiceProvider::HOME
constant.
Laravel provides the \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class
middleware defined inside the app/Http/Kernel.php
file under the route middleware array. You can use this route middleware to prevent access to the application (even if the user has successfully signed in) until the user verifies the email address. Here's an example of limiting users to verified only users when accessing the /dashboard
URL in the application:
Route::get('/dashboard', function () {
return Inertia::render('Dashboard')
})->middleware(['verified'])->name('dashboard');
The user has to verify the email address before accessing the /dashboard
URL. Listing 11 shows a simplified version of the source code for the EnsureEmailIsVerified::class
.
Listing 11: EnsureEmailsVerified middleware class
class EnsureEmailIsVerified
{
public function handle(
$request,
Closure $next,
$redirectToRoute = null
)
{
if (! $request->user() ||
($request->user() instanceof MustVerifyEmail &&
! $request->user()->hasVerifiedEmail())) {
return $request->Redirect::guest(
URL::route($redirectToRoute ?: 'verification.notice')
);
}
return $next($request);
}
}
The middleware redirects the user to the verification.notice
route only when the user has not yet verified the email address. Otherwise, it passes over the request to the next middleware in the middleware pipes.
To get more insight on the topic of Laravel middleware and how it handles the request flow, you can grab a free copy of my e-book on this topic: “Mastering Laravel's Request Flow” (https://tinyurl.com/2pbddgf5).
“Master Laravel's Request Flow” delves deep inside the Laravel Framework and explains all needed details.
Laravel defines the verification.notice
route in the routes/auth.php
file as follows:
Route::get('verify-email',
[
EmailVerificationPromptController::class, '__invoke'
]
)->name('verification.notice');
The __invoke()
method defined on the EmailVerificationPromptController::class
redirects the user to the resources/js/Pages/Auth/VerifyEmail.vue
page. The page allows the user to resend the verification email, as shown in Figure 4.
That's all you need to know, at this stage, regarding email verification in Laravel. Let's move on and study how Password Reset works in Laravel.
Forgot Password and Reset
On the Login page, you can retrieve your forgotten password by clicking the link named Forgot your Password?
Laravel uses the following markup to define this link:
<Link :href="route('password.request')">
Forgot your password?
</Link>
You can find the route password.request
inside the routes/auth.php
file defined as follows:
Route::get('forgot-password',
[PasswordResetLinkController::class, 'create']
)->name('password.request');
Let's explore the create()
method.
public function create(): Response
{
return Inertia::render('Auth/ForgotPassword', [
'status' => session('status'),
]);
}
The create()
method returns the resources/js/Pages/Auth/ForgotPassword.vue
page. Listing 12 shows a simplified version of the ForgotPassword.vue
page.
Listing 12: ForgotPassword.vue page
<script setup>
defineProps({status: String,});
const form = useForm({email: '',});
const submit = () => {form.post(route('password.email'));};
</script>
<template>
<div v-if="status">
{{ status }}
</div>
<form @submit.prevent="submit">
<div>
<TextInput
id="email"
type="email"
v-model="form.email"
required
/>
</div>
<PrimaryButton>
Email Password Reset Link
</PrimaryButton>
</form>
</template>
The <script>
section defines the component's logic and data.
defineProps()
is a function that defines the component's props. In this case, the component has a single prop namedstatus
, which is of type String.- The
useForm
hook manages the component's form data. It creates an object namedform
with a single property email set to an empty string. - The
submit()
function is called when the form is submitted. It uses theform.post()
method to send a POST request to the URL defined by thepassword.email
route.
The <template>
section defines the component's HTML structure and visual appearance.
- A div to display the status of sending a Forgot password reset link.
- The form element contains a single input field to allow the user to enter the email address. The
v-model
directive is used to bind the value of the input to theform.email
property. - The PrimaryButton component is a button the user can click to submit the form and trigger the password reset process.
You can find the route password.email
inside the routes/auth.php
file defined as follows:
Route::post('forgot-password',
[PasswordResetLinkController::class, 'store']
)->name('password.email');
Let's explore the store()
method. Listing 13 shows the source code for the store()
method.
Listing 13: PasswordResetLinkController::class store() method
public function store(Request $request): RedirectResponse
{
$request->validate(['email' => 'required|email', ]);
$status = Password::sendResetLink($request->only('email'));
if ($status == Password::RESET_LINK_SENT)
{
return back()->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
The function takes an instance of the Request
class as its input. This class represents the HTTP request that the user submitted. The method validates the incoming email address to make sure it exists and that it's an email address.
The sendResetLink()
method on the Password Facade class is called, passing the user's email address entered in the form. This method sends a password reset email to the user.
The result of the sendResetLink()
method is stored in the $status
variable. If $status
equals Password::RESET_LINK_SENT
, the password reset email was successfully sent, and the user is redirected to the previous page (ForgotPassword.vue) with a success status message.
If the value of $status
is not equal to Password::RESET_LINK_SENT
, an error occurs, and a ValidationException is thrown with an error message.
Digging more inside the Laravel framework, I discovered that the `Illuminate\Auth\Notifications\ResetPassword.php is the notification that executes when it's time to send the user a password reset link.
Let's track the most important part of this notification class, the protected resetUrl()
function. Listing 14 shows the source code for the resetUrl()
function.
Listing 14: ResetPassword::class resetUrl() function
protected function resetUrl($notifiable)
{
// ...
return url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
The method takes an instance of the $notifiable
object as its input, which represents a recipient of the password reset email.
It returns a password reset URL generated by calling the url()
function and passing it the result of the route
function.
The route()
function generates a URL for the password reset page represented by the password.reset
route. It takes as parameters the token and the email address of the recipient. Laravel generates a random token and stores it in the database inside the password_resets
table.
The password.reset
route is defined inside the routes/auth.php
file as follows:
Route::get('reset-password/{token}',
[NewPasswordController::class, 'create']
)->name('password.reset');
This route handles clicking the password reset link that was sent in the email. Let's explore the create()
method. Listing 15 shows the source code for the NewPasswordController::class create()
method.
Listing 15: NewPasswordController::class create() method
public function create(Request $request): Response
{
return Inertia::render('Auth/ResetPassword', [
'email' => $request->email,
'token' => $request->route('token'),
]);
}
The method returns the resources/js/Pages/Auth/ResetPassword.vue
page passing down to the page the email and token parameters. Listing 16 shows the ResetPassword.vue
page.
Listing 16: ResetPassword.vue page
<script setup>
const props = defineProps({
email: String,
token: String,
});
const form = useForm({
token: props.token,
email: props.email,
password: '',
password_confirmation: '',
});
const submit = () => {
form.post(route('password.store'), {
onFinish: () => form.reset(
'password',
'password_confirmation'
),
});
};
</script>
<template>
<form @submit.prevent="submit">
<div>
<TextInput
id="email"
type="email"
v-model="form.email"
required
/>
</div>
<div>
<TextInput
id="password"
type="password"
v-model="form.password"
required
/>
</div>
<div>
<TextInput
id="password_confirmation"
type="password"
v-model="form.password_confirmation"
required
/>
</div>
<PrimaryButton>
Reset Password
</PrimaryButton>
</form>
</template>
The page prompts the user to submit their email, new password, and confirmed password. It then submits the form to the password.store
route. Let's check this route in the routes/auth.php
file.
Route::post('reset-password',
[NewPasswordController::class, 'store']
)->name('password.store');
This is the last step in retrieving and resetting the user password. Let's explore the store()
method. Listing 17 shows a simplified store()
method version on the NewPasswordController::class
.
Listing 17: NewPasswordController::class store() method
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => [
'required',
'confirmed',
Rules\Password::defaults()
],
]);
$status = Password::reset($request->only(
'email',
'password',
'password_confirmation',
'token'
),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
if ($status == Password::PASSWORD_RESET)
{
return redirect()->route('login')->with('status', __($status));
}
throw ValidationException::withMessages([
'email' => [trans($status)],
]);
}
The store()
method handles the form submission for resetting the password. The method first validates the incoming request using the validate()
method, which ensures that the request contains a token, an email, and a password that matches the confirmation and meets the default password rules.
The Password::reset()
method is then called to reset the user's password. It takes an array of parameters, including the user's email, the new password, the confirmation of the new password, and the reset token. It also takes a closure responsible for updating the user's password and firing an event to notify the system of the password reset upon a successful password reset by Laravel.
The reset()
method matches the incoming token and email with those stored in the password_resets
database table before authorizing a password reset action.
If the password reset is successful, the method redirects the user to the login page with a status message indicating that the password has been reset. If the password reset fails, a ValidationException is thrown with a message indicating the reason for the failure. Figure 5 shows the password reset page.
That's it! Now you can retrieve and reset your forgotten password in Laravel.
User Logout
Logging out of the Laravel application is straightforward. You'll notice a link to log out from the application on the Welcome page. This link posts to a route named logout.
Let's explore the logout route in the routes/auth.php
file.
Route::post('logout',
[
AuthenticatedSessionController::class, 'destroy'
]
)->name('logout');
The destroy()
method defined on the AuthenticatedSessionController::class
is responsible for logging the user out. Let's check it out. Listing 18 shows the destory()
method.
Listing 18: AuthenticatedSessionController::class destory() method
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
The destroy()
method handles the request to log out the authenticated user.
The Auth::guard('web')->logout()
method is called to log out the user by invalidating the user's session on the web guard, which is the default authentication guard for web applications in Laravel (SessionGuard.php).
The session()->invalidate()
method is called to invalidate the user's session, which ensures that any data associated with the user's session is cleared from the storage and generates a new session ID.
The session()->regenerateToken()
method is called to regenerate the CSRF token, which is a security feature that helps to prevent session hijacking. I didn't cover the CSRF protection in Laravel as Laravel automatically handles it. However, if you're interested in learning more about it, you can read about it here: https://laravel.com/docs/10.x/csrf.
Finally, the method redirects the user to the home page. The user is now fully logged out of the application.
Auth Middleware and Protecting Routes
In the last article of the series of building MVC apps with PHP Laravel, I've explained routing and middleware in Laravel. If you need a quick refresh, here's the link: https://www.codemag.com/Article/2301041/Mastering-Routing-and-Middleware-in-PHP-Laravel.
In the context of today's article, let's look at the Authentication route middleware Laravel provides to ensure that an authenticated user can access a secure area in your application.
Let's start by limiting routes to only authenticated users. The routes/auth.php
file defines a route for the /dashboard
URL that limits it to only authenticated users.
Route::get('/dashboard', function() {
return Inertia::render('Dashboard');
})->middleware(['auth'])->name('dashboard');
The route defines the auth middleware to protect the /dashboard
URL. Where's this auth middleware defined? Go to the app/Http/Kernel.php
file and look at the $routeMiddleware
array.
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
];
The name auth is an alias for the authentication middleware at app/Http/Middleware/Authentication::class
. Listing 19 shows the source code for the Authenticate::class
middleware. The Authenticate::class
middleware extends the Illuminate\Auth\Middleware\Authenticate::class
middleware class provided by Laravel framework.
Listing 19: Authenticate middleware
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}
The redirectTo()
function is used internally by the Illuminate\Auth\Middleware\Authenticate::class
to redirect the user to the /login
page when authentication fails.
Let's have a look at the Illuminate\Auth\Middleware\Authenticate::class
.
public function handle(
$request,
Closure $next,
...$guards)
{
$this->authenticate($request, $guards);
return $next($request);
}
The handle()
function is straightforward. It tries to authenticate the user and then hands off the processing of the current request to the remaining middleware in the pipeline.
You can add your own logic to handle user authentication for other scenarios you might have in your application.
Conclusion
This article provided a comprehensive overview of Session authentication, which is widely used by numerous web applications in Laravel. However, authentication in Laravel extends beyond just Session authentication, as there are other providers, such as token-based authentication and authentication with social providers like Google and Twitter.
Subsequent installments of this series will explore token-based authentication, a popular method for authenticating API requests, and will also delve into authenticating with social providers.
I aim to equip you with the knowledge and skills to implement robust and secure Laravel applications.
Happy Laraveling!