serenity_technologies / admin-dashboard-guard
Two-factor passphrase+password admin authentication guard for any admin-restricted Laravel page (Horizon, Telescope, Pulse, custom routes, etc.).
Package info
github.com/Serenity-Technologies/admin-dashboard-guard
pkg:composer/serenity_technologies/admin-dashboard-guard
Requires
- php: ^8.1
- illuminate/auth: ^11.0|^12.0|^13.0
- illuminate/http: ^11.0|^12.0|^13.0
- illuminate/routing: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Suggests
- laravel/sanctum: Required if sanctum_support is enabled in the package config (^3.0|^4.0).
README
A Laravel package that places a two-factor passphrase + password authentication wall in front of any admin-restricted page — Horizon, Telescope, Pulse, Filament, or any custom route — with zero extra database tables.
How It Works
Access to a protected page requires two sequential steps:
- Passphrase step — The admin provides a short-lived secret passphrase (bcrypt-hashed in the database). On success, a 10-minute session window opens.
- Password step — Within that window, the admin enters their account password to receive a fully-authenticated session (valid for 60 minutes by default).
Each protected page runs its own independent session, so authenticating to /horizon does not grant access to /telescope.
Generating a passphrase via Artisan emails it directly to the admin and schedules automatic revocation after a configurable delay.
Requirements
| Requirement | Version |
|---|---|
| PHP | ^8.1 |
| Laravel | ^11.0 | ^12.0 | ^13.0 |
| Laravel Sanctum (optional) | ^3.0 | ^4.0 |
Sanctum is only needed if you want to accept a bearer-token as an alternative to the session flow. It is listed under
suggestincomposer.jsonand is not required by default.
Installation
1. Require the package
composer require serenity_technologies/admin-dashboard-guard
The service provider is auto-discovered via extra.laravel.providers in composer.json.
2. Publish the config
php artisan vendor:publish --tag=admin-dashboard-guard-config
This creates config/admin-dashboard-guard.php.
3. Add the passphrase column to your admin model
php artisan make:migration add_passphrase_to_admins_table
// In the migration: $table->string('passphrase')->nullable();
Run the migration:
php artisan migrate
4. Configure your admin model
Set the model in .env:
ADMIN_DASHBOARD_MODEL=App\Models\Admin
Your model must implement Illuminate\Contracts\Auth\Authenticatable and have both a passphrase column (nullable, stores a bcrypt hash) and a password column.
5. Configure the auth guard
Ensure an admin guard is defined in config/auth.php:
'guards' => [ 'admin' => [ 'driver' => 'session', 'provider' => 'admins', ], ], 'providers' => [ 'admins' => [ 'driver' => 'eloquent', 'model' => App\Models\Admin::class, ], ],
Then set in .env:
ADMIN_DASHBOARD_GUARD=admin
Protecting a Page
The package registers a named middleware alias for every entry in protected_pages. The alias format is admin.{key}.
Horizon
In config/horizon.php:
'middleware' => ['web', 'admin.horizon'],
Telescope
In app/Providers/TelescopeServiceProvider.php:
protected function gate(): void { Gate::define('viewTelescope', fn () => auth('admin')->check()); }
And add the middleware to the route group in your telescope service provider or routes/web.php:
Route::middleware(['web', 'admin.telescope'])->group(function () { // Telescope routes });
Any Custom Route
You can protect any route or route group with the admin.{key} alias, where key matches an entry in protected_pages:
// Protect Pulse Route::middleware(['web', 'admin.pulse'])->group(function () { Route::get('/pulse', ...); }); // Protect a custom admin panel Route::middleware(['web', 'admin.myadmin'])->prefix('admin')->group(function () { // your admin routes });
Adding a New Protected Page
Open config/admin-dashboard-guard.php and add an entry to protected_pages:
'protected_pages' => [ 'horizon' => [ 'path' => 'horizon', 'gate' => 'viewHorizon', 'session_prefix' => 'horizon', 'passphrase_ttl' => 10, 'session_ttl' => 60, 'theme_color' => 'purple', ], 'telescope' => [ 'path' => 'telescope', 'gate' => 'viewTelescope', 'session_prefix' => 'telescope', 'passphrase_ttl' => 10, 'session_ttl' => 60, 'theme_color' => 'indigo', ], // Add any new page here: 'pulse' => [ 'path' => 'pulse', 'gate' => 'viewPulse', // set null to skip gate registration 'session_prefix' => 'pulse', 'passphrase_ttl' => 10, 'session_ttl' => 60, 'theme_color' => 'rose', ], ],
The package automatically:
- Registers an
admin.pulsemiddleware alias - Defines a
viewPulsegate (or skips it ifgateisnull) - Registers
GET /pulse/passwordandPOST /pulse/passwordroutes for the password step
protected_pages Options
| Key | Type | Default | Description |
|---|---|---|---|
path |
string |
(key name) | URL path segment (e.g. 'horizon' → /horizon) |
gate |
string|null |
'view{Key}' |
Gate name to define. Set null to skip. |
session_prefix |
string |
(key name) | Namespace prefix for session keys |
passphrase_ttl |
int |
10 |
Minutes the passphrase verification window is valid |
session_ttl |
int |
60 |
Minutes the fully-authenticated session is valid |
theme_color |
string |
'blue' |
Tailwind colour name used in the login views |
stealth_mode |
bool |
false (global) |
When true, unauthenticated requests receive 404. Overrides the global default for this page. |
Stealth Mode
When stealth mode is enabled the auth wall is completely invisible to unauthenticated requests. The passphrase form is never shown — every bare access attempt returns a plain 404 Not Found, exactly as if the route did not exist.
The only way in is to carry the passphrase directly in the URL query string:
https://yourdomain.com/horizon?passphrase=a8Kq2mRvXpLtN4sYdZbWcJeF7hUgOiTy
A valid passphrase silently redirects to the password step as normal. A wrong passphrase also returns 404 (not 401), so nothing about the auth wall is revealed.
Enable globally (all protected pages)
# .env ADMIN_DASHBOARD_STEALTH=true
Or directly in the published config:
'stealth_mode' => true,
Enable per page (overrides the global default)
'protected_pages' => [ 'horizon' => [ // ... 'stealth_mode' => true, // Horizon: 404 when unauthenticated ], 'telescope' => [ // ... 'stealth_mode' => false, // Telescope: still shows the passphrase form ], ],
Tip: Combine stealth mode with a short
--delaywhen generating passphrases so the URL is valid only for a small window of time.
Artisan Commands
All commands are prefixed with admin-guard: to avoid collisions with the host application.
Generate a passphrase for one admin
php artisan admin-guard:generate-passphrase admin@example.com
Generates a random passphrase, bcrypt-hashes and saves it to the admin record, emails it to the admin, then queues an auto-removal job after --delay hours.
| Option | Default | Description |
|---|---|---|
--bcc= |
— | BCC address for the notification email |
--length= |
32 |
Passphrase length (minimum 16) |
--delay= |
1 |
Hours before the passphrase is automatically revoked |
Example output:
Passphrase generated for: John Smith (admin@example.com)
Passphrase : a8Kq2mRvXpLtN4sYdZbWcJeF7hUgOiTy
Auto-removes in 1 hour(s).
Generate passphrases for all admins
php artisan admin-guard:generate-passphrases
Runs the single-admin flow for every record returned by the configured model. Accepts the same --bcc, --length, and --delay options.
Remove a passphrase for one admin
php artisan admin-guard:remove-passphrase admin@example.com
Immediately nulls out the passphrase column for the given admin, invalidating any active passphrase-step session.
Clear passphrases for all admins
php artisan admin-guard:clear-passphrases
Nulls the passphrase column for every admin that currently has one set. Prompts for confirmation unless --force is passed.
| Option | Description |
|---|---|
--force |
Skip the confirmation prompt (useful in scripts or CI) |
Incident response tip: Run
admin-guard:clear-passphrases --forceto instantly revoke all active passphrases if you suspect a passphrase has been compromised.
Configuration Reference
// config/admin-dashboard-guard.php return [ // The Laravel auth guard used to authenticate the admin. 'guard' => env('ADMIN_DASHBOARD_GUARD', 'admin'), // Fully-qualified Eloquent model class. 'model' => env('ADMIN_DASHBOARD_MODEL', null), // Column names on the model. 'passphrase_column' => env('ADMIN_DASHBOARD_PASSPHRASE_COLUMN', 'passphrase'), 'password_column' => env('ADMIN_DASHBOARD_PASSWORD_COLUMN', 'password'), // Accept a valid Sanctum personal-access token as an alternative to the // session flow. Requires laravel/sanctum. 'sanctum_support' => env('ADMIN_DASHBOARD_SANCTUM', true), // Pages to protect — see "Adding a New Protected Page" above. 'protected_pages' => [ /* ... */ ], ];
Environment Variables
| Variable | Default | Description |
|---|---|---|
ADMIN_DASHBOARD_GUARD |
admin |
Auth guard name |
ADMIN_DASHBOARD_MODEL |
null |
Fully-qualified model class |
ADMIN_DASHBOARD_PASSPHRASE_COLUMN |
passphrase |
Column storing the bcrypt passphrase hash |
ADMIN_DASHBOARD_PASSWORD_COLUMN |
password |
Column storing the bcrypt password hash |
ADMIN_DASHBOARD_SANCTUM |
true |
Enable Sanctum bearer-token fallback |
Customising the Views
Publish the Blade views to override them:
php artisan vendor:publish --tag=admin-dashboard-guard-views
Files land in resources/views/vendor/admin-dashboard-guard/:
| View | Purpose |
|---|---|
passphrase-login.blade.php |
Step 1 — passphrase entry form |
password-login.blade.php |
Step 2 — password entry form |
emails/admin-passphrase.blade.php |
Passphrase notification email |
Each view receives $toolName, $toolPath, and $themeColor variables.
Customising the Passphrase Email Subject
Pass a custom subject when constructing the mailable directly:
use SerenityTechnologies\AdminDashboardGuard\Mail\AdminPassphrase; Mail::to($admin)->send(new AdminPassphrase($passphrase, 'Your Dashboard Passphrase'));
Sanctum Bearer Token Fallback
When sanctum_support is true and laravel/sanctum is installed, the middleware also accepts a valid personal-access token belonging to an instance of the configured model class. This lets programmatic/API clients access protected pages without going through the browser-based session flow.
Security Notes
- Passphrases are never stored in plain text — only a bcrypt hash is saved in the database.
- Each passphrase hash is verified with
Hash::check()by iterating admin records; no plain-text comparison occurs. - The passphrase step has a configurable short TTL (10 minutes by default) and is cleared from the session once the password step completes.
- The fully-authenticated session is TTL-scoped per protected page, so sessions for
/horizonand/telescopeare completely independent. - Use
admin-guard:clear-passphrases --forceto invalidate all active passphrases immediately during a security incident. - The auto-removal delay (
--delay) is enforced via a queued job so that passphrases can never be left active indefinitely by accident.
License
MIT