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.
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.
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.
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.
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.
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.
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.
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
.
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
.
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.
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.
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](/_next/image?url=https%3A%2F%2Fik.imagekit.io%2Fpeqmgufll%2Fcontent%2Flaravel-passwordless-authentication-preview.gif%3FupdatedAt%3D1696487613590&w=3840&q=100)
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.