Laravel PHP

Building a Dev-Only Magic Login Command for Laravel

Feb 05, 2026 3 min read

Learn how to build a Laravel dev-only login command using signed routes, environment gating, and short-lived URLs.

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 5

And 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-link that 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=local

Login Flow — How the Dev Login Link Works

This is the exact runtime flow when using the dev login link during local development:

  1. The developer runs an Artisan command.
  2. Laravel generates a signed, temporary URL.
  3. The developer opens the URL in the browser.
  4. Laravel validates the signature and expiration time.
  5. The user is authenticated via the session.
  6. 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 / Home

Building the Package

Directory Structure

laravel-dev-login-link/
├── composer.json
├── README.md
├── routes/
│   └── web.php
└── src/
    ├── DevLoginLinkServiceProvider.php
    ├── Console/
    │   └── DevLoginLinkCommand.php
    └── Http/
        └── Controllers/
            └── DevLoginController.php

This 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 post

Key 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 handling
  • signed — 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:

  1. Environment gating
    The routes and Artisan command only exist in local, development, or testing environments. In production, they are never loaded.
  2. Signed URLs
    Login links are generated using Laravel’s signed routes and are cryptographically protected using your application’s APP_KEY. Any modification invalidates the link.
  3. Expiration window
    Each login link is short-lived and automatically expires after a fixed time (10 minutes).
  4. 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:

  1. Environment gating
    Routes and commands only exist in local, development, or testing.
  2. Signed URLs
    URLs are cryptographically signed using your APP_KEY.
  3. Short expiration
    Links expire after 10 minutes.
  4. Shell access required
    If someone has terminal access, you already have bigger problems.

Never deploy this to production.

composer install --no-dev

Using 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:@dev

Composer symlinks the package, so changes apply instantly.

The Result

$ php artisan dev:login-link
One-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

Install it:

composer require agile-creative-minds/laravel-dev-login-link --dev

Have questions or improvements? Open an issue or PR on GitHub.