Building a Dev-Only Magic Login Command for Laravel
During local development, how many times have you needed to quickly log in as a different user? Maybe you're testing role-based permissions, debugging a user-specific issue, or just don't remember the password for your test accounts.
The typical workflow involves navigating to /login, entering credentials, submitting the form, and waiting for the redirect — repeated for every user switch. Over time, this becomes a real productivity drain.
What if you could just run:
php artisan dev:login-link 5And get a one-time URL that instantly logs you in as user #5?
In this post, I'll show you how to build this as a reusable Laravel package — covering signed routes, Artisan commands, service providers, and environment-based security.
What We're Building
A package that provides:
- An Artisan command
dev:login-linkthat generates a secure, short-lived login URL - Auto-creation of a default admin user if the database is empty
- Routes that only exist in local/development environments
- Signed URLs that can't be tampered with
Note: This assumes you have session-based auth set up (e.g., Laravel Breeze or Jetstream).
Without it, the login works but you won't see an authenticated UI — you'll just land on/with no visible change.
The Core Concepts
Signed Routes
Laravel's signed routes let you generate URLs with a signature hash that prevents tampering. Combined with temporarySignedRoute(), we get URLs that expire:
$url = URL::temporarySignedRoute(
'dev.login',
now()->addMinutes(10),
['user' => $user->id]
);This produces something like:
https://myapp.test/dev-login/5?expires=1699900000&signature=abc123def456...If anyone modifies the URL, the signature check fails.
If the expiration time passes, the URL becomes invalid.
Environment-Based Loading
We only want this in development. Laravel makes this easy:
if ($this->app->environment(['local', 'development', 'testing'])) {
$this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
}In production, the routes don’t even exist.
The APP_ENV variable in your .env file controls this:
APP_ENV=localLogin Flow — How the Dev Login Link Works
This is the exact runtime flow when using the dev login link during local development:
- The developer runs an Artisan command.
- Laravel generates a signed, temporary URL.
- The developer opens the URL in the browser.
- Laravel validates the signature and expiration time.
- The user is authenticated via the session.
- The user is redirected into the application.
This makes it clear that no passwords are involved and that Laravel’s native security mechanisms are responsible for enforcing access.
Login Flow Diagram
High-level flow of the dev-only magic login command, from Artisan to authenticated session.
Developer
|
| php artisan dev:login-link {userId}
v
Artisan Command
|
| Generates temporary signed URL
v
Signed Login URL
|
| Browser opens URL
v
Laravel Route (signed middleware)
|
| Signature + expiry validated
v
DevLoginController
|
| Auth::login(user)
v
Authenticated Session
|
v
Dashboard / HomeBuilding the Package
Directory Structure
laravel-dev-login-link/
├── composer.json
├── README.md
├── routes/
│ └── web.php
└── src/
├── DevLoginLinkServiceProvider.php
├── Console/
│ └── DevLoginLinkCommand.php
└── Http/
└── Controllers/
└── DevLoginController.phpThis follows standard Laravel package conventions:
- PSR-4 root in
src/ - Console logic under
Console - HTTP controllers under
Http/Controllers - Routes loaded via the service provider
The composer.json
{
"name": "agile-creative-minds/laravel-dev-login-link",
"description": "Dev-only magic login link command for Laravel",
"type": "library",
"license": "MIT",
"require": {
"php": "^8.2",
"illuminate/auth": "^11.0|^12.0",
"illuminate/console": "^11.0|^12.0",
"illuminate/routing": "^11.0|^12.0",
"illuminate/support": "^11.0|^12.0"
},
"autoload": {
"psr-4": {
"AgileCreativeMinds\\DevLoginLink\\": "src/"
}
},
"extra": {
"laravel": {
"providers": [
"AgileCreativeMinds\\DevLoginLink\\DevLoginLinkServiceProvider"
]
}
}
}The extra.laravel.providers section enables package auto-discovery, so no manual service provider registration is required.
The Service Provider
<?php
namespace AgileCreativeMinds\DevLoginLink;
use Illuminate\Support\ServiceProvider;
class DevLoginLinkServiceProvider extends ServiceProvider
{
public function boot(): void
{
if ($this->app->environment(['local', 'development', 'testing'])) {
$this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
}
if ($this->app->runningInConsole()) {
$this->commands([
Console\DevLoginLinkCommand::class,
]);
}
}
}Key points:
- Routes only load in non-production environments
- The command is registered only when running in the console
The Artisan Command
// Full source available on GitHub — see link at the end of this postKey implementation details:
- Blocks execution in production
- Respects custom user models via
auth.providers.users.model - Auto-creates a default admin if the database is empty
- Generates a signed URL valid for 10 minutes
The Route
<?php
use AgileCreativeMinds\DevLoginLink\Http\Controllers\DevLoginController;
use Illuminate\Support\Facades\Route;
Route::get('/dev-login/{user}', DevLoginController::class)
->name('dev.login')
->middleware(['web', 'signed']);Why these middleware matter:
web— enables session handlingsigned— validates the URL signature and expiration
The Controller
<?php
namespace AgileCreativeMinds\DevLoginLink\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
class DevLoginController extends Controller
{
public function __invoke(Request $request, int $user): RedirectResponse
{
$userModel = config('auth.providers.users.model', 'App\\Models\\User');
$userInstance = $userModel::findOrFail($user);
Auth::login($userInstance, remember: false);
if (Route::has('dashboard')) {
return redirect()->route('dashboard');
}
return redirect('/');
}
}The controller is invokable because it serves a single purpose.remember: false avoids persisting long-lived login state for dev sessions.
Security Considerations
Security Model — Why This Is Safe in Development
This approach intentionally bypasses password authentication, but it remains safe for local development because security is enforced at multiple independent layers:
- Environment gating
The routes and Artisan command only exist inlocal,development, ortestingenvironments. In production, they are never loaded. - Signed URLs
Login links are generated using Laravel’s signed routes and are cryptographically protected using your application’sAPP_KEY. Any modification invalidates the link. - Expiration window
Each login link is short-lived and automatically expires after a fixed time (10 minutes). - Console-only generation
Links can only be generated via the terminal. Anyone with shell access to the environment is already trusted.
Together, these layers create a defense-in-depth model. An attacker would need both shell access and a valid non-production environment, which already implies full system access.
Security Enforcement Diagram
[ Request Login URL ]
|
v
[ Environment Check ]
(local / dev / testing?)
|
┌────┴────┐
| Yes |
└────┬────┘
|
v
[ Signed URL Validation ]
- Signature matches APP_KEY
- Parameters untampered
|
v
[ Expiration Check ]
- Within time window?
|
v
[ Login Allowed ]
Auth::login(user)This tool intentionally bypasses password authentication — but remains safe for development:
- Environment gating
Routes and commands only exist inlocal,development, ortesting. - Signed URLs
URLs are cryptographically signed using yourAPP_KEY. - Short expiration
Links expire after 10 minutes. - Shell access required
If someone has terminal access, you already have bigger problems.
Never deploy this to production.
composer install --no-devUsing It Locally with Composer Path Repositories
During development, you can test the package without publishing it.
{
"repositories": [
{
"type": "path",
"url": "./packages/laravel-dev-login-link"
}
]
}Then require it:
composer require agile-creative-minds/laravel-dev-login-link:@devComposer symlinks the package, so changes apply instantly.
The Result
$ php artisan dev:login-linkOne-time login link (expires in 10 minutes):
https://myapp.test/dev-login/1?expires=1699900000&signature=a1b2c3d4e5f6...Fresh database?
No users found. Created default admin user:
+----------+---------------------+
| Field | Value |
+----------+---------------------+
| Email | admin@example.com |
| Password | xK9mP2nQ4rT7 |
+----------+---------------------+What You've Learned
This small package touches many core Laravel concepts:
- Signed routes for secure, expiring URLs
- Environment-based loading
- Package structure and auto-discovery
- Flexible user model resolution
- Artisan command design and output formatting
The same pattern applies to:
- Invite links
- Email verification
- One-time downloads
- Passwordless flows
This pattern is small in scope but broad in application. Once you understand signed routes and environment-based loading, you'll find yourself reaching for this approach in many other scenarios — invite links, email verification, one-time downloads, and passwordless flows all follow the same principles.
Source Code
- GitHub: https://github.com/Agile-Creative-Minds/laravel-dev-login-link
- Packagist: https://packagist.org/packages/agile-creative-minds/laravel-dev-login-link
Install it:
composer require agile-creative-minds/laravel-dev-login-link --dev
Have questions or improvements? Open an issue or PR on GitHub.