0 Comments

Manual auth in Laravel: password reset

With the arrival of Laravel 8, new ways for authentication have been added to the Laravel ecosystem. Fortify, Jetstream and Breeze. Although these tools can save you a lot of time, often when you want something more complex they cost you more time.

Fortunately, Laravel allows you to add manual auth without the use of any package, just Laravel's core. In this series, we're going to learn how to add manual auth in Laravel.

These topics will be covered:

Note: For the examples in this series, I've chosen to use controllers and blade views. But you can also use other technologies, like Livewire or Inertia.js.

Getting started

Resetting a user's password consists of two steps: requesting a password-reset link and resetting the password using the link. We'll start off by requesting a password-reset link.

The request password-link form

First, we'll add a route to show the form for requesting a password-reset link.

// routes/web.php

use App\Http\Controllers\Auth\PasswordRequestController;
use Illuminate\Support\Facades\Route;

Route::get('/forgot-password', [PasswordRequestController::class, 'show'])
    ->middleware('guest')
    ->name('password.request');

Next we'll load a view in the controller.

// app/Http/Controllers/Auth/PasswordRequestController.php

class PasswordRequestController
{
    public function show()
	{
	    return view('auth.request-password');
	}
}

Finally, we'll create a view with a form.

<!-- resources/views/auth/request-password.blade.php -->

<h1>Forgot your password?</h1>
<p>Fill in your email below and we'll send you a link to reset your password.</p>

<form method="post" action="{{ route('password.email') }}">
    @csrf
 
    <label for="email">Your email</label>
    <input required type="email" name="email" id="email" />
    @error('email') {{ $message }} @enderror
  
    <button type="submit">Request</button>
</form>

Handling the form submission

Once the form is submitted, we'll send the user a link to reset their password. We'll start off with the routing.

// routes/web.php

use App\Http\Controllers\Auth\PasswordRequestController;
use Illuminate\Support\Facades\Route;

Route::get('/forgot-password', [PasswordRequestController::class, 'show'])
    ->middleware('guest')
    ->name('password.request');

Route::post('/forgot-password', [PasswordRequestController::class, 'handle'])
    ->middleware('guest')
    ->name('password.email');

Next, we'll add the necessary logic to the controller.

// app/Http/Controllers/Auth/PasswordRequestController.php

use Illuminate\Support\Facades\Password;

class PasswordRequestController
{
    public function show()
	{
	    return view('auth.request-password');
	}
  
    public function handle()
	{
	    request()->validate([
		    'email' => ['required', 'email', 'exists:users']
		]);

	    $status = Password::sendResetLink(request('email'));

	    return $status === Password::RESET_LINK_SENT
            ? back()->with(['success' => 'We have emailed your password reset link!'])
            : back()->withErrors(['email' => 'Please wait before retrying.']);
	}
}

I will explain quickly what the handle method does:

  1. It validates the request and checks if the email exists in the users table.
  2. It uses the Laravel Password facade to send a password link. Under the hood this facade checks the email, creates a token and sends the link.
  3. If the request was successfull a success message will be shown, and else an error message.

Finally, we should display the success message in the view:

<!-- resources/views/auth/request-password.blade.php -->

<h1>Forgot your password?</h1>
<p>Fill in your email below and we'll send you a link to reset your password.</p>

<form method="post" action="{{ route('password.email') }}">
    @csrf
  
    @if(session()->has('success'))
        {{ session()->get('success') }}
    @endif
 
    <label for="email">Your email</label>
    <input required type="email" name="email" id="email" />
    @error('email') {{ $message }} @enderror
  
    <button type="submit">Request</button>
</form>

The password reset form

After a user clicks the link in the email, they should see a form to fill in a new password.

Let's start with the routing.

// routes/web.php

use App\Http\Controllers\Auth\PasswordRequestController;
use App\Http\Controllers\Auth\ResetPasswordController;
use Illuminate\Support\Facades\Route;

Route::get('/forgot-password', [PasswordRequestController::class, 'show'])
    ->middleware('guest')
    ->name('password.request');

Route::post('/forgot-password', [PasswordRequestController::class, 'handle'])
    ->middleware('guest')
    ->name('password.email');

Route::get('/reset-password/{token}', [ResetPasswordController::class, 'show'])
    ->middleware('guest')
    ->name('password.reset');

Then, the controller should display a view with the form.

// app/Http/Controllers/Auth/ResetPasswordController.php

class ResetPasswordController
{
    public function show(string $token)
	{
	    return view('auth.reset-password', [
		    'token' => $token
		]);
	}
}

Now let's create the view.

<!-- resources/views/auth/reset-password.blade.php -->

<h1>Reset your password</h1>

<form method="post" action="{{ route('password.update') }}">
    @csrf
  
    <!-- Token (hidden) -->
    <input type="text" name="token" value="{{ $token }}" />
 
    <!-- Email -->
    <label for="email">Email</label>
    <input required type="email" name="email" id="email" value="{{ request('email') }}" />
    @error('email') {{ $message }} @enderror
 
    <!-- Password -->
    <label for="password">Password</label>
    <input required type="password" name="password" id="password" />
    @error('password') {{ $message }} @enderror
  
    <!-- Confirm Password -->
    <label for="password_confirmation">Confirm Password</label>
    <input required type="password" name="password_confirmation" id="password_confirmation" />
    @error('password_confirmation') {{ $message }} @enderror
  
    <button type="submit">Reset</button>
</form>

Handling the form submission

The final step is to handle the form submission when a user has filled in their new password.

As always, let's start with the routing.

// routes/web.php

use App\Http\Controllers\Auth\PasswordRequestController;
use App\Http\Controllers\Auth\ResetPasswordController;
use Illuminate\Support\Facades\Route;

Route::get('/forgot-password', [PasswordRequestController::class, 'show'])
    ->middleware('guest')
    ->name('password.request');

Route::post('/forgot-password', [PasswordRequestController::class, 'handle'])
    ->middleware('guest')
    ->name('password.email');

Route::get('/reset-password/{token}', [ResetPasswordController::class, 'show'])
    ->middleware('guest')
    ->name('password.reset');

Route::get('/reset-password', [ResetPasswordController::class, 'handle'])
    ->middleware('guest')
    ->name('password.update');

And then we'll add the controller logic.

// app/Http/Controllers/Auth/ResetPasswordController.php

use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;

class ResetPasswordController
{
    public function show(string $token)
	{
	    return view('auth.reset-password', [
		    'token' => $token
		]);
	}
  
    public function handle()
	{
	    request()->validate([
		    'token' => ['required', 'string'],
		    'email' => ['required', 'email'],
			'password' => ['required', 'string', 'min:8', 'confirmed']
		]);

		$status = Password::reset(
		    request(['email', 'password', 'password_confirmation', 'token']),
            function ($user, $password) {
			    $user->update(['password' => Hash::make($password)]);
			    $user->setRememberToken(Str::random(60));
            }
        );

		return $status === Password::PASSWORD_RESET
            ? redirect()->route('login')->with('success', 'Your password has been reset!')
            : back()->withErrors(['email' => 'This password reset token is invalid.']);
	}
}

Again, I'll explain quickly what's happening:

  1. We validate the request, and check if the password meets the security requirements.
  2. Next, we check if the token is valid and if so the callback is called, where the password is reset.
  3. Finally, a success message is shown to the user if the request was successfull, and otherwise an error message.

Note: It's important to have protected $guarded = []; on your User model. If you use fillable fields instead, you should use this code in the callback:

function ($user, $password) {
    $user->update(['password' => Hash::make($password)]);
    $user->forceFill(['password' => Hash::make($password)]);
    $user->setRememberToken(Str::random(60));
    $user->save();
}

Conclusion

Adding reset-password functionality in Laravel is very easy, thanks to Laravel's Password facade. If you want to learn more about this subject, I'd suggest reading the Laravel docs.

Share this article:

Subscribe to my newsletter

Continue reading:

Manual auth in Laravel: registering

With the arrival of Laravel 8, new ways for authentication have been added to the Laravel ecosystem. Fortify, Jetstream and Breeze. Although these tools can...

Manual auth in Laravel: signing in and out

With the arrival of Laravel 8, new ways for authentication have been added to the Laravel ecosystem. Fortify, Jetstream and Breeze. Although these tools can...

Manual auth in Laravel: password confirmation

With the arrival of Laravel 8, new ways for authentication have been added to the Laravel ecosystem. Fortify, Jetstream and Breeze. Although these tools can...

Leave a comment

Comments (0)

    No comments found.