Download the PHP package arzcode/filament-magic-login without Composer

On this page you can find all versions of the php package arzcode/filament-magic-login. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package filament-magic-login

Filament Magic Login

Passwordless "email me a login link" for Filament panels, added alongside the standard email/password form rather than replacing it. Multi-panel aware, single-use hashed tokens, translated into English, Spanish and Catalan.

The action appears in one of two places, chosen per panel:

MagicLinkPosition::BelowForm (default) MagicLinkPosition::EmailFieldHint
A full-width button under the "Sign in" button, set apart by an "or" rule — same shape, no colour. A labelled link on the email field's hint row.

Nothing about the password form changes: password login, password reset and registration keep working exactly as before.


Requirements

PHP 8.3+
Laravel 12 or 13
Filament 5

Installation

The install command offers to publish the config file and — unless you have chosen the cache storage driver — to publish and run the migration that creates the magic_login_tokens table and to schedule the token pruner in routes/console.php. It then offers to register the plugin on a panel for you, which is the step that puts the action on your login page, and finally to add the "send a login link" action to your user resource.

Every step is a question and every one can be declined; nothing here is mandatory. Each one defaults to yes — pressing Enter through the command installs the package — except publishing the config file, which defaults to no: every option has a working default in the package, so that file is only worth having when you mean to edit it, and the defaults still apply without it.

Because the answers are the defaults, running it with --no-interaction (CI, a Docker build) wires everything up unattended. Every edit it makes is named as it happens, only ever adds to your code, and filament-magic-login:uninstall takes it all back out again; anything it cannot rewrite with certainty is printed for you to paste rather than guessed at. It also stays quiet about whatever is already in place, so re-running the command is safe.

Your users table is never touched.

Uninstalling

The uninstall command undoes what the install command did, in the order that leaves the application working at every step:

  1. Removes ->plugin(MagicLoginPlugin::make()) from your panel providers, the HasMagicLinkAction trait from any custom login page, SendMagicLinkAction::make() from whichever action arrays hold it, and the scheduled pruner from routes/console.php, along with the now-unused imports — Laravel's own Schedule import included, where nothing else came to use it. Without this, composer remove would leave your source pointing at classes that no longer exist and every request would fail. An array left with nothing in it is kept rather than deleted — a getHeaderActions() still has to return one.
  2. Drops the magic_login_tokens table.
  3. Deletes the published config, migration and translations.

Both destructive steps are confirmed first, and the table question defaults to no. Keeping the table is a fine answer — installing the package again finds it in place and the migration it republishes steps over it rather than failing — but it does mean the old tokens are still there, so the command says as much and names the flag that drops it.

Option Effect
--force Skip every confirmation (for CI or scripted teardown).
--drop-tokens Drop the table without asking, keeping the other confirmations.
--keep-tokens Leave the table and its rows alone.
--keep-code Do not touch your source; report the panels to unwire by hand instead.

The code edits work on PHP's token stream, not regular expressions, and the result is parsed before it is written — a file that would not compile is left untouched. Registrations it cannot rewrite with certainty (a grouped use A, B;, a plugin built through a variable, a conditional registration) are reported with their file and line numbers rather than guessed at, as are any other references such as event listeners importing MagicLinkConsumed.

Register the plugin

filament-magic-login:install offers to do this for you. To do it by hand, add the plugin to any panel that already calls ->login():

That is the whole setup. The plugin swaps Filament's stock login page for its own subclass, so you do not have to create any files. The installer only edits a panel provider it can rewrite with certainty — a provider that returns its panel from more than one place, for instance, is reported and left for you. If your panel uses a custom login page, see Using your own login page.

Users who belong to another panel

By default a link is only sent when the user can reach the panel whose login page they are on. Anyone else gets the same "check your inbox" confirmation and no email, because the page never says whether an address exists. With two panels, that means a client who asks on the admin login page gets nothing.

sendToReachablePanel() sends them a link for a panel they can reach instead:

The panel they asked at is still tried first, so a user who can reach it stays there. Each fallback is checked with canAccessPanel() in its own right, and the link uses that panel's own lifetime, route and "remember me" setting. The confirmation on the page stays exactly the same.

Only panels on the same auth guard qualify, since the user was looked up through that guard. The true form skips any panel that fails that or lacks the plugin. A named list throws a LogicException instead, so a typo shows up as an error rather than as missing emails.

Sending a link to a user

An administrator can email a specific user a login link from inside a panel, choosing how long it lives — for somebody being onboarded who has never set a password, for a user whose reset mail keeps bouncing, for a contractor who needs a way in until Monday.

One class covers all three placements. In the resource's table:

and in getHeaderActions() on the View and Edit pages:

In the table row the action renders as an icon button — hover for its label — while the two page-header actions keep their label. Pass any explicit style (->button(), ->iconButton(), ->link()) to override that per placement.

filament-magic-login:install offers to do all of that for you. It finds your user resource by comparing each resource's $model against the model your panel's guard authenticates, and follows a resource to an extracted table class through its own imports. If it cannot identify one resource with certainty — none match, or two do — it prints the snippet and leaves your source alone. Re-generating the resource with make:filament-resource --force will wipe the wiring; re-run the installer, or paste it back.

Three things worth being explicit about:

Fluent method Default What it does
panel(string\|Closure\|null) the current panel Which panel the link is minted for.
panels(array\|Closure\|null) — Panels to consider, best first: the first one the user can reach wins.
anyPanel(bool\|Closure) false The same, over every panel that registers the plugin.
expiresAfter(int\|Closure\|null) admin.expires_after_minutes Which choice is pre-selected.
maxExpiresAfter(int\|Closure\|null) admin.max_expires_after_minutes Ceiling for this placement.
expiryPresets(array\|Closure\|null) admin.expiry_presets The buttons offered, in minutes.
askForExpiry(bool\|Closure) true false sends at the default with no field.
ability(string\|Closure\|null) admin.ability Gate ability checked against the target user.
icon(string\|Closure\|null) admin.icon, then icon Icon for this placement. Filament's own method.

Everything Filament\Actions\Action offers works as usual — ->label(), ->icon(), ->visible(), ->authorize(), ->after(). To change the wording of the confirmations, override ->action().

The envelope is the default icon in all three placements, and in the modal. Change it once for the whole panel on the plugin, or per placement on the action:

Pass false to either plugin method for no icon at all. adminIcon() follows icon() when you have not set it, so an application that wants one icon everywhere says so once. Both take a BackedEnum — Heroicon::Key — as readily as a string.

A name no icon set has falls back down that same chain rather than rendering: heroicon-s-email is not a heroicon, and a typo like that would otherwise be a SvgNotFound — an error page on the whole users table, from one string in a config file. That check runs on whatever the icon turns out to be, including a name passed to Filament's own ->icon() on the action, and logs the fallback once so the icon quietly being the envelope again is at least explained in laravel.log. It covers this action only: a bad icon name on any other action in your table is still Filament's error to throw.

Who may send a link

By default the action is visible to anyone who can already reach the row or the record page, which means it inherits whatever your resource's canViewAny() / canView() and your panel's auth middleware already decide. That is where an application with opinions about who may administer users has already expressed them.

To require a policy ability as well:

The default is not that, on purpose: Laravel's Gate denies an ability no policy defines, so shipping 'sendMagicLink' as the default would make the action vanish in every application that installed it.

If you run more than one panel, set admin.ability. ->panel('app'), ->panels([...]) and ->anyPanel() all let an administrator of one panel mint a login credential for another, which is the feature working as intended and exactly the case worth gating.

Three guards are always on regardless. The action hides itself when the record is not something that can be authenticated, when the record is the administrator's own row — somebody already signed in has no use for a link to where they already are — and when the user fails canAccessPanel() on the target panel. A link the panel would turn away at the door is not worth offering, and there is nothing the administrator could do about it anyway. SendMagicLinkToUser refuses that last one too, which is what covers access revoked between the page being drawn and the modal being submitted.

Note that "the target panel" is the one the link is minted for, not the one you are looking at. A user who cannot reach your admin panel but can reach your app panel gets no action on the admin panel's users table by default — and does get one where you have said which panel to mint for:

One action, several panels

Naming one panel per placement only works when you know which panel each user belongs to. Where a users table mixes them — staff in the admin panel, clients in app — hand the action a list instead, best first. The link is minted for the first panel that row's user can actually reach:

So a colleague gets an admin-panel link, a client gets an app one, and someone in both is sent where you are standing, because admin comes first in the list. ->anyPanel() is the same over every panel that registers the plugin, current panel first, for when you would rather not keep the list up to date:

Where the panel is not the one you are looking at, the confirmation modal says which one it is, so "send a login link" never quietly means a different door than the administrator expects. The per-send settings — the lifetime, the presets, the ceiling, the rate limit — are read from that panel too, not from the one you are on.

Two placements can still sit side by side where you want the choice to be yours rather than the recipient's, each showing only for the users its own panel would admit:

Every panel you name has to register the plugin — ->anyPanel() skips the ones that do not, since that list is inferred rather than named. canAccessPanel() is called for each candidate panel, once per row per action, so keep it cheap (or eager-load whatever it reads) on a large table.

A note on the link itself: anyone already signed in to the panel who opens a link is sent to the panel home, and the token is not consumed. That covers a user opening the same email a second time, and an administrator clicking the link they just sent. It is intentional — a link should not silently swap an authenticated session — but it does mean "open it yourself to check" is not a test. Open it in a private window, or send it to a user who is signed out.

Configuration

Every option has a global default in config/filament-magic-login.php and a per-panel override on the plugin. Panel-level setters win; anything you do not set falls back to config.

Fluent method Config key Default What it does
expiresAfter(int\|Closure $minutes) expires_after_minutes 15 Link lifetime.
position(MagicLinkPosition\|Closure) position BelowForm Where the action renders.
label(string\|Closure\|null) — from translations Action label.
icon(string\|BackedEnum\|Closure\|false\|null) icon heroicon-o-envelope Icon on the login page's action. false removes it, and a name no icon set has falls back to the default.
notification(class-string\|Closure) notification MagicLinkNotification Notification used to deliver the link.
rateLimit(int $maxAttempts, int $decaySeconds) rate_limit.* 3 / 300 Limits link requests, keyed by email + IP.
consumeRateLimit(int $maxAttempts, int $decaySeconds) consume_rate_limit.* 10 / 60 Limits link consumption, keyed by IP.
redirectTo(string\|Closure\|null) — the panel URL Where to land after a successful login.
routePath(string\|Closure) route_path magic-login Route segment under the panel path.
invalidatePrevious(bool\|Closure) invalidate_previous true Drop the user's other unused tokens for this panel when a new link is issued.
honorRemember(bool\|Closure) honor_remember true Carry the "remember me" checkbox into the magic-link session.
useCustomLoginPage(bool\|Closure) — false Skip login page detection entirely.
sendToReachablePanel(array\|bool\|Closure) — false Send a user who cannot reach this panel a link for one they can. See Users who belong to another panel.
adminExpiresAfter(int\|Closure\|null) admin.expires_after_minutes falls back to expires_after_minutes Expiry pre-selected in the admin modal.
expiryPresets(array\|Closure) admin.expiry_presets [15, 60, 480, 1440, 4320] Toggle buttons offered there, in minutes.
maxAdminExpiresAfter(int\|Closure) admin.max_expires_after_minutes 4320 (3 days) Ceiling for an admin-chosen expiry.
adminRateLimit(int $maxAttempts, int $decaySeconds) admin.rate_limit.* 10 / 60 Limits admin-issued links, keyed by the administrator. 0 disables it.
adminAbility(string\|Closure\|null) admin.ability null Gate ability required to send a link.
adminIcon(string\|BackedEnum\|Closure\|false\|null) admin.icon follows icon Icon on the "send a login link" action and its modal. false removes it.
— queue true Queue the shipped notification sent from the login page. Admin-issued links are always sent right away. See Why the login page queues and the admin action does not.
— queue_connection deferred Connection for that queued notification. deferred sends it right after the response, with no worker. null uses your default connection.
— storage.driver database database or cache. Global only, not per panel.
— blur_timing true Pad unknown-address responses so timing does not leak account existence.
— log_rejections.* true / default channel / info Log every refusal with its reason. See Events.

A fuller example:

Two panels can run different settings at the same time — each panel gets its own plugin instance and its own consume route (filament.{panelId}.magic-login.consume).

Using your own login page

If the panel already has a custom login page, add the trait to it:

The plugin then leaves your page alone. If it finds a custom login page without the trait it throws a LogicException at boot rather than silently replacing your page — either add the trait, or call ->useCustomLoginPage() to opt out of the check entirely.

Registration order does not matter: ->login(MyLogin::class)->plugin(...) and ->plugin(...)->login(MyLogin::class) behave identically.

Customising the email

Any notification class works as long as it satisfies the constructor contract:

The simplest route is to extend Arzcode\FilamentMagicLogin\Notifications\MagicLinkNotification, which already implements the contract and gives you a translated MailMessage to tweak.

Queueing a custom notification is your call — implement ShouldQueue on it. The queue config option only decides whether the shipped notification is queued from the login page. A custom notification that implements ShouldQueue is queued from the admin action too, so "sent" there again means "handed to the queue".

Notifications are sent through Laravel's notification system, so a notifiable implementing HasLocalePreference receives the email in its own locale automatically.

Events

Listen to these for audit logging:

Event Fired when
MagicLinkRequested($user, $token, $panelId, $issuedBy) A link was issued and emailed.
MagicLinkConsumed($user, $token, $panelId) A link was redeemed and the user signed in.
MagicLinkRejected($reason, $email, $panelId, $ip) Anything was refused.

$issuedBy is the administrator who sent the link, or null when the user asked for it themselves on the login page — $event->wasIssuedByAdministrator() says which. It was added as an optional fourth argument rather than as a separate event on purpose: a new event class would have left every existing listener with an audit log quietly missing the more privileged half of its entries.

The attribution lives on this event only, so MagicLinkConsumed cannot tell you a redeemed link was admin-issued. If you need that, record it here keyed by $event->token->id and join on consumption.

MagicLinkRejected reasons: rate_limited, unknown_user, cannot_access_panel, invalid, expired, used. The first three are raised on the request side (where the UI deliberately stays silent), the rest on the consume side.

The package logs every rejection itself, at info on the default channel, as filament-magic-login: link rejected [unknown_user] with the reason, email, panel and IP as context. That is where to look when someone says the email never arrived. The entry holds the address as it was typed. Set log_rejections.enabled to false to turn it off, or change log_rejections.channel and log_rejections.level. For something richer, such as a security channel or an audit table, write your own listener:

Storage

database (default)

Adds a magic_login_tokens table. Each row stores the token hash, the panel, the guard, the remember flag, requesting IP and user agent, an expiry and a used_at timestamp — so you can audit exactly which links were issued and redeemed.

cache

No migration at all. Entries are keyed by hash and expire on their own.

Set it globally in config; it is not a per-panel option.

Pruning (database driver)

Rows are kept for 24 hours after they expire, then become prunable. The install command offers to schedule Laravel's pruner for you; to do it by hand, add this to routes/console.php:

The cache driver needs no pruning.

Note that filament-magic-login:uninstall does not remove this — it reports the lines and leaves them for you to delete, the same as any other code you wrote by hand.

Security notes

Why the login page queues and the admin action does not

The login page queues the email to avoid leaking which addresses have an account. The form answers the same way whether or not an address is registered, and for an unknown address it waits a random 50–150 ms (blur_timing) so the response takes about as long as a real send. That padding is sized for pushing a job onto the queue. Sending over SMTP inside the request takes hundreds of milliseconds to several seconds, so with queue => false a registered address would answer noticeably slower than an unknown one, and anyone timing the form could tell them apart. Queueing also keeps a slow or broken mail server from hanging the login form.

Leave queue on.

By default the email is sent right after the response, not by a worker. queue_connection defaults to Laravel's deferred connection, which runs the job in the same PHP process once the response has gone back to the browser. The visitor sees the confirmation straight away, the response time still does not depend on the mail server, and the email arrives within seconds instead of whenever a worker gets to it. That matters here: a user who waits too long asks for a second link, which invalidates the first, and then clicks the first email when it finally arrives.

Two things to know about deferred:

The admin action always sends right away. Enumeration is not a concern there, because the administrator already picked the user from the users table. What they need is an honest answer: sending right away means "sent" is only reported once the mail server accepted the email. If it were queued, a stopped worker would still report success while nothing arrives. The cost is a short wait in the modal, which is acceptable for an administrator.

Multi-factor authentication

A magic link does not trigger the 2FA challenge, by design. Receiving and clicking a link sent to the account's mailbox already proves possession of a second factor, so asking for a TOTP code on top would be a second challenge for the same guarantee.

Concretely: Filament 5 runs its multi-factor challenge inside the login page's authenticate() method rather than in middleware, and this package authenticates through the panel guard directly. Password logins on the same panel keep their challenge, untouched.

Bypassing the challenge does not bypass the enrolment requirement. A panel configured with ->multiFactorAuthentication([...], isRequired: true) still sends a magic-link user to the set-up page until they have registered a second factor, exactly as it would after a password login. Both behaviours are covered by tests.

If your threat model wants a TOTP code even after a mailbox-proven login, don't register this plugin on that panel — there is no option to re-add the challenge.

Email scanners

Outlook SafeLinks, Apple Mail Privacy Protection and similar scanners follow links before a human does. HEAD requests are answered with 204 No Content without consuming the token, which covers the common case. A scanner issuing a real GET will still burn the link; a confirmBeforeLogin() option (GET shows a "Continue" button, POST consumes) is planned for a later release.

Testing

Contributing

Pull requests are welcome. Please keep composer test, composer analyse and composer format:test green, and add any new user-facing string to all three language files — a test asserts they carry identical keys.

License

MIT. See LICENSE.


All versions of filament-magic-login with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
filament/filament Version ^5.0
illuminate/contracts Version ^12.35 || ^13.0
laravel/prompts Version ^0.3
spatie/laravel-package-tools Version ^1.92
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package arzcode/filament-magic-login contains the following files

Loading the files please wait ...