Authenticating Redash with Joomla using Mod_Authnz_External

We often use Joomla with custom components to host a couple of internal applications. These application usually generate heaps of data that could be used for analytics. We looked into writing a custom reporting extension, but due to time constrains, as well as the desire to minimize custom code led us to discover and implement Redash. Redash connects to any data source, including MySQL, and can generate reports and visualizations.

Because all our users are handled with Joomla authentication, we wanted to create a simple single sign-on for both applications. LDAP was an option as both support it, but this was decided against for various reason. This left us with authenticating Redash with the Joomla authentication.

By placing a reverse proxy in front of Redash, we are able to query Joomla for a valid Joomla login and only forward on requests that are authenticated.

Overview

redash

Redash Configuration

Redash supports header based authentication. By enabling the remote user setting in the Redash configuration (usually /opt/redash/.env), Redash will create or login a user with the value provided in the header. After adding these variables, supervisorctl will need restarted (supervisorctl restart all).

# Joomla Login
REDASH_REMOTE_USER_LOGIN_ENABLED="true"
REDASH_REMOTE_USER_HEADER="X-Forwarded-Remote-User"

Adding this header is a security issue as any end user could add it as run requests directly. To mitigate this, we restrict access to Redash to the IP address of our proxy server. Here is a sample nginx redash configuration (/etc/nginx/sites-available/redash) with the IP restrictions, then restart nginx (service restart nginx).

upstream rd_servers {
  server 127.0.0.1:5000;
}

server {

  server_tokens off;

  listen 80 default;
  listen 443 ssl;

  server_name redash.domain.org;
  ssl_certificate /etc/nginx/ssl/domain.crt;
  ssl_certificate_key /etc/nginx/ssl/domain.key;

  access_log /var/log/nginx/rd.access.log;

  gzip on;
  gzip_types *;
  gzip_proxied any;

  location / {
    allow 10.0.0.2/32; # allow from Apache proxy ip
    deny all;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass       http://rd_servers;
  }
}

Joomla Configuration

To inerface the Apache reverse proxy with Joomla, we need an executable CLI. Upload the following as modauthnzexternal.php (or get the most recent version at my Github) to the /cli folder in Joomla and make it executable (chmod +x modauthnzexternal.php). The reverse proxy has to be on the same server as the Joomla installation. The user it runs under need permissions to run modauthnzexternal.php.

#!/usr/bin/env php
<?php
/**
 * This is a CRON script which should be called from the command-line,
 * not the web. For example something like:
 * env php /path/to/joomla/cli/app.php
 */

// Make sure we're being called from the command line, not a web interface
if (PHP_SAPI !== 'cli') die('This is a command line only application.');

// Set flag that this is a valid Joomla entry point
define('_JEXEC', 1);

// Configure error reporting to maximum for CLI output.
error_reporting(E_ALL ^ E_NOTICE ^ E_WARNING);
ini_set('display_errors', 1);

// Load system defines
if (!defined('_JDEFINES')) {
	define('JPATH_BASE', dirname(dirname(__FILE__)));
	require_once JPATH_BASE . '/includes/defines.php';
}
require_once JPATH_BASE . '/includes/framework.php';

// Fool Joomla into thinking we're in the administrator with com_lawnvoice as active component
$app = JFactory::getApplication('site');
$_SERVER['HTTP_HOST'] = 'domain.com';
$_SERVER['REQUEST_METHOD'] = 'GET';

class CliModAuthnzExternal extends JApplicationCli {

	public function doExecute() {
								
		$this->addLogger();

		JLog::add('### Starting Auth Request ###', JLog::INFO, 'ModAuthnzExternal');

        $app = JFactory::getApplication();
		
		$stdin = fopen('php://stdin', 'r');
		stream_set_blocking($stdin, false);

        // Get the log in credentials.
		$credentials = [];
		$credentials['username']  = trim(fgets($stdin));
		$credentials['password']  = trim(fgets($stdin));
		$credentials['secretkey'] = null;
        
        $options = [];
		$options['remember'] = 0;
		$options['return']   = '';
		
		// foreach ($_ENV as $k => $v) {
		// 	JLog::add($k.'='.$v, JLog::INFO, 'ModAuthnzExternal');
		// }

		// Accept the login if the user name matchs the password
		if (true !== $app->login($credentials, $options)) {
			$msg = 'Login Failed for'.$credentials['username'];
			JLog::add($msg, JLog::NOTICE, 'ModAuthnzExternal');
			fwrite(STDERR, "$msg\n");
			exit(1);
		} else {
			$msg = 'Login Success for '.$credentials['username'];
			JLog::add($msg, JLog::NOTICE, 'ModAuthnzExternal');
			// fwrite(STDERR, "$msg\n");
			$user = JFactory::getUser();
			JLog::add(print_r($user, true), JLog::INFO, 'ModAuthnzExternal');
			// putenv('JOOMLA_ID', $user->id);
			// apache_setenv('JOOMLA_ID', $user->id);
			// fwrite(STDOUT, "JOOMLA_ID {$user->id}");
			// putenv('JOOMLA_USERNAME', $user->username);
			// apache_setenv('JOOMLA_USERNAME', $user->username);
			// fwrite(STDOUT, "JOOMLA_USERNAME {$user->username}");
			// putenv('JOOMLA_NAME', $user->name);
			// apache_setenv('JOOMLA_NAME', $user->name);
			// fwrite(STDOUT, "JOOMLA_NAME {$user->name}");
			// putenv('JOOMLA_EMAIL', $user->email);
			// apache_setenv('JOOMLA_EMAIL', $user->email);
			// fwrite(STDOUT, "JOOMLA_EMAIL {$user->email}");
			exit(0);
		}
	}

	protected function addLogger() {
		JLog::addLogger(
			array(
				 // Sets file name
				 'text_file' => 'ModAuthnzExternal.log.php'
			),
			// Sets messages of all log levels to be sent to the file
			JLog::ALL,
			// The log category/categories which should be recorded in this file
			// In this case, it's just the one category from our extension, still
			// we need to put it inside an array
			['ModAuthnzExternal']
		);
	}
}

JApplicationCli::getInstance('CliModAuthnzExternal')->execute();

Server Configuration

Install mod_authnz_external (apt install libapache2-mod-authnz-external) and enable it (authnz_external). You will also need proxy, proxy_http, and possibly headers. These came with Apache on Ubuntu 16.04, just had to enable them (a2enmod proxya2enmod proxy_httpa2enmod headers). Restart Apache (systemctl restart apache2).

Apache Reverse Proxy Configuration

Create a new Apache VirtualHost at the domain you want to access Redash. Add the following configuration options inside <Virtualhost /> (in additions to the defaults). I am using the proxy and Redash with https, you may have to modify to use http. Restart Apache.

AddExternalAuth joomla /path/to/joomla/cli/modauthnzexternal.php
SetExternalAuthMethod joomla pipe
ProxyPass / https://redash.sitename.tld/
ProxyPassReverse / https://redash.sitename.tld/
SSLProxyEngine on
<Proxy *>
allow from all
AuthType Basic
AuthName "Joomla"
AuthBasicProvider external
AuthExternal joomla
Require valid-user
# only use one of these e for http, s for https
# RequestHeader add X-Forwarded-Remote-User %{REMOTE_USER}e
RequestHeader add X-Forwarded-Remote-User %{REMOTE_USER}s
</Proxy>

Possible Enhancements

Right now, any valid Joomla user is authenticated. This is not a problem for us, but it would be on most installations. 

Other Notes

This was tested with Joomla 3.8.5, Apache 2.4.18, and Ubuntu 16.04.

 

Contact Us To Setup A Meeting

Feel free to call or email anytime to setup a meeting. We would love to discuss your project to see if we can help!

contact us