Support the ongoing development of Laravel.io →
Article Hero Image

How to set up Laravel Magic Link?

4 Dec, 2024 7 min read

Photo by Anton Darius on Unsplash

User authentication is crucial for making web applications secure and easy to use. Traditionally, passwords have been the main way to log in, but they come with many problems. People often forget their passwords, reuse weak ones, or face the hassle of resetting them. These issues can also lead to security risks like stolen credentials.

Magic link login offers a simple and secure alternative. Instead of relying on passwords, users receive a unique, one-time-use link via email to log in. This approach removes the need for passwords entirely, making the process easier, faster, and safer for everyone.

In this article, I will walk you through setting up a magic link in your Laravel project.

Prerequisite

To follow this guide, you need:

  • Laravel and Composer are installed in your local system.
  • Basic knowledge of Laravel.

Step 1: Create a Laravel Project

  1. Create a Laravel project using the Breeze starter kit:

    laravel new magic-link-demo --breeze
    
  2. Navigate to your project directory and open it in your favorite code editor. Open the terminal and serve your project using bthe elow command:

    php artisan serve
    
  3. Now your laravel app is serving at http://127.0.0.1:8000/

Step 2: Register new user

Once you serve the application & open it you can see the navbar you will find a register button in top right corner. Clicking on this will open a register form, please register a new user using the registration form.

Step 3: Create a view for the magic link login page.

Like a login page, we will create a separate page for a magic link login. We will have only one field in this form(email address). Below is the code for the magic link login page view.

<x-guest-layout>
    <!-- Session Status -->
    <x-auth-session-status class="mb-4" :status="session('status')" />

    <form method="POST" action="{{ route('magic-link-login.store') }}">
        @csrf

        <!-- Email Address -->
        <div>
            <x-input-label for="email" :value="__('Email')" />
            <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required
                autofocus autocomplete="username" />
            <x-input-error :messages="$errors->get('email')" class="mt-2" />
        </div>

        <div class="flex items-center justify-end mt-4">

            <x-primary-button class="ms-3">
                {{ __('Log in') }}
            </x-primary-button>
        </div>
    </form>
</x-guest-layout>

Step 4: Create a controller for the magic link

Let’s create a controller for the magic link by running the following command:

php artisan make:controller MagicLinkController 

Now we will define the following methods in this controller for handling the magic link login.

  1. create() This method renders a magic link login view.

    public function create()
      {
          return view('auth.magic-link-login');
      }
    

    This will create a MagicLinkController in the app/Http/Controllers directory.

  2. store() This method handles the form submission, generates a Magic Link, and sends it via email.

    public function store()
        {
    
            request()->validate(['email' => 'required|email']);
    
    				 // Find the user by their email
            $user = User::where('email', request()->email)->first();
    
    				// If the email doesn’t match any user, return an error
            if (! $user) {
                return back()->withErrors(['email' => 'Account with matching email address not found.']);
            }
    
            // Generate a temporary signed URL for the user to login.
            $url = url()->temporarySignedRoute(
                'login.token',
                now()->addMinutes(30),
                ['user' => $user->id]
            );
    
    				// Notify the user via email
            $user->notify(new MagicLinkLogin($url));
    
            return back()->with('status', 'A login link has been sent to your email address.');
        }
    
  3. LoginViaToken() This method authenticates users when they log in via Magic Link.

    public function loginViaToken()
        {
            $user = User::findOrFail(request()->user);
    
            Auth::login($user);
    
            Log::info('User logged in via token.', ['email' => $user->email]);
    
            request()->session()->regenerate();
    
            return redirect()->intended(route('dashboard', absolute: false));
        }
    

Step 5: Create the Notification

To send the Magic Link email, we’ll use Laravel’s Notification system.

  1. Generate a MagicLinkLogin notification by following the below command:

    php artisan make:notification MagicLinkLogin
    

    This will create MagicLinkLogin.php file in app/Notifications.

  2. configure email by modifying toMail method in this file. Here is the complete code for this.

    <?php
    
    namespace App\Notifications;
    
    use Illuminate\Bus\Queueable;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Notifications\Messages\MailMessage;
    use Illuminate\Notifications\Notification;
    
    class MagicLinkLogin extends Notification
    {
        use Queueable;
    
        /**
         * Create a new notification instance.
         */
        public function __construct(public string $url)
        {
            //
        }
    
        /**
         * Get the notification's delivery channels.
         *
         * @return array<int, string>
         */
        public function via(object $notifiable): array
        {
            return ['mail'];
        }
    
        /**
         * Get the mail representation of the notification.
         */
        public function toMail(object $notifiable): MailMessage
        {
            return (new MailMessage)
                ->line('Click here to login instantly.')
                ->action('Login', $this->url)
                ->line('Thank you for using our application!');
        }
    
        /**
         * Get the array representation of the notification.
         *
         * @return array<string, mixed>
         */
        public function toArray(object $notifiable): array
        {
            return [
                //
            ];
        }
    }
    
    

Step 6: Configure routes.

Add the following routes to routes/web.php

Route::get('/magic-link-login', [MagicLinkController::class, 'create'])->name('magic-link-login');
Route::post('/magic-link-login', [MagicLinkController::class, 'store'])->name('magic-link-login.store');

Route::get('/magic-link-login/{user}', [MagicLinkController::class, 'loginViaToken'])
    ->name('login.token')
    ->middleware('signed');

Here is the details about these routes:

  • GET /magic-link-login: Displays the Magic Link login form.
  • POST /magic-link-login: Handles form submissions and sends the Magic Link.
  • GET /magic-link-login/{user}: Logs in the user when they click the Magic Link.

Step 7: Configure the email mailer.

For development, we’ll use the log driver to log emails instead of sending them. Update your .env file:

MAIL_MAILER=log
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

You can also configure other mailers like smtp, mailgun, and Postmark.

Step 8: Add a magic link option to the login page.

To make the Magic Link login accessible, we’ll add a button to our existing login page. This allows users to choose between traditional login (email + password) or Magic Link login.

Here’s the complete code for your updated login page:

<x-guest-layout>
    <!-- Session Status -->
    <x-auth-session-status class="mb-4" :status="session('status')" />

    <form method="POST" action="{{ route('login') }}">
        @csrf

        <!-- Email Address -->
        <div>
            <x-input-label for="email" :value="__('Email')" />
            <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required
                autofocus autocomplete="username" />
            <x-input-error :messages="$errors->get('email')" class="mt-2" />
        </div>

        <!-- Password -->
        <div class="mt-4">
            <x-input-label for="password" :value="__('Password')" />

            <x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required
                autocomplete="current-password" />

            <x-input-error :messages="$errors->get('password')" class="mt-2" />
        </div>

        <!-- Remember Me -->
        <div class="block mt-4">
            <label for="remember_me" class="inline-flex items-center">
                <input id="remember_me" type="checkbox"
                    class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500" name="remember">
                <span class="ms-2 text-sm text-gray-600">{{ __('Remember me') }}</span>
            </label>
        </div>

        <div class="flex items-center justify-between mt-4">
            @if (Route::has('password.request'))
                <a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                    href="{{ route('password.request') }}">
                    {{ __('Forgot your password?') }}
                </a>
            @endif

            <x-primary-button class="ms-3">
                {{ __('Log in') }}
            </x-primary-button>
        </div>

        <p class="py-4 text-center font-semibold text-xl">OR</p>
        <div class="flex justify-center">
            <a class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150"
                href="{{ route('magic-link-login') }}">
                {{ __('Login with magic link') }}
            </a>
        </div>
    </form>
</x-guest-layout>

Step 9: Test the magic link feature.

  1. Navigate to the main login page. You’ll notice a new “Login with Magic Link” button under the standard login form

    Laravel magic link example

  2. When you click the button, you’ll be redirected to the Magic Link login page. This page contains a form where users can enter their email address. Enter the details of the registered user and submit the form

    Laravel magic link example

  3. If you’ve set the MAIL_MAILER=log in your .env file, open the Laravel logs to find the Magic Link URL or if you have configured a mailer please check your mailbox.

  4. Click the link to log in seamlessly! You will be redirected to the application dashboard directly.

Congratulations!🥳 You have successfully set up & tested the magic link login in your laravel application.

Conclusion

You’ve successfully implemented Magic Link Login in your Laravel application! This feature is a game-changer for user experience, offering you a secure and hassle-free way to log in.🎉

Btw, if you want to save time and kickstart your project easily, you can consider choosing a pre-built SaaS Boilerplate like Jetship Laravel Starter Kit.

It simplifies authentication with its One-Click Magic Link Setup and offers various authentication methods, including traditional email/password and social logins via Google, GitHub, and Twitter.lara

Last updated 1 week ago.

driesvints, abhidave001, saanvi-ts liked this article

3
Like this article? Let the author know and give them a clap!

Other articles you might like

Article Hero Image December 13th 2024

How to add WebAuthn Passkeys To Backpack Admin Panel

Want to make your Laravel Backpack admin panel more secure with a unique login experience for your a...

Read article
Article Hero Image December 13th 2024

Quickest way to setup PHP Environment (Laravel Herd + MySql)

Setting up a local development environment can be a time taking hassle—whether it's using Docker or...

Read article
Article Hero Image December 9th 2024

Access Route Model-Bound Models with "#[RouteParameter]"

Introduction I've recently been using the new #[RouteParameter] attribute in Laravel, and I've been...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.