Using LDAP authentication with MediaWiki and IIS

15 Jan

I wanted MediaWiki to use Windows Authentication automatically when users visited the Wiki.

This wasn’t as straight forward as I had hoped but I finally managed to get it working, this is roughly how I went about it:

  • Install PHP
  • Install LDAP Module
  • Install Mediawiki
  • Install IIS URL Rewrite Module
  • Force HTTPS in IIS by adding this to the web.config:
<rule name="Force HTTPS" enabled="true">
<match url="(.*)" ignoreCase="false" />
<conditions>
<add input="{HTTPS}" pattern="off" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" appendQueryString="true" redirectType="Permanent" />
</rule>
  • Install auth_remoteuser.php:
.
//
// Copyright 2006 Otheus Shelling
// Copyright 2007 Rusty Burchfield
// Copyright 2009 James Kinsman
// Copyright 2010 Daniel Thomas
// Copyright 2010 Ian Ward Comfort
//
// In 2009, the copyright holders determined that the original publishing of this code
// under GPLv3 was legally and logistically in error, and re-licensed it under GPLv2.
//
// See http://www.mediawiki.org/wiki/Extension:AutomaticREMOTE_USER
//
// Adapted by Rusty to be compatible with version 1.9 of MediaWiki
// Optional settings from Emmanuel Dreyfus
// Adapted by VibroAxe (James Kinsman) to be compatible with version 1.16 of MediaWiki
// Adapted by VibroAxe (James Kinsman) to allow domain substitution for Integrated Windows Authentication
// Adapted by drt24 (Daniel Thomas) to add the optional $wgAuthRemoteuserMailDomain and remove hardcoding
//   of permissions for anonymous users.
// Adapted by Ian Ward Comfort to detect mismatches between the session user and REMOTE_USER
//
// Add these lines to your LocalSettings.php
//
// // Don't let anonymous people do things...
// $wgGroupPermissions['*']['createaccount']   = false;
// $wgGroupPermissions['*']['read']            = false;
// $wgGroupPermissions['*']['edit']            = false;
//
// /* This is required for Auth_remoteuser operation
// require_once('extensions/Auth_remoteuser.php');
// $wgAuth = new Auth_remoteuser();
//
// The constructor of Auth_remoteuser registers a hook to do the automatic
// login.  Storing the Auth_remoteuser object in $wgAuth tells mediawiki to use
// that object as the AuthPlugin.  This way the login attempts by the hook will
// be handled by us.
//
// You probably want to edit the initUser function to set the users real name
// and email address properly for your configuration.

// Extension credits that show up on Special:Version
$wgExtensionCredits['other'][] = array(
                                'name' => 'AutomaticREMOTE USER',
                                'version' => '1.1.4',
                                'author' => array( 'Otheus Shelling', 'Rusty Burchfield', 'James Kinsman', 'Daniel Thomas', 'Ian Ward Comfort' ),
                                'url' => 'https://www.mediawiki.org/wiki/Extension:AutomaticREMOTE_USER',
                                'description' => 'Automatically logs users using the REMOTE_USER environment variable.',
);

// We must allow zero length passwords. This extension does not work in MW 1.16 without this.
$wgMinimalPasswordLength = 0;

$wgAuthRemoteuserAuthz = true;
$wgAuthRemoteuserDomain = null;

/* User's name */
$wgAuthRemoteuserName = isset( $_SERVER["AUTHENTICATE_CN"] )
                ? $_SERVER["AUTHENTICATE_CN"]
                : '';

/* User's Mail */
$wgAuthRemoteuserMail = isset( $_SERVER["AUTHENTICATE_MAIL"] )
                ? $_SERVER["AUTHENTICATE_MAIL"]
                : '';
$wgAuthRemoteuserNotify = false; /* Do not send mail notifications */
$wgAuthRemoteuserDomain = "PUT_DOMAIN_NAME_HERE"; /* Remove NETBIOSDOMAIN\ from the beginning or @NETBIOSDOMAIN at the end of a IWA username */
/* User's mail domain to append to the user name to make their email address */
$wgAuthRemoteuserMailDomain = "example.com";

$wgExtensionFunctions[] = 'Auth_remote_user_hook';

/**
* This hook is registered by the Auth_remoteuser constructor.  It will be
* called on every page load.  It serves the function of automatically logging
* in the user.  The Auth_remoteuser class is an AuthPlugin and handles the
* actual authentication, user creation, etc.
*
* Details:
* 1. Check to see if the user has a session and is not anonymous.  If this is
*    true, check whether REMOTE_USER matches the session user.  If so, we can
*    just return; otherwise we must logout the session user and login as the
*    REMOTE_USER.
* 2. If the user doesn't have a session, we create a login form with our own
*    fake request and ask the form to authenticate the user.  If the user does
*    not exist authenticateUserData will attempt to create one.  The login form
*    uses our Auth_remoteuser class as an AuthPlugin.
*
* Note: If cookies are disabled, an infinite loop /might/ occur?
*/
function Auth_remote_user_hook() {
                global $wgUser, $wgRequest, $wgAuthRemoteuserDomain, $wgAuth;

                // For a few special pages, don't do anything.
                $title = $wgRequest->getVal( 'title' );
                if ( ( $title == Title::makeName( NS_SPECIAL, 'UserLogout' ) ) ||
                                ( $title == Title::makeName( NS_SPECIAL, 'UserLogin' ) ) ) {
                                return;
                }

                // Process the username if required
                if ( !isset( $_SERVER['REMOTE_USER'] ) ) {
                                return;
                }
                if ( isset( $wgAuthRemoteuserDomain ) && strlen( $wgAuthRemoteuserDomain ) ) {
                                $username = str_replace( "$wgAuthRemoteuserDomain\\", "", $_SERVER['REMOTE_USER'] );
                                $username = str_replace( "@$wgAuthRemoteuserDomain", "", $username );
                } else {
                                $username = $_SERVER['REMOTE_USER'];
                }

                // Check for valid session
                $user = User::newFromSession();
                if ( !$user->isAnon() ) {
                                if ( $user->getName() == $wgAuth->getCanonicalName( $username ) ) {
                                                return;            // Correct user is already logged in.
                                } else {
                                                $user->doLogout(); // Logout mismatched user.
                                }
                }

                // Copied from includes/SpecialUserlogin.php
                if ( !isset( $wgCommandLineMode ) && !isset( $_COOKIE[session_name()] ) ) {
                                wfSetupSession();
                }

                // If the login form returns NEED_TOKEN try once more with the right token
                $trycount = 0;
                $token = '';
                $errormessage = '';
                do {
                                $tryagain = false;
                                // Submit a fake login form to authenticate the user.
                                $params = new FauxRequest( array(
                                                'wpName' => $username,
                                                'wpPassword' => '',
                                                'wpDomain' => '',
                                                'wpLoginToken' => $token,
                                                'wpRemember' => ''
                                                ) );

                                // Authenticate user data will automatically create new users.
                                $loginForm = new LoginForm( $params );
                                $result = $loginForm->authenticateUserData();
                                switch ( $result ) {
                                                case LoginForm :: SUCCESS :
                                                                $wgUser->setOption( 'rememberpassword', 1 );
                                                                $wgUser->setCookies();
                                                                break;
                                                case LoginForm :: NEED_TOKEN:
                                                                $token = $loginForm->getLoginToken();
                                                                $tryagain = ( $trycount == 0 );
                                                                break;
                                                case LoginForm :: WRONG_TOKEN:
                                                                $errormessage = 'WrongToken';
                                                                break;
                                                case LoginForm :: NO_NAME :
                                                                $errormessage = 'NoName';
                                                                break;
                                                case LoginForm :: ILLEGAL :
                                                                $errormessage = 'Illegal';
                                                                break;
                                                case LoginForm :: WRONG_PLUGIN_PASS :
                                                                $errormessage = 'WrongPluginPass';
                                                                break;
                                                case LoginForm :: NOT_EXISTS :
                                                                $errormessage = 'NotExists';
                                                                break;
                                                case LoginForm :: WRONG_PASS :
                                                                $errormessage = 'WrongPass';
                                                                break;
                                                case LoginForm :: EMPTY_PASS :
                                                                $errormessage = 'EmptyPass';
                                                                break;
                                                default:
                                                                $errormessage = 'Unknown';
                                                                break;
                                }

                                if ( $result != LoginForm::SUCCESS && $result != LoginForm::NEED_TOKEN ) {
                                                error_log( 'Unexpected REMOTE_USER authentication failure. Login Error was:' . $errormessage );
                                }
                                $trycount++;
                } while ( $tryagain );

                return;
}

class Auth_remoteuser extends AuthPlugin {
                /**
                * Disallow password change.
                *
                * @return bool
                */
                function allowPasswordChange() {
                                return false;
                }

                /**
                * This should not be called because we do not allow password change.  Always
                * fail by returning false.
                *
                * @param $user User object.
                * @param $password String: password.
                * @return bool
                * @public
                */
                function setPassword( $user, $password ) {
                                return false;
                }

                /**
                * We don't support this but we have to return true for preferences to save.
                *
                * @param $user User object.
                * @return bool
                * @public
                */
                function updateExternalDB( $user ) {
                                return true;
                }

                /**
                * We can't create external accounts so return false.
                *
                * @return bool
                * @public
                */
                function canCreateAccounts() {
                                return false;
                }

                /**
                * We don't support adding users to whatever service provides REMOTE_USER, so
                * fail by always returning false.
                *
                * @param User $user
                * @param string $password
                * @return bool
                * @public
                */
                function addUser( $user, $password ) {
                                return false;
                }

                /**
                * Pretend all users exist.  This is checked by authenticateUserData to
                * determine if a user exists in our 'db'.  By returning true we tell it that
                * it can create a local wiki user automatically.
                *
                * @param $username String: username.
                * @return bool
                * @public
                */
                function userExists( $username ) {
                                return true;
                }

                /**
                * Check whether the given name matches REMOTE_USER.
                * The name will be normalized to MediaWiki's requirements, so
                * lower it and the REMOTE_USER before checking.
                *
                * @param $username String: username.
                * @param $password String: user password.
                * @return bool
                * @public
                */
                function authenticate( $username, $password ) {
                                global $wgAuthRemoteuserAuthz, $wgAuthRemoteuserDomain;

                                if ( isset( $wgAuthRemoteuserAuthz ) && !$wgAuthRemoteuserAuthz ) {
                                                return false;
                                }

                                if ( !isset( $_SERVER['REMOTE_USER'] ) ) {
                                                $_SERVER['REMOTE_USER'] = "";
                                }

                                if ( isset( $wgAuthRemoteuserDomain ) && strlen( $wgAuthRemoteuserDomain ) > 0 ) {
                                                $usertest = str_replace( "$wgAuthRemoteuserDomain\\", "", $_SERVER['REMOTE_USER'] );
                                                $usertest = str_replace( "@$wgAuthRemoteuserDomain", "", $usertest );
                                } else {
                                                $usertest = $_SERVER['REMOTE_USER'];
                                }

                                return ( strtolower( $username ) == strtolower( $usertest ) );

                }

                /**
                * Check to see if the specific domain is a valid domain.
                *
                * @param $domain String: authentication domain.
                * @return bool
                * @public
                */
                function validDomain( $domain ) {
                                return true;
                }

                /**
                * When a user logs in, optionally fill in preferences and such.
                * For instance, you might pull the email address or real name from the
                * external user database.
                *
                * The User object is passed by reference so it can be modified; don't
                * forget the & on your function declaration.
                *
                * @param User $user
                * @public
                */
                function updateUser( &$user ) {
                                // We only set this stuff when accounts are created.
                                return true;
                }

                /**
                * Return true because the wiki should create a new local account
                * automatically when asked to login a user who doesn't exist locally but
                * does in the external auth database.
                *
                * @return bool
                * @public
                */
                function autoCreate() {
                                return true;
                }

                /**
                * Return true to prevent logins that don't authenticate here from being
                * checked against the local database's password fields.
                *
                * @return bool
                * @public
                */
                function strict() {
                                return true;
                }

                /**
                * When creating a user account, optionally fill in preferences and such.
                * For instance, you might pull the email address or real name from the
                * external user database.
                *
                * @param $user User object.
                * @public
                */
                function initUser( &$user ) {
                                global $wgAuthRemoteuserName, $wgAuthRemoteuserMail, $wgAuthRemoteuserMailDomain,
                                                $wgAuthRemoteuserNotify, $wgAuthRemoteuserDomain;

                                if ( isset( $wgAuthRemoteuserDomain ) && strlen( $wgAuthRemoteuserDomain ) ) {

                                                $username = str_replace( "$wgAuthRemoteuserDomain\\", "", $_SERVER['REMOTE_USER'] );
                                                $username = str_replace( "@$wgAuthRemoteuserDomain", "", $username );
                                } else {
                                                $username = $_SERVER['REMOTE_USER'];
                                }

                                if ( isset( $wgAuthRemoteuserName ) ) {
                                                $user->setRealName( $wgAuthRemoteuserName );
                                } else {
                                                $user->setRealName( '' );
                                }

                                if ( isset( $wgAuthRemoteuserMail ) ) {
                                                $user->setEmail( $wgAuthRemoteuserMail );
                                } elseif ( isset( $wgAuthRemoteuserMailDomain ) ) {
                                                $user->setEmail( $username . '@' . $wgAuthRemoteuserMailDomain );
                                } else {
                                                $user->setEmail( $username . "@example.com" );
                                }

                                $user->mEmailAuthenticated = wfTimestampNow();
                                $user->setToken();

                                // turn on e-mail notifications
                                if ( isset( $wgAuthRemoteuserNotify ) && $wgAuthRemoteuserNotify ) {
                                                $user->setOption( 'enotifwatchlistpages', 1 );
                                                $user->setOption( 'enotifusertalkpages', 1 );
                                                $user->setOption( 'enotifminoredits', 1 );
                                                $user->setOption( 'enotifrevealaddr', 1 );
                                }

                                $user->saveSettings();
                }

                /**
                * Modify options in the login template.  This shouldn't be very important
                * because no one should really be bothering with the login page.
                *
                * @param $template UserLoginTemplate object.
                * @public
                */
                function modifyUITemplate( &$template ) {
                                // disable the mail new password box
                                $template->set( 'useemail', false );
                                // disable 'remember me' box
                                $template->set( 'remember', false );
                                $template->set( 'create', false );
                                $template->set( 'domain', false );
                                $template->set( 'usedomain', false );
                }

                /**
                * Normalize user names to the MediaWiki standard to prevent duplicate
                * accounts.
                *
                * @param $username String: username.
                * @return string
                * @public
                */
                function getCanonicalName( $username ) {
                                // lowercase the username
                                $username = strtolower( $username );

                                // uppercase first letter to make MediaWiki happy
                                return strtoupper ( $username );
                }
}

* Be sure to edit the entry “PUT_DOMAIN_NAME_HERE” above.

Edit local_settings.ini

##LDAP Authentication Plugin
require_once('extensions/Auth_remoteuser/Auth_remoteuser.php');
$wgAuth = new Auth_remoteuser();

# SECURITY section added for SSO
$wgGroupPermissions['*']['createaccount'] = false;
$wgGroupPermissions['*']['read'] = false;
$wgGroupPermissions['*']['edit'] = false;
# Pages anonymous (not-logged-in) users may see
$wgWhitelistRead = array( "Main Page", "Special:Userlogin", "-", "MediaWiki:Monobook.css" );
# Remote_User Auth with MS AD LDAP enhancements
//require_once(’extensions/Auth_remoteuser_iis.php’);
//$wgAuth = new Auth_remoteuser();

$wgLDAPServerNames = array( "mydomainnamehere"=>"mydomaincontroller1.mydomainnamehere.local, mydomaincontroller2.mydomainnamehere.local");
$wgLDAPSearchStrings = array( "mydomainnamehere"=>"USER-NAME" );
$wgLDAPBaseDNs = array( "mydomainnamehere"=>"DC=mydomaincontroller1.mydomainnamehere.local, DC=mydomainnamehere.local" );

* Again make sure you enter you own domain settings at the end of that.

  • Then, using group policy, you can make sure the site is in the INTRANET ZONE (if its on a different subnet to your machine)

Site to zone template:

    1. intranet
    2. trusted
    3. internet
    4. restricted
  • I also removed the logout button (as users are automatically logged in with their AD credentials) with this MediaWiki code:
//remove logout button
function StripLogout(&$personal_urls, &$wgTitle) { 
        unset( $personal_urls["logout"]);
       return true;
}

$wgHooks['PersonalUrls'][] = 'StripLogout';
  •  In IIS I set it to use only ‘Windows Authentication:

  • Also set the providers to NTLM and Negotiate:

I installed PHPMyAdmin to edit the MySQL databases. I then logged in with my own AD credentials to MediaWiki and then edited the ug_user id for a sysop user to be that of the user_id of my AD account:

The end.