October 05, 2023

Laravel Passwordless Authentication

In this article, we'll explore how to implement passwordless authentication in Laravel using a magic link. This method allows users to log in without traditional passwords, simplifying the process and enhancing user experience. Follow along to set up your Laravel project and create a seamless sign-in and sign-up process with email verification!

laravel-passwordless-authentication

Laravel Passwordless Authentication

In this article, we will learn how to implement passwordless authentication in Laravel using a magic link. ✨

But what exactly is passwordless authentication? In this approach, users are not required to enter a traditional password when logging in. Instead, they receive a magical link via email. Clicking this link grants them access to the application.

What are the perks of passwordless authentication, you ask?

  • No more memorizing complex passwords.
  • Say goodbye to the hassle of password resets.
  • Eliminate the need to store and secure passwords.
  • It's a straightforward and user-friendly experience.

So, let's dive right in! 🚀

We'll assume you already have a Laravel project up and running. If you don't, fear not—you can create a new Laravel project with a simple command in your terminal.

laravel new laravel-passwordless-authentication
bash

Create some controller that we will use later for the project.

  • SignInController, This controller will be in charge of managing the sign-in process and dispatching magic links to users via email.
  • SignUpController, Responsible for handling user sign-up and sending magic links for email verification.
  • AuthenticatedController, Designed to handle incoming requests initiated from the magic link.

Before proceeding, please ensure that you have made the password column in your users table nullable. If it's not already, make the necessary modification to set the password column as nullable.

$table->string('password')->nullable();
php

Now, let's proceed to the next stage by configuring routes for the controllers we've previously created.

routes/web.php
Route::middleware('guest')->group(function () {
    Route::get('/login', [\App\Http\Controllers\SignInController::class, 'index'])->name('login');
    Route::post('/login', [\App\Http\Controllers\SignInController::class, 'store'])->name('login.store');
    Route::post('/register', \App\Http\Controllers\SignUpController::class)->name('register.store');
    Route::get('/login/magic-link/{email}', \App\Http\Controllers\AuthenticatedController::class)->name('passwordless.authentication');
});
php

To send the magic link to the user's email, we'll create a Mail class called SignInLink. To generate this class and the associated email template, you can use the following artisan command

php artisan make:mail SignInLink --markdown=emails.sign-in-link
bash

This command will generate the SignInLink class located in the app/Mail directory, along with the sign-in-link.blade.php template in the resources/views/emails directory.

The SignInLink class is an extension of the Mailable class and is responsible for delivering the magic link to the user's email. To enable asynchronous email delivery, we'll configure this class for queuing by adding implements ShouldQueue to it.

app/Mail/SignInLink.php
<?php
 
namespace App\Mail;
 
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
 
class SignInLink extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;
 
    /**
     * Create a new message instance.
     */
    public function __construct(
        public readonly string $url,
    ){}
 
    /**
     * Get the message envelope.
     */
    public function envelope(): Envelope
    {
        return new Envelope(
            subject: 'Sign In Link',
        );
    }
 
    /**
     * Get the message content definition.
     */
    public function content(): Content
    {
        return new Content(
            markdown: 'emails.sign-in-link',
            with: [
                'url' => $this->url,
            ],
        );
    }
 
    /**
     * Get the attachments for the message.
     *
     * @return array<int, \Illuminate\Mail\Mailables\Attachment>
     */
    public function attachments(): array
    {
        return [];
    }
}
 
php

sign-in-link.blade.php is a Blade template designed to be used for sending the magic link to the user's email.

resources/views/emails/sign-in-link.blade.php
<x-mail::message>
# Sign In Link
 
Use the button below to sign in into your account.
 
<x-mail::button :url="$url">
Sign In
</x-mail::button>
 
Thanks,<br>
{{ config('app.name') }}
 
<br>
 
<small>
If you’re having trouble clicking the "Sign In" button, copy and paste the URL below into your web browser: <br />
<a href="{{ $url }}">{{ $url }}</a>
</small>
 
</x-mail::message>
markdown

We've taken care of creating the SignInLink class and the sign-in-link.blade.php template.

Now, in the interest of code efficiency and to prevent duplication, we're introducing a versatile feature known as the SendMagicLink trait. This handy trait will find its application in both the SignInController and SignUpController, streamlining the shared logic for the sign-in and sign-up processes.

app/Traits/SendMagicLink.php
<?php
 
namespace App\Traits;
 
trait SendMagicLink
{
    public function requestMagicLink(string $email): void
    {
        $url = \URL::temporarySignedRoute('passwordless.authentication', now()->addMinutes(15), [
            'email' => $email,
        ]);
 
        \Mail::to($email)->send(new \App\Mail\SignInLink($url));
    }
}
 
php

The requestMagicLink method serves the purpose of sending a magic link to the user's email. We make use of the temporarySignedRoute method to generate a secure, time-limited URL, which functions as the magic link. This URL remains valid for a duration of 15 minutes, after which it expires automatically.

To facilitate the delivery of the magic link to the user's email, we utilize the Mail facade.

Now, let's proceed to the next step, where we'll integrate the previously created Traits into the User model.

app/Models/User.php
use App\Traits\SendMagicLink;
 
class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, SendMagicLink;
    
    // ...
}
 
php

We incorporate the use of Traits in the User model to streamline the process. This enables us to employ the requestMagicLink method seamlessly in both the SignInController and SignUpController. As a result, we can initiate the requestMagicLink function from the User model directly, like this: $user->requestMagicLink($email). This approach eliminates the need for redundant code.

Now, let's proceed to the next task, which involves the creation of two distinct classes: SignInRequest and SignUpRequest. These classes are designed to validate incoming requests from the sign-in and sign-up forms.

php artisan make:request SignInRequest
 
php artisan make:request SignUpRequest
bash

The purpose of the SignInRequest class is to validate incoming requests originating from the sign-in form.

app/Http/Requests/SignInRequest.php
<?php
 
namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
class SignInRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }
 
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'email' => ['required', 'email', 'exists:users,email'],
        ];
    }
}
 
php

The SignUpRequest class serves the role of validating requests that come from the sign-up form.

app/Http/Requests/SignUpRequest.php
<?php
 
namespace App\Http\Requests;
 
use Illuminate\Foundation\Http\FormRequest;
 
class SignUpRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }
 
    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
     */
    public function rules(): array
    {
        return [
            'name'  => ['required', 'string', 'max:255'],
            'email' => ['required', 'email', 'unique:users,email'],
        ];
    }
}
 
php

Now, let's proceed to the next phase, where we'll implement the logic for the sign-in process within the SignInController.

app/Http/Controllers/SignInController.php
<?php
 
namespace App\Http\Controllers;
 
use App\Http\Requests\SignInRequest;
use App\Models\User;
 
class SignInController extends Controller
{
    public function index()
    {
        // Render the sign-in page.
    }
 
    public function store(SignInRequest $request)
    {
        $user = User::where('email', $request->email)->firstOrFail();
 
        $user->requestMagicLink($user->email);
 
        return back()->with([
            'type'    => 'success',
            'message' => "Check your email. We've sent you a sign in link."
        ]);
    }
}
 
php

Next, we'll tackle the sign-up process by implementing the necessary logic within the SignUpController.

app/Http/Controllers/SignUpController.php
<?php
 
namespace App\Http\Controllers;
 
use App\Http\Requests\SignUpRequest;
use App\Models\User;
 
class SignUpController extends Controller
{
    public function __invoke(SignUpRequest $request)
    {
        $attributes = $request->validated();
        $user = User::create([
            'name'  => $attributes['name'],
            'email' => $attributes['email'],
        ]);
 
        $user->requestMagicLink($user->email);
 
        return back()->with([
            'type'    => 'success',
            'message' => "Sign up successful! We've sent you a sign in link."
        ]);
    }
}
 
php

As you can observe, we utilize the requestMagicLink method from the User model to dispatch the magic link to the user's email. Following this, we redirect the user back to the sign-in page with a success message.

Now that we've established the logic for both the sign-in and sign-up processes, the next step involves creating the functionality to manage requests initiated from the magic link. The magic link, which is essentially a signed URL, serves as the means to authenticate the user.

To facilitate this, we'll be introducing a new AuthenticatedController class, which will be responsible for handling requests originating from the magic link.

app/Http/Controllers/AuthenticatedController.php
<?php
 
namespace App\Http\Controllers;
 
use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request;
 
class AuthenticatedController extends Controller
{
    public function __invoke(Request $request, string $email)
    {
        if (!$request->hasValidSignature()) {
            return redirect()->route('login')->with([
                'type'    => 'error',
                'message' => 'The sign in link is invalid or has expired.'
            ]);
        }
 
        $user = \App\Models\User::where('email', $email)->firstOrFail();
 
        auth()->login($user);
        $request->session()->regenerate();
 
        return redirect()->intended(RouteServiceProvider::HOME);
    }
}
 
php

We employ the hasValidSignature() method to verify the validity of the signed URL. In cases where the signed URL is either invalid or has expired, we guide the user back to the sign-in page while displaying an appropriate error message. Conversely, if the signed URL is valid, we proceed to authenticate the user and redirect them to the home page.

To test this functionality, we have the flexibility to use any mail service provider. However, for the purpose of this article, we will demonstrate the testing process using the Mailtrap service.

To enable this, we need to modify the .env file to configure and utilize the Mailtrap service.

.env
MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
env

Because we've integrated the ShouldQueue interface into the SignInLink class, the email delivery process now runs in the background, allowing for asynchronous operations. To activate the queue worker, all you need to do is run this command in your terminal:

php artisan queue:work
bash

Now, you're ready to put the sign-in and sign-up processes to the test. Upon signing in or signing up, you'll promptly receive an email containing a magic link.

Here's a glimpse of the feature we've developed thus far.

Laravel Passwordless Authentication Preview
Laravel Passwordless Authentication Preview

This is just one of several possible approaches for implementing passwordless authentication, and it's important to note that its effectiveness may vary depending on your specific requirements and circumstances. If you'd like to explore this implementation further, you can get the full source code from GitHub.

raprmdn
Rafi Putra Ramadhan
Laravel
Passwordless
Authentication