Password-less Login with Laravel 8+

Create a pass-phrase or 'magic-link' login system for Laravel 8 and Jetstream

✅ Checked works with Laravel 10

Introduction

Users don't manage passwords well. They forget them or choose easy to remember passwords then use that same password on every site they visit. We then have to build features into our application to let them login when the password is forgotten or allow them to change the password at any time. We have to ensure we hold passwords securely even if our application is not that important because a user might be trusting us with their use-everywhere password.

Our applications are easier to manage with less support issues if we use a password-less login process. Such schemes are popular on sites like Medium with their 'magic link' or Notion.so with their login code such as jay-bawl-sack-lid

There is a good discussion of these password-less login methods in this article on medium.

So, how could we implement such a solution with a new Laravel 8 Jetstream project?

This article is using Jetstream with Livewire (this site is TALL stack focussed) but the principles should hold for Inertia also.

Prepare

I'm starting with a new Laravel 8 project, with Jetstream installed in Livewire flavour.

Remove the requirement for passwords

Our first task is to remove passwords from the Login process. To make this simple, we will give everyone a default password of 'password'. It won't be used, but it prevents us having to make too many changes to the Fortify+Jetstream code.

Remove password fields from login and register forms

resources/views/auth/login.blade.php
            <div>
                <x-jet-label value="Email" />
                <x-jet-input class="block w-full mt-1" type="email" name="email" :value="old('email')" required autofocus />
            </div>
{{-- 
            <div class="mt-4">
                <x-jet-label value="Password" />
                <x-jet-input class="block w-full mt-1" type="password" name="password" required autocomplete="current-password" />
            </div>
--}}
            <input type="hidden" value="password" name="password" />

Note the additional line for creating a hidden password field with the value 'password'

Create user with default password

Fortify actions are available in the app/Actions/Fortify folder. We can adjust the CreateNewUser.php file to use our default password.

Here, the password field is commented out of the validation, and the password added to the user record is just the hash of the string 'password'.

Testing: We should now be able to register a user, and login without any password. We have built a very insecure application at this point!

Article sponsored by SixTokens.com

Create a source of pass-phrases

For this solution a passphrase is a combination of 3 or 4 words separated by hyphens. The words are sourced from a list published by the EFF and are chosen because they are short and easy to spell.

Rather than publish the code and word list here, you can access it via https://github.com/snapey/passphrase

  • Create a folder within app called Utility

  • Place PassPhrase.php and wordlist.txt in this folder

Put pass-phrase into session when user logs in

When the user registers, Laravel will fire a Registered event and when the user is logged in either by the login form or the remember function, then a Login event is fired. We are going to listen for these events and place our randomly generated pass-phrase into session. Ultimately, the user will only be allowed access to our application if they can provide the same code that we have in session.

Create a listener

php artisan make:listener RequirePassPhrase

This uses our utility class to create a pass-phrase and store it along with an expiry timestamp.

With this; $this->generator->passPhrase(3); we create a phrase with three words. Set this according to your preferences. The EFF article mentioned earlier explains;

for k words chosen from a list of length n, there are nk possible passphrases of this type. It will take an adversary about nk/2 guesses on average to crack this passphrase.

Our wordlist is approximately 4100 words, so 3 words is 4100x4100x4100/2 = 34,400,000,000 guesses so don't go overboard with the number of words in the passphrase.

Bind our Listener to Events

Listening for events is configured in the app\Providers\EventServiceProvider. We add our listener for the two events. We can remove the email verification listener as if the user can receive the passcode then they verified the email at the same time.

Send the pass-phrase to our user

Having created the code and put it in session we need to send this to the user. Notifications are the easiest to implement here, and you could, for instance, choose to send the code to the user by one of the other notification methods such as SMS text.

Create the notification

php artisan make:notification AdvisePassPhrase

Adjust your new notification (use your own words as required)

Call the Notification and provide the pass-phrase

In our earlier Listener, add a line to send the notification;

Line 7 is added to the earlier file

Testing

Provided we have configured a mail service such as mailtrap, when we register or login, a mail should be received containing our passphrase.

Accept and validate the pass-phrase

Create Controller

php artisan make:controller PassPhraseController

Call this from your routes file

Create a form for the capture of the pass-phrase

The easiest route with a new application is to just copy the Login view and edit a few of the fields;

Testing

  • Visit the route /login/confirm and check you can see the form.

  • Entering an invalid code should show the message that the code is incorrect

  • Entering a valid code should direct to the home route

  • Waiting 15 minutes and entering a code should report that the code has expired

Add middleware to block access until pass-phrase accepted

We can create a middleware that checks the user's session. If it contains a passphase key then the user is in the middle of logging in and should not be permitted to access the application. We need to except the login routes from the middleware so that the user can access the login process.

Make Middleware

php artisan make:middleware PassPhraseGuard

Include the Middleware as a global route middleware

Include the new middleware in your web middleware stack

We added Line 10

Testing

  • Once logged in, access to all pages should be blocked, directing the user to the confirm pass-phrase page.

  • Landing on the site after 15 minutes should return to the guest mode

  • Remember me should work as normal

Cleaning up

Remove references to passwords

In the config/fortify.php file, turn off the ability to reset and change passwords by commenting out the resetPasswords and updatePasswords features.

Conclusion

In this article we created a password-less login process that uses a pass-phrase technique. In the second part of this article we will add a magic-link alternative.

Feedback

If you have any suggestions how this article can be improved, contribute to discussion at https://github.com/snapey/talltips/discussions

Last updated

Was this helpful?