Use Basic Authentication to protect the Laravel Horizon Dashboard

• 3 min read

Laravel Horizon is probably the best Laravel package in the ecosystem. I recently added Horizon to a project, which does not have a users-table and does not provide any form of authentication. To still access the Horizon dashboard in production, I've updated the HorizonServiceProvider to use HTTP Basic Authentication. Thanks to that, the dashboard is still protected by a combination of username and password.

Here's how I've done this.

Let's start with the ServiceProvider. Open HorizonServiceProvider.php and update the class like so:

  1. Add an authorization()-method to the class. By adding the method in our version of the ServiceProvider, we overload the method on the HorizonApplicationServiceProvider and instruct Horzion to allow all users access to the dashboard.
  2. Remove the no longer required gate()-method. gate() has been called from the authorization()-method on the HorizonApplicationServiceProvider. As we've overloaded the authorization()-method, gate() is no longer required.

The ServiceProvider should now look like this:

namespace App\Providers;

use Laravel\Horizon\Horizon;
use Laravel\Horizon\HorizonApplicationServiceProvider;

class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        parent::boot();
    }

    /**
     * Overload authorization method from \Laravel\Horizon\HorizonApplicationServiceProvider
     * to allow access to Horizon without having a logged in user.
     *
     * @return void
     */
    protected function authorization()
    {
        Horizon::auth(function ($request) {
            return true;
        });
    }
}

Next we create a new Middleware. Currently, Horizon allows access to all users, which is something we don't want. (Remember: we still want to restrict access to the dashboard to a defined group of users).

First we need a place to store the username and password strings which we will use in our Basic Authentication Middleware. As we're adding this functionality to Horizon, I think it makes sense to add the config values to the config/horizon.php-file.

Append the following basic_auth configuration array to the config/horizon.php-file.

// config/horizon.php

return [

    // ...

    'basic_auth' => [
        'username' => env('HORIZON_BASIC_AUTH_USERNAME'),
        'password' => env('HORIZON_BASIC_AUTH_PASSWORD'),
    ],

];

Also don't forget to add the new env variables HORIZON_BASIC_AUTH_USERNAME and HORIZON_BASIC_AUTH_PASSWORD to your .env and .env.example file. (Sensitive information like passwords should never be stored in files committed to the repository).

Next, we create a new Middleware by using the artisan-CLI (php artisan make:middleware HorizonBasicAuthMiddleware) or by creating a new file: app/Http/Middleware/HorizonBasicAuthMiddleware.php.

Update the handle()-method with the following code:

namespace App\Http\Middleware;

use Closure;

class HorizonBasicAuthMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $authenticationHasPassed = false;

        if ($request->header('PHP_AUTH_USER', null) && $request->header('PHP_AUTH_PW', null)) {
            $username = $request->header('PHP_AUTH_USER');
            $password = $request->header('PHP_AUTH_PW');

            if ($username === config('horizon.basic_auth.username') && $password === config('horizon.basic_auth.password')) {
                $authenticationHasPassed = true;
            }
        }

        if ($authenticationHasPassed === false) {
            return response()->make('Invalid credentials.', 401, ['WWW-Authenticate' => 'Basic']);
        }

        return $next($request);
    }
}

A quick explanation on whats happening here:

First a $authenticationHasPassed variable is set to false. We'll use this variable later to determine if an "unauthenticated" response should be returned. Then we read the header values for "PHP_AUTH_USER" and "PHP_AUTH_PW". These headers contain the values a user might have entered in to the browser to authenticate.

Next we check if the username and password values match the values we've defined in our configuration file. If they pass, we set the $authenticationHasPassed-variable to true.

Finally, we check the $authenticationHasPassed-variable. If the value is still false, we return a 401 response and set the WWW-Authenticate-header to Basic. This will prompt the user to fill in the username and password. If the value is true – meaning authentication has passed – we let Laravel process the request further and allow access to the Horizon dashboard.

The last step is to register the new Middleware and use it for Horizon. Add the following line to the $routeMiddleware-property in App\Http\Kernel.php:

protected $routeMiddleware = [
    // your other Middlewares
    'horizonBasicAuth' => \App\Http\Middleware\HorizonBasicAuthMiddleware::class,
];

Now add horizonBasicAuth to the Middleware array in the Horizon configuration. The config/horizon.php-file should look like this.

    /*
    |--------------------------------------------------------------------------
    | Horizon Route Middleware
    |--------------------------------------------------------------------------
    |
    | These middleware will get attached onto each Horizon route, giving you
    | the chance to add your own middleware to this list or change any of
    | the existing middleware. Or, you can simply stick with this list.
    |
    */

    'middleware' => ['web', 'horizonBasicAuth'],

When you now try to access the Horizon dashboard under http://your-app.test/horizon, your browser will prompt you to enter the correct username and password.