outatime-io / filament-login-shortcut
A secure login shortcut selector for Filament panels.
Package info
github.com/outatime-io/filament-login-shortcut
pkg:composer/outatime-io/filament-login-shortcut
Requires
- php: ^8.2
- filament/filament: ^4.7 || ^5.2
- illuminate/support: ^12.0 || ^13.0
Requires (Dev)
- larastan/larastan: ^2.9 || ^3.0
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0 || ^11.0
- pestphp/pest: ^3.0 || ^4.0 || ^5.0
- pestphp/pest-plugin-laravel: ^3.0 || ^4.0 || ^5.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-06 18:22:57 UTC
README
Filament Login Shortcut
A secure login shortcut selector for Filament panels.
Warning
This package provides passwordless sign-in as any eligible user account. Enabling it outside a local environment can grant access to privileged accounts. Non-local use requires explicit authorization through the built-in IP allow-list or an application-defined authorization callback, plus appropriate network and organizational controls. Production use should generally remain disabled, even though deliberate production activation is technically possible.
Filament Login Shortcut is a plugin for Filament panels that adds a passwordless sign-in shortcut to the panel login screen: an authorized visitor picks an eligible user account from a searchable select and signs in as that user without entering a password. It removes the friction of repeatedly typing credentials while developing or testing an application.
The shortcut ships disabled, must be enabled explicitly per panel, refuses to render outside explicitly allowed environments unless the built-in IP allow-list or an application-defined authorization callback approves the request, and never bypasses canAccessPanel().
Features
- Searchable sign-in selector rendered before the panel login form via Filament's documented
AUTH_LOGIN_FORM_BEFORErender hook - Explicit opt-in per panel; disabled by default
- Fail-closed authorization: environment allow-list with a mandatory IP allow-list or callback outside
local - Four mutually exclusive eligibility strategies: all users, exact email addresses, email domains, custom query
- Tunable search: columns, result limit, minimum search length, debounce
- Rate limiting per panel, session, and IP for searches and logins
- Minimal audit data: events carry identifiers, never emails or names; no IP addresses logged by default
- Hardened sessions: regeneration on login and same-host redirect protection
- Broad compatibility: Filament 4.7+ and 5.2+ on PHP 8.2–8.5
Compatibility
| Package | Supported versions |
|---|---|
| PHP | 8.2–8.5 |
| Laravel | 12–13 (as resolved by Filament) |
| Livewire | 3.x with Filament 4; 4.x with Filament 5 |
| Filament | 4.7+ and 5.2+ |
Filament v3 is intentionally unsupported. CI tests representative combinations of the lowest and current supported Laravel and PHP versions for both supported Filament release series, starting from the minimum versions listed above.
Installation
Install the package as a development dependency:
composer require --dev outatime-io/filament-login-shortcut
Laravel package discovery registers the service provider automatically; no manual provider registration is required.
Optionally publish the configuration file if you want to override the documented fallback defaults:
php artisan vendor:publish --tag=filament-login-shortcut-config
Deploy production and staging with composer install --no-dev --optimize-autoloader; this keeps the passwordless login code out of those deployments entirely.
Use a normal dependency only when you intentionally need the login shortcut in a non-local environment, such as a protected staging system. In that case, configure an explicit non-local authorization callback and ensure the package is installed in that environment.
Basic usage
Register the plugin on every intended panel in its panel provider:
use Filament\Panel; use OutatimeIo\FilamentLoginShortcut\LoginShortcutPlugin; public function panel(Panel $panel): Panel { return $panel ->plugins([ LoginShortcutPlugin::make() ->enabled(), ]); }
The feature is off unless enabled() is called — even locally.
On the login screen, the plugin renders a Login shortcut block above the normal login form with a user search field. It lists eligible users immediately; typing filters them by the configured search columns (email by default). Pick an account and press Login as user: you are signed in as that user and redirected to the intended URL or the panel. If the panel guard already has an authenticated session, the block does not render.
The plugin uses Filament's documented AUTH_LOGIN_FORM_BEFORE hook, so it appears before the normal form without replacing it. A shared Livewire component provides debounced, bounded search.
Enablement and environments
Environment variables are deliberately not supported: FILAMENT_LOGIN_SHORTCUT_ENABLED and FILAMENT_LOGIN_SHORTCUT_ALLOWED_ENVIRONMENTS do nothing, so availability cannot be toggled per deployment by accident. Local is always allowed once the plugin is enabled, so a local-only setup needs nothing beyond the registration shown above:
To allow a non-local environment, include its exact name in the panel-provider allow-list. Explicit authorization is mandatory outside local and is re-evaluated for render, each search, and submit: either the built-in IP allow-list or an authorization callback. The IP allow-list covers the overwhelmingly common policy of restricting access to specific source addresses:
use Filament\Panel; LoginShortcutPlugin::make() ->enabled(true) ->allowedEnvironments(['local', 'staging']) ->allowedIps(['10.0.0.5', '203.0.113.7']);
allowedIps() performs exact matches against request()->ip(); trusted-proxy configuration therefore governs what that returns, and the package never parses X-Forwarded-For itself. Entries are trimmed, validated, and canonicalized — 2001:0DB8::7 and its fully expanded spelling both match a client reported as 2001:db8::7; empty or malformed values throw InvalidConfiguration. An explicitly empty list denies every non-local client. Pass a closure returning the list when it must be resolved per request, e.g. from configuration; resolution errors are reported through the exception handler and fail closed to a denying empty list:
use Illuminate\Http\Request; use OutatimeIo\FilamentLoginShortcut\LoginShortcutPlugin; LoginShortcutPlugin::make() ->enabled(true) ->allowedEnvironments(['local', 'staging']) ->allowedIps(fn (): array => config('services.login_shortcut.allowed_ips', [])) // Composes with any explicit callback: both must pass. ->authorizeUsing( fn (Request $request): bool => $request->hasHeader('X-My-Proxy-Assertion'), );
Other good policies include a VPN/private-network check, an application access policy, or an identity-aware proxy assertion. Do not trust X-Forwarded-For or similar headers unless Laravel trusted proxies are configured correctly. Exceptions fail closed. A translated warning is displayed whenever an authorized non-local component renders.
Eligible users and search
The Select is filtered only by the active eligibility strategy:
allUsers()— the default — lists every user from the configured model.usersWithEmails()andusersWithEmailDomains()list only the matching users.usersUsingQuery()lists only the users returned by its Eloquent query.
Strategies are mutually exclusive: the last strategy method called replaces the previous one. There is deliberately no ID-list strategy.
// All users (default) LoginShortcutPlugin::make()->enabled(true)->allUsers(); // Exact email addresses LoginShortcutPlugin::make()->enabled(true)->usersWithEmails([ 'admin@example.test', 'editor@example.test', ]); // Exact email domains; @ is optional LoginShortcutPlugin::make()->enabled(true)->usersWithEmailDomains(['local.test']); // Custom query must return a Builder for the configured user model use Illuminate\Database\Eloquent\Builder; LoginShortcutPlugin::make()->enabled(true)->usersUsingQuery( fn (Builder $query): Builder => $query->where('is_admin', true), );
Domain matching is parameterized and suffix-based (person@example.test matches, person@notexample.test and person@example.test.attacker.test do not). It lowercases both sides; database collation and Unicode case folding may still differ by engine.
Important:
canAccessPanel()does not filter the initially displayed users or search results. Changing a user'scanAccessPanel()result alone will never add or remove that user from the Select.
The package calls canAccessPanel() only after a user has been selected and the Login as user button is pressed. If it returns false, login is denied; the user may still have appeared in the Select. To restrict what the Select shows in the first place, use an eligibility strategy — with the custom query above, non-admin users are never queried for or shown at all.
The default model is App\Models\User, label is email, search column is email, limit is 20, minimum length is 1, and debounce is 300ms. The model must be an Eloquent model implementing Authenticatable; integer, UUID, ULID, and other string auth identifiers are supported.
use App\Models\User; use Illuminate\Contracts\Auth\Authenticatable; LoginShortcutPlugin::make() ->userModel(User::class) ->searchColumns(['email', 'name']) ->searchResultLimit(20) ->minimumSearchLength(1) ->searchDebounce(300) ->userLabelUsing( fn (Authenticatable $user): string => sprintf('%s (%s)', $user->getAttribute('name'), $user->getAttribute('email')), );
Search never runs below the minimum length, escapes SQL wildcard input, limits results with a hard maximum, and returns only identifier and label. At submission, the selected identifier is looked up again through the trusted constrained query.
Plugin API reference
All fluent methods return the plugin instance, so they can be chained in a panel provider. The configuration methods in the first table are the supported public configuration API; the accessors in the last table exist for Filament and the package's internal services.
| Method | Argument | Default | Purpose |
|---|---|---|---|
make() |
none | — | Creates a new plugin instance. |
enabled(bool|Closure $enabled = true) |
Boolean or callback receiving Request, Panel, and environment |
false |
Turns the feature on or off. It must be enabled even on local. |
allowedEnvironments(array $environments) |
Exact environment names | ['local'] |
Allows named non-local environments. Local is always allowed once enabled. |
userModel(string $model) |
Eloquent Authenticatable model class |
App\\Models\\User |
Changes the queried and authenticated model. Invalid model classes throw InvalidConfiguration. |
searchColumns(array $columns) |
Simple database column names | ['email'] |
Defines searchable columns. An empty list or unsafe name throws InvalidConfiguration. |
searchResultLimit(int $limit) |
Positive integer | 20 |
Caps initially shown and searched results; the hard maximum still applies. |
minimumSearchLength(int $length) |
Positive integer | 1 |
Suppresses remote search below this character count. |
searchDebounce(int $milliseconds) |
Integer at least zero | 300 |
Sets the search debounce interval in milliseconds. |
userLabelUsing(Closure $callback) |
fn (Authenticatable $user): string |
User email |
Produces the visible user label. |
authorizeUsing(Closure $callback) |
fn (Request $request, Panel $panel, string $environment): bool |
none | Required outside local unless allowedIps() is set; use IP/VPN, policy, or proxy authorization. false or exceptions deny access. Composes with allowedIps(): both must pass. |
allowedIps(array|Closure $ips) |
Exact IP addresses, or a callback returning them | none | Built-in source-IP allow-list enforced outside local via exact match against request()->ip(). Satisfies the non-local authorization requirement on its own and composes with authorizeUsing() (both must pass). Entries are canonicalized so equivalent IPv6 spellings match; empty or malformed entries throw InvalidConfiguration; an explicitly empty list denies all non-local clients; closure errors are reported and fail closed. Ignored in local. |
allUsers() |
none | active default strategy | Lists every configured-model user. It never filters the Select by canAccessPanel(). Replaces another strategy. |
usersWithEmails(array $emails) |
Exact email addresses | none | Restricts eligible users to these addresses. Replaces another strategy. |
usersWithEmailDomains(array $domains) |
Exact domains, optional @ |
none | Restricts eligible users to these domain suffixes. Replaces another strategy. |
usersUsingQuery(Closure $callback) |
fn (Builder $query): Builder for configured model |
none | Provides a custom eligible-users query. Replaces another strategy; a wrong builder/model throws InvalidConfiguration. |
logIpAddresses(bool $value = true) |
Boolean | config audit.log_ip_addresses (false) |
Includes IPs in audit records. Enable only with suitable retention controls. |
transformIpAddressUsing(Closure $callback) |
fn (?string $ip): ?string |
none | Transforms an IP before it is included in audit data, e.g. hashes it. |
Package configuration reference
Published configuration supplies fallbacks for the options below when a panel-provider method is not called. Enablement and allowed environments are configured only through the panel provider.
| Key | Default | Notes |
|---|---|---|
user_model |
App\\Models\\User |
Fallback user model. |
search.columns |
['email'] |
Fallback searchable columns. |
search.result_limit |
20 |
Fallback result limit. |
search.hard_maximum |
100 |
Absolute result-limit ceiling. |
search.minimum_length |
1 |
Fallback search threshold. |
search.debounce |
300 |
Fallback debounce in milliseconds. |
rate_limits.searches_per_minute |
60 |
Per panel/session/IP search limit. |
rate_limits.logins_per_minute |
10 |
Per panel/session/IP login limit. |
rate_limits.denials_per_minute |
20 |
Per panel/session/IP denial limit. |
audit.log_channel |
null |
Optional Laravel log channel for concise audit records. |
audit.log_ip_addresses |
false |
Whether audit entries include IP data. |
Integration accessors
These public methods exist for Filament and the package's Livewire/query services. Application code normally configures the methods above instead of calling these directly.
| Method | Returns | Purpose |
|---|---|---|
getId() |
string |
The fixed plugin identifier: filament-login-shortcut. |
register(Panel $panel) |
void |
Registers the login-form render hook. Called by Filament. |
boot(Panel $panel) |
void |
Filament lifecycle hook; currently no operation. |
isEnabled() |
bool|Closure |
Resolves the panel-provider enabled value. |
environments() |
array |
Resolves normalized allowed environments. |
model() |
string |
Resolves and validates the configured user model. |
columns() |
array |
Resolves and validates search columns. |
limit() |
int |
Resolves the result limit, capped by search.hard_maximum. |
minimumLength() |
int |
Resolves the search threshold. |
debounce() |
int |
Resolves the debounce interval. |
label() |
?Closure |
Returns the optional user-label callback. |
authorization() |
?Closure |
Returns the optional authorization callback. |
ipAllowlist() |
?array |
Resolves the normalized built-in IP allow-list, if set. Closure results are validated at resolution time; errors yield an empty, denying list and are reported. |
query() |
?Closure |
Returns the custom-query strategy callback, if set. |
emails() |
?array |
Returns the exact-email strategy list, if set. |
domains() |
?array |
Returns the domain strategy list, if set. |
shouldLogIps() |
bool |
Resolves whether audit entries include IP data. |
ipTransformer() |
?Closure |
Returns the optional IP-transform callback. |
Authentication and auditing
The component refuses authenticated panel guards, uses the current panel's configured guard, and checks canAccessPanel() when present after a user is selected. It regenerates the session and redirects only to a same-host intended URL or the panel URL. It has separate per-panel/session/IP rate limits for search and login. It does not replace an existing session or act as impersonation.
Three audit events are dispatched through Laravel's event() dispatcher and can be consumed with standard listeners:
Event (namespace OutatimeIo\FilamentLoginShortcut\Events) |
Public properties |
|---|---|
AutoLoginSucceeded |
userModel, identifier, panelId, environment, occurredAt, ipAddress |
AutoLoginDenied |
reason, panelId, environment, occurredAt, ipAddress |
AutoLoginFailed |
reason, panelId, environment, occurredAt, ipAddress |
Properties contain identifiers (not emails or names), panel ID, environment, an immutable timestamp, and a stable reason code where relevant (rate_limited, already_authenticated, invalid_selection, panel_access_denied, …). ipAddress is null unless explicitly enabled; when enabled, transform it before it is stored, e.g. hash it:
LoginShortcutPlugin::make() ->logIpAddresses() ->transformIpAddressUsing(fn (?string $ip): ?string => $ip ? hash('sha256', $ip) : null);
Set audit.log_channel in package configuration for optional concise log records. No database audit table is created.
Privacy
You remain responsible for lawful purpose, access controls, log security, retention, deletion, incident review, and any IP-address processing. This package follows data minimization but does not itself make an application GDPR compliant.
Testing
Run composer validate, composer format, composer analyse, and composer test. The GitHub workflow covers representative PHP, Laravel, and Filament combinations from the compatibility table above.
Contributing and security
Bug reports and pull requests are welcome — see CONTRIBUTING.md. Please report suspected vulnerabilities privately as described in SECURITY.md.
Changelog
Notable changes are documented in the changelog.
License
Released under the MIT license.