Skip to main content
  1. Programming/

How Did We Protect Telescope And Horizon In Headless Laravel App?

Photo by @flyd2069 from Unsplash
Disclaimer: This is not the only way or the most efficient way to do it. It worked for me and I decided to share.

The problem #

Imagine you’re running a headless Laravel application which has bunch of APIs with only token based authentication. Well, in an API only application, authenticating Telescope and Horizon is not that straightforward. But you need both of these dashboards in your production environment to monitor your application’s performance and queue system. Leaving it wide open for everyone is not ideal scenario, as they may contain sensitive or confidential information.

So, what to do now? #

In my company, we faced this several times. Fortunately, like other parts of Laravel, you can customize the authentication system of these packages as well and it’s pretty simple. We planned to make use of that and to do some shenanigans to go our way. The idea is basically to set a secret hashed key for authentication. We can put this in .env and retrieve when we need. Then while visiting the dashboard we can append the unhashed password to the URL and match it against our secret key. We would go one step further so that we don’t have to append this key every time we go to a new link, e.g. /telescope/requests, /telescope/jobs.

Let’s dive in… #

First pick a secure password and put the hash of it in .env. We will call it INSPECTION_SECRET.

INSPECTION_SECRET='$2y$10$L/4eZD6mYq/xiORTOhOY9OtDZlKnjrFJZViOx.LpZakEuiQhggt4O' # P@55W0RD (pretty secure, huh?)

Since it is not a good idea to access environment variables via env() in production environment, we should keep it in one of our config files. Let’s put it in config/app.php.

'inspection_secret' => env('INSPECTION_SECRET'),

Great! Now we can get the hash using config('app.inspection_secret'). 🙌

We are using same secret for Telescope and Horizon. Feel free to define separate keys.

Okay, how to use this key? 🗝️ #

Now head over to the TelescopeServiceProvider class. There you will see a gate() method, but it requires a $user object which we don’t have. So it is not useful to us, instead, we would override the authorization() method of the TelescopeApplicationServiceProvider class.

Laravel\Telescope\TelescopeApplicationServiceProvider

20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * Configure the Telescope authorization services.
 *
 * @return void
 */
protected function authorization()
{
    $this->gate();

    Telescope::auth(function ($request) {
        return app()->environment('local') ||
                Gate::check('viewTelescope', [$request->user()]);
    });
}

Notice that it calls the gate() method, we will get rid of it. And inside the auth() method, we will pass our own closure. In TelescopeServiceProvider class, override the authorization() method with the following body:

App\Providers\TelescopeServiceProvider

73
74
75
76
77
78
protected function authorization()
{
  Telescope::auth(function($request) {
    //...
  });
}

Closure #

Now, let’s fill up the body of the closure.

App\Providers\TelescopeServiceProvider

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
protected function authorization()
{
  Telescope::auth(function($request) {
    // If app is in development mode,
    // don't require any password
    if (app()->environment('local')) {
      return true;
    }

    // If request has a password, check
    // it against our stored secret
    if ($request->filled('password')) {
      return Hash::check($request->get('password'), config('app.inspection_secret'));
    }

    return false;
  });
}

Alright, now to test, change your APP_ENV to production and hit telescope route, e.g. http://localhost:8000/telescope. Voila! You will see a 403 Forbidden page from Laravel. To open the gate, append ?password=P@55W0RD to the URL, e.g. http://localhost:8000/telescope?password=P@55W0RD and you will land in the dashboard.

But wait… #

Yes, it keeps loading forever, right? Open the network tab in browser developer tools and check. You would see swarm of forbidden requests. Why? Because we’ve locked the way and these requests do not have the key. To resolve this issue, we should save the key somewhere. How about session? Change your authorization() method like following.

App\Providers\TelescopeServiceProvider

 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
protected function authorization()
{
  Telescope::auth(function($request) {
    // If app is in development mode,
    // don't require any password
    if (app()->environment('local')) {
      return true;
    }

    // A password is already saved in session
    if (Session::has('inspection_secret')) {
        return Hash::check(
          Session::get('inspection_secret'),
          config('app.inspection_secret')
        );
    }

    // If request has a password, check
    // it against our stored secret
    if ($request->filled('password')
      && Hash::check($request->get('password'), config('app.inspection_secret'))) {
      // Store the key for future requests in session
      Session::put('inspection_secret', $request->get('password'));

      return true;
    }

    // In case any change in password, wipe out the old one
    Session::forget('inspection_secret');

    return false;
  });
}

Few things we’ve added. First, we are checking if the session has any previous password stored and matching against it. Second, if the request has a password, we are saving it for future requests in this session. So when we land in the dashboard our password is already saved in the session and all background requests will be authenticated using the stored key. Finally, we are deleting the key from session if password is changed in .env, so it is effective immediately.

We are done #

Now our telescope dashboard is secured. But what about Horizon? You could do exactly the same for Horizon in HorizonServiceProvider class. However, instead of writing same code again, I would suggest to create a service/support class and use that on both places for the authentication. Again, there might be plenty of room for improvement as there is no one true way to achieve the same goal. If you do find some improvement or a better way please share with the world. 🤝