A MediaWiki CAS Setup with SimpleSAMLphp

I want to illustrate how to configure MediaWiki to authenticate with a central auth server using CAS. The CTL wiki has long been overdue for the change to Single Sign-on.

I'm going to focus on how to get things working with CAS, but if you want to use a different identity provider, you can not only use any auth method PluggableAuth supports, but also any of SimpleSAML's modules - many of these are included with SimpleSAML itself and are well-supported.

SimpleSAMLphp is an amazing project that's been around for a long time, is very stable, and has a large user community with lots of support and documentation. It's one of those projects that's so flexible that it takes some time to learn how to use it, what it can do, and how it works.

SimpleSAML configuration

I'm using nginx here, and you'll need to add a nested location clause in your wiki's server configuration, to make the /simplesaml/ path accessible. The goal here is to be able to access the endpoint at https://yourwiki.edu/simplesaml/ in your browser - SimpleSAML provides a web app interface here that will help you configure it. Additionally, this is required to be in place as a basic piece of how the authentication process works with SimpleSAML. After verifying a user's identity with the identity provider (a remote CAS server), the browser is redirected to the SimpleSAML application here, which then passes control over to your wiki via the mediawiki extension.

In /etc/nginx/sites-available/wiki.conf:

server {
    # ...

    location ^~ /simplesaml {
        alias /usr/share/simplesamlphp/www;

        location ~ \.php(/|$) {
            include fastcgi_params;
            fastcgi_pass unix:/run/php/php7.4-fpm.sock;
            fastcgi_param SCRIPT_FILENAME $request_filename;
            fastcgi_split_path_info ^(.+?\.php)(/.*)$;
            fastcgi_param PATH_INFO $fastcgi_path_info;
            fastcgi_index index.php;
        }
    }
    # ...
}
                

Now the actual SimpleSAML configuration, in /etc/simplesamlphp/config.php and /etc/simplesamlphp/authsources.php. Notes for config.php:

Now you'll configure the connection to your CAS server.

authsources.php:

'my-cas' => [
    'cas:CAS',
    'cas' => [
        'login' => 'https://mycas.example.edu/cas/login',
        'serviceValidate' => 'https://mycas.example.edu/cas/p3/serviceValidate',
        'logout' => 'https://mycas.example.edu/cas/logout',
        'name' => [
            'en' => 'My Wiki',
        ],
        'attributes' => [
            'username' => '/cas:serviceResponse/cas:authenticationSuccess/cas:user',
            'givenName' => '/cas:serviceResponse/cas:authenticationSuccess/cas:attributes/cas:givenName',
            'lastName' => '/cas:serviceResponse/cas:authenticationSuccess/cas:attributes/cas:lastName',
            'mail' => '/cas:serviceResponse/cas:authenticationSuccess/cas:attributes/cas:mail',
        ],
    ],
    // I'm not using ldap - CAS v3 should contain
    // all the info I need
    'ldap' => [],
],
                

Sometimes it can help to see all the CAS data available to you when configuring something like this. To do this, you can debug the SimpleSAML's CAS module and print out the results from the CAS server. You can add a few lines to the casServiceValidate() function, after $result is fetched from the identity provider:

ob_start();
var_dump($result):
error_log(ob_get_clean(), 3, '/tmp/cas.log');
                

Then tail cas.log while making an auth attempt. Configure your attributes in authsources.php as needed.

MediaWiki configuration

Download PluggableAuth and SimpleSAMLphp, and unzip those into your extensions directory. You'll also want to install SimpleSAMLphp - either through your operating system or from their site.

You'll need to specify in MediaWiki's LocalSettings file where it's installed - usually it's something like /usr/share/simplesamlphp or /var/simplesamlphp.

$wgSimpleSAMLphp_InstallDir = '/usr/share/simplesamlphp';
$wgSimpleSAMLphp_AuthSourceId = 'my-cas';
$wgSimpleSAMLphp_UsernameAttribute = 'username';
$wgSimpleSAMLphp_RealNameAttribute = ['givenName', 'lastName'];
$wgSimpleSAMLphp_EmailAttribute = 'mail';
$wgPluggableAuth_ButtonLabel = 'Log in with CAS';

wfLoadExtension( 'PluggableAuth' );
wfLoadExtension( 'SimpleSAMLphp' );
                

Note that the exact syntax of these config options have changed with PluggableAuth 6.0, but you can sort out those differences if that's what you're using. I'm using 5.7 because I'm on the LTS release of MediaWiki (1.35.x).

With all these steps in place, the auth process should basically be working. But then, what happens on a successful auth? Is a user created in MediaWiki? The simplest path is just to use the same username you got from the CAS server, and set up MediaWiki to allow accounts to be created automatically via the autocreateaccount permission. In fact, PluggableAuth's installation steps state: "The createaccount or autocreateaccount user rights must be granted to all users." That's not necessarily true, and I'll cover that in the next section.

User association

Auto-creation of accounts is certainly one path forward. In my case, we needed a more nuanced approach: we have 40 or so wiki users with no real username standard, though it's usually either firstname, or first initial + last name. These users should all have emails associated with them as well. I'll use that to associate the user with the email we get from CAS. In addition, we don't want users to be able to create accounts themselves just by authenticating through CAS. We have a private wiki and opt for a more manual approach - We create users ourselves when needed.

You can override the SimpleSAML username using its MandatoryUserInfoProvider functionality. In fact, you can even do some queries to the user database during this step if CAS does provide some info that can be helpful in finding your wiki user. In our case, the email from CAS will likely match the wiki user's email. Still, that's not always the case, so it's only provided as a fallback method. My username mapper first checks a hardcoded list of cas users and wiki users that we know about.

$usernameMap = [
    'casUsername1' => 'wikiUser1',
    'casUsername2' => 'wikiUser2',
    // etc
];

// Associate a CAS user with a wiki user
$wgSimpleSAMLphp_MandatoryUserInfoProviderFactories['username'] = function($config) {
    return new \MediaWiki\Extension\SimpleSAMLphp\UserInfoProvider\GenericCallback(
        function($attributes) {
            $uni = $attributes['username'][0];

            // If the uni is in our hardcoded array, just use
            // that username
            if (array_key_exists($uni, $usernameMap)) {
                return $usernameMap[$uni];
            }

            if (!isset($attributes['mail'])) {
                throw new Exception('missing email address');
            }

            $dbr = wfGetDB(DB_REPLICA);
            $id = $dbr->selectField(
                'user',
                'user_id', [
                    'user_email' => [
                        // Look up a user via CAS mail OR
                        // uni@example.edu
                        $attributes['mail'][0],
                        $uni . '@example.edu'
                    ]
                ]);

            if ($id) {
                $user = User::newFromId($id);
            } else {
                // If we didn't find a user,
                // just return the uni
                return $uni;
            }

            if ($user) {
                // If there's a MediaWiki user, return
                // its username
                $username = $user->getName();
            } else {
                $username = $uni;
            }

            return $username;
        }
    );
};