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.
Custom Authorization Conditions
You can define additional custom conditions to authorize an admin before allowing dashboard access. This is useful for checking user roles, statuses, or other custom criteria.
1. Global or Per-Page Config Condition Class
Create an invokable/callable class that receives the admin model, the HTTP request, and the tool name:
namespace App\Auth; use Illuminate\Http\Request; class AuthorizeAdminDashboardAccess { public function __invoke(mixed $admin, Request $request, string $tool): bool { return $admin && in_array($admin->role, ['SuperAdmin', 'Admin', 'Developer']); } }
Then register it globally in config/admin-dashboard-guard.php:
'condition' => App\Auth\AuthorizeAdminDashboardAccess::class,
Or configure it per-page inside the protected_pages array:
'protected_pages' => [ 'horizon' => [ 'path' => 'horizon', 'condition' => App\Auth\AuthorizeAdminDashboardAccess::class, ], ],
2. Runtime Callback (Closure)
Alternatively, register a closure check dynamically in your AppServiceProvider (or any service provider) using the static checkUsing method:
use SerenityTechnologies\AdminDashboardGuard\Http\Middleware\AuthenticateAdminForDashboard; AuthenticateAdminForDashboard::checkUsing(function ($admin, $request, $tool) { return in_array($admin->role, ['SuperAdmin', 'Admin', 'Developer']); });
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