background
Home / Articles / Laravel Passwordless Authentication
ARTICLE

Laravel Passwordless Authentication

How to implement passwordless authentication in Laravel using magic link.

#Laravel  #Passwordless  #Authentication  
  Rafi Putra RamadhanOctober 05, 202313 min read  

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.

1laravel 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.

1$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
1Route::middleware('guest')->group(function () { 2 Route::get('/login', [\App\Http\Controllers\SignInController::class, 'index'])->name('login'); 3 Route::post('/login', [\App\Http\Controllers\SignInController::class, 'store'])->name('login.store'); 4 Route::post('/register', \App\Http\Controllers\SignUpController::class)->name('register.store'); 5 Route::get('/login/magic-link/{email}', \App\Http\Controllers\AuthenticatedController::class)->name('passwordless.authentication'); 6});
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

1php 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
1<?php 2 3namespace App\Mail; 4 5use Illuminate\Bus\Queueable; 6use Illuminate\Contracts\Queue\ShouldQueue; 7use Illuminate\Mail\Mailable; 8use Illuminate\Mail\Mailables\Content; 9use Illuminate\Mail\Mailables\Envelope; 10use Illuminate\Queue\SerializesModels; 11 12class SignInLink extends Mailable implements ShouldQueue 13{ 14 use Queueable, SerializesModels; 15 16 /** 17 * Create a new message instance. 18 */ 19 public function __construct( 20 public readonly string $url, 21 ){} 22 23 /** 24 * Get the message envelope. 25 */ 26 public function envelope(): Envelope 27 { 28 return new Envelope( 29 subject: 'Sign In Link', 30 ); 31 } 32 33 /** 34 * Get the message content definition. 35 */ 36 public function content(): Content 37 { 38 return new Content( 39 markdown: 'emails.sign-in-link', 40 with: [ 41 'url' => $this->url, 42 ], 43 ); 44 } 45 46 /** 47 * Get the attachments for the message. 48 * 49 * @return array<int, \Illuminate\Mail\Mailables\Attachment> 50 */ 51 public function attachments(): array 52 { 53 return []; 54 } 55} 56
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
1<x-mail::message> 2# Sign In Link 3 4Use the button below to sign in into your account. 5 6<x-mail::button :url="$url"> 7Sign In 8</x-mail::button> 9 10Thanks,<br> 11{{ config('app.name') }} 12 13<br> 14 15<small> 16If you’re having trouble clicking the "Sign In" button, copy and paste the URL below into your web browser: <br /> 17<a href="{{ $url }}">{{ $url }}</a> 18</small> 19 20</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
1<?php 2 3namespace App\Traits; 4 5trait SendMagicLink 6{ 7 public function requestMagicLink(string $email): void 8 { 9 $url = \URL::temporarySignedRoute('passwordless.authentication', now()->addMinutes(15), [ 10 'email' => $email, 11 ]); 12 13 \Mail::to($email)->send(new \App\Mail\SignInLink($url)); 14 } 15} 16
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
1use App\Traits\SendMagicLink; 2 3class User extends Authenticatable 4{ 5 use HasApiTokens, HasFactory, Notifiable, SendMagicLink; 6 7 // ... 8} 9
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.

1php artisan make:request SignInRequest 2 3php 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
1<?php 2 3namespace App\Http\Requests; 4 5use Illuminate\Foundation\Http\FormRequest; 6 7class SignInRequest extends FormRequest 8{ 9 /** 10 * Determine if the user is authorized to make this request. 11 */ 12 public function authorize(): bool 13 { 14 return true; 15 } 16 17 /** 18 * Get the validation rules that apply to the request. 19 * 20 * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> 21 */ 22 public function rules(): array 23 { 24 return [ 25 'email' => ['required', 'email', 'exists:users,email'], 26 ]; 27 } 28} 29
php

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

app/Http/Requests/SignUpRequest.php
1<?php 2 3namespace App\Http\Requests; 4 5use Illuminate\Foundation\Http\FormRequest; 6 7class SignUpRequest extends FormRequest 8{ 9 /** 10 * Determine if the user is authorized to make this request. 11 */ 12 public function authorize(): bool 13 { 14 return true; 15 } 16 17 /** 18 * Get the validation rules that apply to the request. 19 * 20 * @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string> 21 */ 22 public function rules(): array 23 { 24 return [ 25 'name' => ['required', 'string', 'max:255'], 26 'email' => ['required', 'email', 'unique:users,email'], 27 ]; 28 } 29} 30
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
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Http\Requests\SignInRequest; 6use App\Models\User; 7 8class SignInController extends Controller 9{ 10 public function index() 11 { 12 // Render the sign-in page. 13 } 14 15 public function store(SignInRequest $request) 16 { 17 $user = User::where('email', $request->email)->firstOrFail(); 18 19 $user->requestMagicLink($user->email); 20 21 return back()->with([ 22 'type' => 'success', 23 'message' => "Check your email. We've sent you a sign in link." 24 ]); 25 } 26} 27
php

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

app/Http/Controllers/SignUpController.php
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Http\Requests\SignUpRequest; 6use App\Models\User; 7 8class SignUpController extends Controller 9{ 10 public function __invoke(SignUpRequest $request) 11 { 12 $attributes = $request->validated(); 13 $user = User::create([ 14 'name' => $attributes['name'], 15 'email' => $attributes['email'], 16 ]); 17 18 $user->requestMagicLink($user->email); 19 20 return back()->with([ 21 'type' => 'success', 22 'message' => "Sign up successful! We've sent you a sign in link." 23 ]); 24 } 25} 26
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
1<?php 2 3namespace App\Http\Controllers; 4 5use App\Providers\RouteServiceProvider; 6use Illuminate\Http\Request; 7 8class AuthenticatedController extends Controller 9{ 10 public function __invoke(Request $request, string $email) 11 { 12 if (!$request->hasValidSignature()) { 13 return redirect()->route('login')->with([ 14 'type' => 'error', 15 'message' => 'The sign in link is invalid or has expired.' 16 ]); 17 } 18 19 $user = \App\Models\User::where('email', $email)->firstOrFail(); 20 21 auth()->login($user); 22 $request->session()->regenerate(); 23 24 return redirect()->intended(RouteServiceProvider::HOME); 25 } 26} 27
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
1MAIL_MAILER=smtp 2MAIL_HOST=sandbox.smtp.mailtrap.io 3MAIL_PORT=2525 4MAIL_USERNAME= 5MAIL_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:

1php 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.