maize-tech/laravel-magic-login
Laravel Magic Login adds passwordless authentication to your Laravel app using temporary signed magic links sent by email. Users log in with one click, no password required, with configurable expiry and easy customization.
Install and publish
composer require maize-tech/laravel-magic-login
php artisan vendor:publish --provider="Maize\MagicLogin\MagicLoginServiceProvider"
php artisan migrate
Publish both the config and the migration — the package needs a table to persist issued tokens, otherwise link generation will fail at runtime (not at install time), which is a common first-day surprise.
Wire it into your authenticatable model
Add the package trait to the model you want to log in (usually App\Models\User, but any Authenticatable works):
use Maize\MagicLogin\Concerns\HasMagicLogin;
class User extends Authenticatable
{
use HasMagicLogin;
}
Where to look first, in this order
config/magic-login.php — token lifetime, route prefix/middleware, guard, redirect target, and whether package routes are registered at all.expires_at column type and indexes; if you need per-guard scoping you'll be adding a column here.First use case
Replace a "forgot password" flow, or bolt passwordless login onto an existing app without touching your password auth at all: keep Auth::attempt() for normal logins, and add a "email me a login link" endpoint next to it. The user clicks the link, the token is consumed, and they land authenticated. This is the lowest-risk adoption path because it's additive — no changes to the users table and no change to existing guards.
Request-a-link endpoint + mail
Keep the "send link" action behind your own controller or form request rather than exposing the model call directly. That gives you a single place for rate limiting, user lookup policy (don't leak which emails exist), and audit logging:
public function __invoke(MagicLinkRequest $request)
{
$user = User::where('email', $request->email)->first();
// Always return the same response — don't confirm account existence.
if ($user) {
$user->sendMagicLoginLink(); // package-provided; check trait for exact name
}
return back()->with('status', 'If that account exists, a link is on its way.');
}
Customise the mail without forking
The default notification is intentionally plain. Swap in your own by extending/replacing the notification class in config, or by overriding the trait method that dispatches it. Always implements ShouldQueue on your replacement — sending synchronously inside the login request is the most common cause of slow passwordless endpoints.
Disable package routes and own the endpoints
For anything beyond a prototype, set the route-registration flag to false in the config and declare routes yourself. You keep your app's middleware stack (throttle, locale, tenant resolution), and you can version the URLs:
Route::middleware(['throttle:6,1'])->group(function () {
Route::post('/login/link', SendLoginLink::class)->name('login.link');
Route::get('/login/link/{token}', ConsumeLoginLink::class)->name('login.consume');
});
Multi-guard / multi-model setups
If you have admin and user guards, issue and consume tokens per guard. The config's guard setting is global, so either run two config contexts or add a guard (or model_type) column to the tokens table and resolve the guard at consumption time. Without this you'll authenticate an admin into the web guard and get silent redirect loops.
Post-login destination
Prefer a configurable redirect plus Laravel's intended() fallback so a user who was bounced from a deep link returns there after clicking the email. Hardcoding /home in the config is the usual first thing you'll change.
Testing
Notification::fake();
$this->post('/login/link', ['email' => $user->email])->assertSessionHasNoErrors();
Notification::assertSentTo($user, MagicLoginNotification::class);
$token = MagicLoginToken::where('user_id', $user->id)->latest()->first();
$this->get(route('login.consume', $token->token))
->assertRedirect();
$this->assertAuthenticatedAs($user);
// Single-use assertion
$this->get(route('login.consume', $token->token))->assertForbidden();
Use $this->travel(31)->minutes() to test expiry rather than sleeping or mocking the clock.
Housekeeping
Schedule pruning of expired tokens (MagicLoginToken::where('expires_at', '<', now())->delete() in a daily job). The table grows monotonically otherwise, and on high-traffic apps it's a real index-bloat problem within months.
Referrer-Policy: no-referrer (or strict-origin-when-cross-origin) on the consumption route if the post-login page loads any third-party assets.auth:api/auth:admin is your real guard but the config says web, login "succeeds" and the user still appears logged out. Verify Auth::guard(...)->check() in a test, not by eye.route:cache and config:cache will hide newly installed routes and config changes. Run php artisan optimize:clear before debugging a missing endpoint.hash_equals() or compare hashed tokens in the query (constant-time, and avoids timing oracles). Never == on raw token strings.laravel/framework constraint, not the package.How can I help you explore Laravel packages today?