# Laravel Passwordless Login: signed magic links for a Laravel application

> This package gives a Laravel application a temporary signed route that logs a user in when it is visited, and stops there. It deliberately does not send the link, because it makes no assumption about how you talk to your users. What it does give you is the generation side, a per-guard configuration trait, and a set of environment variables covering expiry, single use and revocation.

**edalzell/laravel-passwordless-login** — A simple, safe magic login link generator for Laravel

- Repository: https://github.com/edalzell/laravel-passwordless-login
- Stars: 874 · Forks: 74
- Language: PHP
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/edalzell-laravel-passwordless-login

## What the package does, and the one thing it refuses to do

The package description is a simple, safe magic login link generator for Laravel, and the README's first sentence after the title is more precise: it provides a temporary signed route that logs in a user.

Then comes the sentence that defines the package's scope. What it does not provide is a way of actually sending the link to the route to the user, and the reason given is that the author does not want to make any assumptions about how you communicate with your users. That is a reasonable and fairly common boundary for a package in this space: the delivery mechanism is the part where applications differ most, whether you send an email, a text message, a push notification or a printed letter, and the part where a library that picked one would force a dependency on a mail provider or a queue.

The consequence is that you write the sending code yourself and the package gives you a URL. The README's usage example ends with a comment saying exactly that, to send the URL in an email or text message to the user.

Under the hood the mechanism is Laravel's own signed URL mechanism, since the package exposes a temporary signed route and the configuration has a login route path, an HTTP verb for it and an expiry measured in minutes. The README also documents handling InvalidSignatureException and ExpiredSignatureException in the application's Handler.php file, which is the Laravel way of reacting to a broken link.

The tree is small and conventional for a Laravel package: a src/ directory, a config/ directory, a routes/ directory, a tests/ directory, a composer.json, a phpunit.xml and a CHANGELOG.md.

## Installing it and generating a first link

Installation is a single Composer command:

```shell
composer require grosv/laravel-passwordless-login
```

The package name in the Composer repository is grosv/laravel-passwordless-login, which is worth noting because the GitHub repository you are reading belongs to a different organisation than the one publishing the package. The README's badges also point at the grosv repository for the build status, so the project appears to have moved between owners while keeping the same package identity.

Generating a link takes three steps in the README's simple usage example. Find the user, construct a LoginUrl with the user, and call generate:

```php
use App\User;
use Grosv\LaravelPasswordlessLogin\LoginUrl;

function sendLoginLink()
{
    $user = User::find(1);

    $generator = new LoginUrl($user);
    $generator->setRedirectUrl('/somewhere/else'); // Override the default url to redirect to after login
    $url = $generator->generate();

    //OR Use a Facade
    $url = PasswordlessLogin::forUser($user)->generate();

    // Send $url in an email or text message to your user
}
```

The two forms are equivalent. The object form lets you override settings on the instance before generating, which is how setRedirectUrl changes where the user lands after login, and the facade form is the short path when you want the defaults.

The README is unusually blunt about the failure mode it worries about most. The biggest mistake it can see someone making is creating a login link for one user and sending it to another, and it asks you to be careful and test your code. That is a real hazard in any magic link flow, and the package cannot prevent it because the binding between the generated URL and the delivery channel lives in your code.

## The trait for applications with several kinds of user

Applications that authenticate more than one kind of user, customers and administrators for instance, need per-model defaults. The package handles that with a trait.

Including the trait gives the model a set of accessor methods, and you implement only the ones you want to change. Each one reads from the configuration:

```php
use Grosv\LaravelPasswordlessLogin\Traits\PasswordlessLogin;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use PasswordlessLogin;

    public function getGuardNameAttribute(): string
    {
        return config('laravel-passwordless-login.user_guard');
    }

    public function getShouldRememberLoginAttribute(): bool
    {
        return config('laravel-passwordless-login.remember_login');
    }

    public function getLoginRouteExpiresInAttribute(): int
    {
        return config('laravel-passwordless-login.login_route_expires');
    }

    public function getRedirectUrlAttribute(): string
    {
        return config('laravel-passwordless-login.redirect_on_success');
    }
}
```

With the trait in place you can call createPasswordlessLoginLink() directly on the user object and get a link built from the trait's defaults, which is shorter than constructing a generator each time.

The guard question has its own section in the README, and it is the detail that bites in a multi-guard application. If User is on the web guard and Admin is on the admin guard, each model overrides getGuardNameAttribute to return its own:

```php
class Admin extends Authenticatable
{
    use PasswordlessLogin;

    public function getGuardNameAttribute(): string
    {
        return 'admin';
    }
}
```

The package uses that value both to retrieve the user and to log them in, so a link generated for an Admin authenticates against the admin guard rather than against the globally configured LPL_USER_GUARD. That is exactly the behaviour you want, and it is also the behaviour that would be a security problem if it were not there, since an admin session created through a user guard would be the wrong kind of session entirely.

## Configuration: expiry, single use, cache markers and the route itself

The settings live in a config file you can publish, or you can set them in the .env file. The README lists the full set:

```dotenv
LPL_REMEMBER_LOGIN=false
LPL_LOGIN_ROUTE=/magic-login
LPL_LOGIN_ROUTE_ACTION=get
LPL_LOGIN_ROUTE_NAME=magic-login
LPL_LOGIN_ROUTE_EXPIRES=30
LPL_REDIRECT_ON_LOGIN=/
LPL_USER_GUARD=web
LPL_USE_ONCE=false
LPL_REQUIRE_CACHE_MARKER=false
LPL_CACHE_STORE=
LPL_INVALID_SIGNATURE_MESSAGE="Expired or Invalid Link"
```

Most of these are self-explanatory once you know the vocabulary. LPL_REMEMBER_LOGIN controls whether the session is a remembered one, like the user ticking Remember Me. LPL_LOGIN_ROUTE is the path of the route the package registers, and the README warns to make sure it does not collide with one of your other routes. LPL_LOGIN_ROUTE_NAME is the route name, and the same collision warning applies. LPL_LOGIN_ROUTE_EXPIRES is the lifetime of the link in minutes, and the README recommends the shortest value that makes sense for your use case. LPL_REDIRECT_ON_LOGIN is where the user lands afterwards.

LPL_LOGIN_ROUTE_ACTION selects the HTTP verb, get or post, and it defaults to get. If you use post you have to exclude the route from CSRF verification, and the README links Laravel's documentation on excluding URIs from CSRF.

The two that deserve more attention are LPL_USE_ONCE and LPL_REQUIRE_CACHE_MARKER. LPL_USE_ONCE makes a link expire after first use, so it is consumed and cannot be replayed. LPL_REQUIRE_CACHE_MARKER is the switch that makes revocation possible: with it on, a link is only valid if it also has a matching entry in the cache. The README explains why it defaults to false, and the reason is backwards compatibility, since links generated before the feature existed, or before an upgrade that introduced it, would otherwise stop working. It also warns that turning it on means a cleared or evicted cache invalidates every outstanding link, so the cache store has to be durable enough for the link lifetimes.

LPL_CACHE_STORE selects the cache store from config/cache.php, blank meaning the application default. The guidance is to point markers at a specific store when they need to survive things that do not affect link validity, such as a cache:clear on deploy, a Redis restart or maxmemory eviction. That advice matters most once the marker requirement is on, because then whether the marker survives decides whether the link works at all.

## Revoking a link before it expires

A magic link that lives for thirty minutes is a window in which something can go wrong: the user sets a password afterwards, or changes their email address, and the old link still works. The package has an explicit revocation call for that case.

```php
use Grosv\LaravelPasswordlessLogin\PasswordlessLogin;

// Revoke any outstanding magic link for a user
PasswordlessLogin::invalidateForUser($user);
```

The README's example of when to use it is exactly the case above, after a user sets a password or changes their email. The intended flow is that a sensitive change triggers revocation of any link that was outstanding.

Generating a new link clears any prior invalidation, so issuing a fresh link needs nothing more than calling generate(). That is a sensible design, since the newest link should win.

The caveat is the important part, and the README states it in a note: invalidateForUser() only takes effect on multi-use links when LPL_REQUIRE_CACHE_MARKER is true. Otherwise a link's own signature and expiry are all that get checked, and the revocation call is a no-op. Single-use links always check the cache marker regardless of that setting, because they have to be tracked to know they were consumed.

So the behaviour you get depends on two settings working together, and the default configuration is the conservative one where revocation does nothing. If your application depends on revocation working, you have to turn the marker requirement on and make sure your cache is durable, which is the same condition as before.

## Limits: it does not send, and revocation is off by default

Two limits deserve to be stated before you pick this package.

The first is the one the README leads with. There is no delivery. You get a URL and you decide how it reaches the user, which means you own the channel, the rate limiting, the expiry of the message in the user's inbox and the audit trail of what was sent. For a package whose entire purpose is to reduce phishing risk by avoiding passwords, that is a significant amount of responsibility left with the caller. If your organisation needs an auditable record of issued login links, you are building that yourself.

The second is that the strongest security feature is not on by default. The cache marker requirement defaults to false, and with it false the invalidateForUser() call does nothing for multi-use links. The reasoning given is backwards compatibility, which is a legitimate concern, but it means that installing the package and calling the revocation method gives a false sense of control unless you also set LPL_REQUIRE_CACHE_MARKER=true and configure a cache that survives. That coupling between two settings is easy to miss, and the README only states it in a note under the revocation section.

A third smaller point: the package signs links and validates them on the server, but it does not address the case of a link that leaks through a shared mailbox, a referrer header or a browser history. That is inherent to the magic link pattern rather than a defect here, and it is the reason the README recommends the shortest workable expiry.

The last practical note is about the guard model. Overriding getGuardNameAttribute is how a multi-guard application avoids authenticating an admin through the user guard. It is opt-in per model, which means a model that forgets the override silently inherits the global guard, and the failure is invisible until it matters.

## This package against Laravel's built-in signed URLs and against a full auth package

Two comparisons clarify where this sits.

The first is against using Laravel's own signed URLs directly. The framework can sign a route with a temporary signed URL, and it can validate the signature and the expiry in a middleware. Building the flow yourself is therefore possible: sign a route, check the user, call Auth::login. The difference in approach is that this package adds the pieces an authentication flow needs beyond the signature, namely registering the route, the guard handling, the remember-me option, the redirect target, the invalid-signature message and the revocation mechanism. If your application already has a working signed-url login flow, this package is redundant. If you are starting from nothing, it saves you from assembling those pieces and, more usefully, from assembling them inconsistently across several user types.

The second is against a larger authentication package with magic links as one feature among many, typically also handling social login, two-factor and session management. The difference is scope in both directions. A larger package gives you one place to configure all of that, and it gives you a maintained answer to the awkward questions. This package gives you a smaller surface with one job done carefully, no opinion about the rest of your authentication, and a codebase small enough to read in full.

The deciding factor is usually whether your application already has an authentication story. A Laravel application with a conventional login form and a need to add a magic link for one flow is the case this package fits. A new application that needs passwordless login, social login and multi-factor from scratch is better served by something that treats all three as a coherent whole.

## Conclusion

Adopt laravel-passwordless-login when your Laravel application needs to authenticate a user from a link you send by email or SMS, and you want the signing, expiry and guard handling handled for you rather than hand-rolled. Do not adopt it if you need the package to deliver the link, because it never sends anything, and do not enable single-use or revocation behaviour without a durable cache. Verify first that your chosen expiry is short enough, that LPL_LOGIN_ROUTE does not collide with an existing route or route name, and that if you turn on LPL_REQUIRE_CACHE_MARKER your cache store survives a deploy.

## FAQ

### How do I install laravel-passwordless-login and generate a magic link?

Install it with composer require grosv/laravel-passwordless-login, then create a LoginUrl for the user and call generate, or use the facade with PasswordlessLogin::forUser($user)->generate(). The package returns a URL and does not send it, so you deliver it by email or text message yourself.

### How do I make a Laravel magic login link expire after a single use?

Set LPL_USE_ONCE=true in the environment file. The README notes that single-use links always check the cache marker regardless of the LPL_REQUIRE_CACHE_MARKER setting, since they have to be tracked to know they were consumed.

### Why does PasswordlessLogin::invalidateForUser() not revoke my links?

The README states that the call only takes effect on multi-use links when LPL_REQUIRE_CACHE_MARKER is true. Otherwise a link's own signature and expiry are all that is checked, so the revocation is a no-op, and enabling the marker means your cache must be durable enough to outlive your links.

## Sources

- [edalzell/laravel-passwordless-login on GitHub](https://github.com/edalzell/laravel-passwordless-login)
- [Issues](https://github.com/edalzell/laravel-passwordless-login/issues)
- [License: MIT](https://github.com/edalzell/laravel-passwordless-login/blob/main/LICENSE)
- [README](https://github.com/edalzell/laravel-passwordless-login/blob/main/README.md)
- [Releases](https://github.com/edalzell/laravel-passwordless-login/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/edalzell-laravel-passwordless-login
