„MediawikiShibAuthWithPersistentID” változatai közötti eltérés

Innen: KIFÜ Wiki
(ToDo)
2. sor: 2. sor:
  
 
This extension is based on the original [http://www.mediawiki.org/wiki/Extension:Shibboleth_Authentication Extension:Shibboleth Authentication], the basic information will not be copied, here you can find the differences and the explanation of these differences.
 
This extension is based on the original [http://www.mediawiki.org/wiki/Extension:Shibboleth_Authentication Extension:Shibboleth Authentication], the basic information will not be copied, here you can find the differences and the explanation of these differences.
 +
 +
The main object of this development is to make the extension to support opaque persistent-id. Persistent-id could come from the Identity Provider (IdP), where the user has been authenticated as value of persistent nameid, or as value of eduPersonTargetedID attribute. From the view of the mediawiki the route is, how the persistent-id is coming, irrelevant, for the mediawiki it is given by the Service Provider (SP). [https://spaces.internet2.edu/display/SHIB2/NativeSPTargetedID More about persistent-id]
 +
 +
The main point is that persistent-id meets the privacy requirements much better than e.g. the mediawiki gets e-mail address of the user.
 +
 +
== Preparation ==
 +
 +
You have to add an SQL table to be able to pairing persistent-id and the local-id of the user.
 +
 +
<source lang="sql">
 +
 +
CREATE TABLE IF NOT EXISTS `wm_user_persistentid` (
 +
  `userID` int(10) NOT NULL,
 +
  `persistentID` varchar(255) NOT NULL,
 +
  UNIQUE KEY `userID` (`userID`,`persistentID`)
 +
) ENGINE=MyISAM DEFAULT CHARSET=latin1;
 +
 +
</source>
  
 
== LocalSettings.php ==
 
== LocalSettings.php ==
655. sor: 673. sor:
 
</source>
 
</source>
  
== ToDo ==
 
* Testing - I tested only on MediaWiki 1.15.3, it worked properly
 
* Bugfix - If I modify UserLoadFromSession to UserLoadAfterLoadFromSession, the [http://www.mediawiki.org/wiki/Extension_talk:Shibboleth_Authentication#MW_1.15rc1 "reload bug"] is still with us :S
 
* Cleaning the code
 
* Anything else?
 
  
 
==After login==
 
==After login==
688. sor: 701. sor:
  
 
</source>
 
</source>
 +
 +
 +
== ToDo ==
 +
* Testing - I tested only on MediaWiki 1.15.3, it worked properly
 +
* Bugfix - If I modify UserLoadFromSession to UserLoadAfterLoadFromSession, the [http://www.mediawiki.org/wiki/Extension_talk:Shibboleth_Authentication#MW_1.15rc1 "reload bug"] is still with us :S
 +
* Cleaning the code
 +
* Write SQL install script
 +
* Anything else?

A lap 2010. május 3., 11:49-kori változata

Extension:Shibboleth Authentication with persistent-id support

This extension is based on the original Extension:Shibboleth Authentication, the basic information will not be copied, here you can find the differences and the explanation of these differences.

The main object of this development is to make the extension to support opaque persistent-id. Persistent-id could come from the Identity Provider (IdP), where the user has been authenticated as value of persistent nameid, or as value of eduPersonTargetedID attribute. From the view of the mediawiki the route is, how the persistent-id is coming, irrelevant, for the mediawiki it is given by the Service Provider (SP). More about persistent-id

The main point is that persistent-id meets the privacy requirements much better than e.g. the mediawiki gets e-mail address of the user.

Preparation

You have to add an SQL table to be able to pairing persistent-id and the local-id of the user.

CREATE TABLE IF NOT EXISTS `wm_user_persistentid` (
  `userID` int(10) NOT NULL,
  `persistentID` varchar(255) NOT NULL,
  UNIQUE KEY `userID` (`userID`,`persistentID`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

LocalSettings.php

I made only a little change to make easier to configure the modul with different Shibboleth variable names, and set a working logout link.

// Shibboleth Authentication Stuff
// Load ShibAuthPlugin
require_once('extensions/ShibAuthPlugin.php');


// Last portion of the shibboleth WAYF url for lazy sessions.
// This value is found in your shibboleth.xml file on the setup for your SP
// WAYF url will look something like: /Shibboleth.sso/WAYF/$shib_WAYF
//$shib_WAYF = "idp.example.org";
 
//Are you using an old style WAYF (Shib 1.3) or new style Discover Service (Shib 2.x)?
//Values are WAYF or DS, defaults to WAYF
$shib_WAYFStyle = "DS";
 
 
// Is the assertion consumer service located at an https address (highly recommended)
// Default for compatibility with previous version: false
$shib_Https = true;
 
// Prompt for user to login
$shib_LoginHint = "Login via Single Sign-on";
 
// Where is the assertion consumer service located on the website?
// Default: "/Shibboleth.sso"
$shib_AssertionConsumerServiceURL = "/Shibboleth.sso";
 
//Config headernames
$SHIB_PERSISTENTID = 'persistent-id'; // persistent-id (eduPersonTargetedID)
$SHIB_EPPN = 'eppn'; // eduPersonPrincipalName
$SHIB_MAIL = 'mail'; // E-mail
$SHIB_CN = 'cn'; // Common Name

// Map persistent-id
$shib_persistentid = ( isset( $_SERVER[$SHIB_PERSISTENTID] ) ) ? $_SERVER[$SHIB_PERSISTENTID] : null;

// Map username
$shib_UN = ( isset( $_SERVER[$SHIB_EPPN] ) ) ? $_SERVER[$SHIB_EPPN] : null; 

// Map real name
$shib_RN = ( isset( $_SERVER[$SHIB_CN] ) ) ? $_SERVER[$SHIB_CN] : null; 

// Map e-mail
$shib_email = ( isset( $_SERVER[$SHIB_MAIL] ) ) ? $_SERVER[$SHIB_MAIL] : null;

// The ShibUpdateUser hook is executed on login.
// It has two arguments:
// - $existing: True if this is an existing user, false if it is a new user being added
// - &$user: A reference to the user object. 
//           $user->updateUser() is called after the function finishes.
// In the event handler you can change the user object, for instance set the email address or the real name
// The example function shown here should match behavior from previous versions of the extension:
 
$wgHooks['ShibUpdateUser'][] = 'ShibUpdateTheUser';

function ShibUpdateTheUser($existing, &$user) {
	global $shib_email;
	global $shib_RN;
	if (! $existing) {
		if($shib_email != null)
			$user->setEmail($shib_email);
		if($shib_RN != null)
			$user->setRealName($shib_RN);
	}
	return true;
}

// Shibboleth doesn't really support logging out very well.  To take care of
// this we simply get rid of the logout link when a user is logged in through
// Shib.  Alternatively, you can uncomment and set the variable below to a link
// that will either clear the user's cookies or log the user out of the Idp and
// instead of deleting the logout link, the extension will change it instead.
$shib_logout = "/Shibboleth.sso/Logout?return=https://" . $_SERVER['SERVER_NAME'] .
        "/%3Ftitle=Special:Userlogout&returnto=Main_Page";

// Activate Shibboleth Plugin
SetupShibAuth();

ShibAuthPlugin.php

I rewrote the ShibUserLoadFromSession function, extended with a few new function, and comments. :) I did not change ShibAuthPlugin class, and made only a little changes in SetupShibAuth function.

I did not change the version information neither.


<?php

/**
 * Version 1.2.3 (Works out of box with MW 1.13 or above)
 *
 * Authentication Plugin for Shibboleth (http://shibboleth.internet2.edu)
 * Derived from AuthPlugin.php
 * Much of the commenting comes straight from AuthPlugin.php
 *
 * Portions Copyright 2006, 2007 Regents of the University of California.
 * Portions Copyright 2007, 2008 Steven Langenaken
 * Released under the GNU General Public License
 *
 * Documentation at http://www.mediawiki.org/wiki/Extension:Shibboleth_Authentication
 * Project IRC Channel: #sdcolleges on irc.freenode.net
 *
 * Extension Maintainer:
 *	* Steven Langenaken - Added assertion support, more robust https checking, bugfixes for lazy auth, ShibUpdateUser hook
 * Extension Developers:
 *	* D.J. Capelis - Developed initial version of the extension
 */

require_once('AuthPlugin.php');

class ShibAuthPlugin extends AuthPlugin {
    var $existingUser = false;

    /**
     * Check whether there exists a user account with the given name.
     * The name will be normalized to MediaWiki's requirements, so
     * you might need to munge it (for instance, for lowercase initial
     * letters).
     *
     * @param string $username
     * @return bool
     * @access public
     */
    function userExists( $username ) {
        return true;
    }


    /**
     * Check if a username+password pair is a valid login.
     * The name will be normalized to MediaWiki's requirements, so
     * you might need to munge it (for instance, for lowercase initial
     * letters).
     *
     * @param string $username
     * @param string $password
     * @return bool
     * @access public
     */
    function authenticate( $username, $password) {

        global $shib_UN;
        return $username == $shib_UN;

    }

    /**
     * Modify options in the login template.
     *
     * @param UserLoginTemplate $template
     * @access public
     */
    function modifyUITemplate( &$template ) {
        $template->set('useemail', false);
        $template->set('remember', false);
        $template->set('domain', false);
        $template->set( 'usedomain', false );
    }

    /**
     * Set the domain this plugin is supposed to use when authenticating.
     *
     * @param string $domain
     * @access public
     */
    function setDomain( $domain ) {
        $this->domain = $domain;
    }

    /**
     * Check to see if the specific domain is a valid domain.
     *
     * @param string $domain
     * @return bool
     * @access 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
     * @access public
     */
    function updateUser( &$user ) {
        wfRunHooks('ShibUpdateUser', array($this->existingUser, $user));

        //For security, set password to a non-existant hash.
        if ($user->mPassword != "nologin") {
            $user->mPassword = "nologin";
        }

        $user->setOption('rememberpassword', 0);
        $user->saveSettings();
        return true;
    }


    /**
     * Return true if 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.
     *
     * If you don't automatically create accounts, you must still create
     * accounts in some way. It's not possible to authenticate without
     * a local account.
     *
     * This is just a question, and shouldn't perform any actions.
     *
     * @return bool
     * @access public
     */
    function autoCreate() {
        return true;
    }

    /**
     * Can users change their passwords?
     *
     * @return bool
     */
    function allowPasswordChange() {
        global $shib_pretend;

        return $shib_pretend;

    }

    /**
     * Set the given password in the authentication database.
     * Return true if successful.
     *
     * @param string $password
     * @return bool
     * @access public
     */
    function setPassword( $password ) {
        global $shib_pretend;

        return $shib_pretend;
    }

    /**
     * Update user information in the external authentication database.
     * Return true if successful.
     *
     * @param User $user
     * @return bool
     * @access public
     */
    function updateExternalDB( $user ) {
        //Not really, but wiki thinks we did...
        return true;
    }

    /**
     * Check to see if external accounts can be created.
     * Return true if external accounts can be created.
     * @return bool
     * @access public
     */
    function canCreateAccounts() {
        return false;
    }

    /**
     * Add a user to the external authentication database.
     * Return true if successful.
     *
     * @param User $user
     * @param string $password
     * @return bool
     * @access public
     */
    function addUser( $user, $password ) {
        return false;
    }


    /**
     * Return true to prevent logins that don't authenticate here from being
     * checked against the local database's password fields.
     *
     * This is just a question, and shouldn't perform any actions.
     *
     * @return bool
     * @access public
     */
    function strict() {
        return false;
    }

    /**
     * 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.
     *
     * The User object is passed by reference so it can be modified; don't
     * forget the & on your function declaration.
     *
     * @param User $user
     * @access public
     */
    function initUser( &$user, $autocreate ) {
        $this->updateUser($user);
    }

    /**
     * If you want to munge the case of an account name before the final
     * check, now is your chance.
     */
    function getCanonicalName( $username ) {
        return $username;
    }
}

function ShibGetAuthHook() {

    global $wgVersion;
    global $wgUser;

    if ($wgVersion >= "1.13") {
        if ($wgUser) {
            // It does not work properly... The object is that if we have 
            // session from a logged in user, then the mediawiki does not use
            // the database, load only from session
            return 'UserLoadAfterLoadFromSession';
        } else {
            return 'UserLoadFromSession';
        }
    } else {
        return 'AutoAuthenticate';
    }

}

/*
 * End of AuthPlugin Code, beginning of hook code and auth functions
*/

$wgExtensionFunctions[] = 'SetupShibAuth';
$wgExtensionCredits['other'][] = array(
        'name' => 'Shibboleth Authentication',
        'version' => '1.2.3',
        'author' => "Regents of the University of California, Steven Langenaken",
        'url' => "http://www.mediawiki.org/wiki/Extension:Shibboleth_Authentication",
        'description' => "Allows logging in through Shibboleth",
);

function SetupShibAuth() {
    global $shib_UN;
    global $shib_persistentid;

    global $wgHooks;
    global $wgAuth;
    global $wgCookieExpiration;

    if ( $shib_persistentid == null && $shib_UN == null ) {

        $wgHooks['PersonalUrls'][] = 'ShibLinkAdd';

    } else {

        $wgCookieExpiration = -3600;
        $wgHooks[ShibGetAuthHook()][] = "ShibUserLoadFromSession";
        $wgHooks['PersonalUrls'][] = 'ShibActive'; /* Disallow logout link */
        $wgAuth = new ShibAuthPlugin();

    }

}

function ShibGetUserIDFromPersistentID( $persistentID ) {

    $dbr = wfGetDB( DB_SLAVE );
    $res = $dbr->selectRow( "user_persistentid", array( "userID" ), array( "persistentID" => $persistentID ) );
    return ( $res  ) ? $res->userID : false;
}


function ShibAddUserPersistentID( $userID, $persistentID ) {

    $dbr = wfGetDB( DB_MASTER );
    return $dbr->insert( "user_persistentid",  array( "userID" => $userID, "persistentID" => $persistentID ) );

}

function ifUserAlreadyInLocalDB ( $username ) {
    return ( User::idFromName( $username ) != null && User::idFromName( $username ) != 0 ) ? User::idFromName( $username ) : false;
}

function setupUserAlreadyInLocalDB ( $username ) {

    global $wgAuth;

    $user = User::newFromName( ucfirst( $username ) );
    $user->load();
    $wgAuth->existingUser = true;
    $wgAuth->updateUser( $user ); //Make sure password is nologin
    $user->setupSession();
    $user->setCookies();

    return true;

}

function addUserLocalDBwithBlackMagic ( &$user, $username ) {

    global $shib_pretend;
    global $wgRedirectOnLogin;  

    /*
	 * Since we only get called when someone should be logged in, if they
	 * aren't let's make that happen.  Oddly enough the way MW does all
	 * this is simply to use a loginForm class that pretty much does
	 * most of what you need.  Creating a loginform is a very very small
	 * part of this object.
    */
    require_once('specials/SpecialUserlogin.php');

    //This section contains a silly hack for MW
    global $wgLang;
    global $wgContLang;
    global $wgRequest;
    $wgLangUnset = false;

    if(!isset($wgLang)) {
        $wgLang = $wgContLang;
        $wgLangUnset = true;
    }

    ShibKillAA();

    //This creates our form that'll do black magic
    $lf = new LoginForm($wgRequest);

    //Place the hook back (Not strictly necessarily MW Ver >= 1.9)
    //ShibBringBackAA();

    //And now we clean up our hack
    if($wgLangUnset == true) {
        unset($wgLang);
        unset($wgLangUnset);
    }

    //Okay, kick this up a notch then...
    $user->setName( ucfirst( $username ) );

    //The mediawiki developers entirely broke use of this the
    //straightforward way in 1.9, so now we just lie...
    $shib_pretend = true;

    //Now we _do_ the black magic
    $lf->mRemember = false;
    $user->loadDefaults( ucfirst( $username ) );
    $lf->initUser($user, true);

    //Stop pretending now
    $shib_pretend = false;

    //Finish it off
    $user->saveSettings();
    $user->setupSession();
    $user->setCookies();
    $lf->successfulLogin();
    return true;
}

/* Tries to be magical about when to log in users and when not to. */
function ShibUserLoadFromSession($user, &$result = true) {
    global $shib_persistentid;
    global $shib_UN;
    global $wgRedirectOnLogin;


    ShibKillAA();

    //For versions of mediawiki which enjoy calling AutoAuth with null users
    if ($user === null) {
        $user = User::loadFromSession();
    }

    //They already with us?  If so, nix this function, we're good.
    if($user->isLoggedIn()) {

        ShibBringBackAA();
        return true;
    }


    if ( $shib_persistentid != null && $shib_UN == null ) {

        // If the user only has persistentID, and his persistentID has not been in the DB yet,
        // he will be handled as new user, even if he had local id created from username earlier.
        // It is not possible to link the new account persID account with the old one without $shib_UN

        if (  $userID = ShibGetUserIDFromPersistentID( $shib_persistentid )  ) {

            //We know his persistentID and he has a linked local account

            return setupUserAlreadyInLocalDB( User::whoIs( $userID ));

        } else {
            
            // We have to choose a local name for the new user, or develop
            // to be able to modify userCreateTemplate and redirect the user there

            $tempName = "TEMPUserName".substr(date("YssHIs")*rand(1,30)+"10101",0,10);
            $wgRedirectOnLogin = "Special:RenameUser";

            // Add and load the new user
            addUserLocalDBwithBlackMagic( $user, $tempName );

            // Add the persistentID
            ShibAddUserPersistentID( ifUserAlreadyInLocalDB( $tempName ), $shib_persistentid );

            return true;

        }

    } elseif ( $shib_persistentid != null && $shib_UN != null ) {

        if ( ShibGetUserIDFromPersistentID( $shib_persistentid ) == ifUserAlreadyInLocalDB( $shib_UN )  ) {

            // If so, the user has a local account and it has been already
            // linked with his persistentID, so he can log in
            return setupUserAlreadyInLocalDB( $shib_UN );

        } elseif ( ShibAddUserPersistentID( ifUserAlreadyInLocalDB( $shib_UN ), $shib_persistentid ) ) {

            // If we made the link between local account and persistentID
            // then the user can log in

            return setupUserAlreadyInLocalDB( $shib_UN );

        } else {

            // A brand new user
            addUserLocalDBwithBlackMagic( $user, $shib_UN );

            // If we managed to add a new local user, we have to link to the persistentID
            ShibAddUserPersistentID( ifUserAlreadyInLocalDB( $shib_UN ), $shib_persistentid );

            return true;

        }

    } elseif ( $shib_persistentid == null && $shib_UN != null ) {

        //The old way...
        if ( ifUserAlreadyInLocalDB( $shib_UN ) ) {

            return setupUserAlreadyInLocalDB( $shib_UN );

        } else {

            return addUserLocalDBwithBlackMagic ( $user, $shib_UN );

        }

    }

    return false;

}

function ShibKillAA() {
    global $wgHooks;

    //Temporarily kill The AutoAuth Hook to prevent recursion
    foreach ($wgHooks[ShibGetAuthHook()] as $key => $value) {
        if($value == "Shib".ShibGetAuthHook())
            $wgHooks[ShibGetAuthHook()][$key] = 'ShibBringBackAA';
    }
}
/* Puts the auto-auth hook back into the hooks array */
function ShibBringBackAA() {
    global $wgHooks;

    foreach ($wgHooks[ShibGetAuthHook()] as $key => $value) {
        if($value == 'ShibBringBackAA')
            $wgHooks[ShibGetAuthHook()][$key] = "Shib".ShibGetAuthHook();
    }
    return true;
}

/* Add login link */
function ShibLinkAdd(&$personal_urls, $title) {
    global $shib_WAYF, $shib_LoginHint, $shib_Https, $shib_AssertionConsumerServiceURL;
    global $shib_WAYFStyle;
    if (! isset($shib_AssertionConsumerServiceURL) || $shib_AssertionConsumerServiceURL == '')
        $shib_AssertionConsumerServiceURL = "/Shibboleth.sso";
    if (! isset($shib_Https))
        $shib_Https = false;
    if (! isset($shib_WAYFStyle))
        $shib_WAYFStyle = 'DS';
    if ($shib_WAYFStyle == 'DS')
        $shib_ConsumerPrefix = 'DS';
    else
        $shib_ConsumerPrefix = '';
    $pageurl = $title->getLocalUrl();
    if (! isset($shib_LoginHint))
        $shib_LoginHint = "Login via Single Sign-on";

    $personal_urls['SSOlogin'] = array(
            'text' => $shib_LoginHint,
            'href' => ($shib_Https ? 'https' :  'http') .'://' . $_SERVER['HTTP_HOST'] .
                    $shib_AssertionConsumerServiceURL . "/" . $shib_ConsumerPrefix . $shib_WAYF .
                    '?target=' . (isset($_SERVER['HTTPS']) ? 'https' : 'http') .
                    '://' . $_SERVER['HTTP_HOST'] . $pageurl, );
    return true;
}	

/* Kill logout link */
function ShibActive(&$personal_urls, $title) {
    global $shib_logout;
    global $shib_RN;
    global $shib_map_info;

    if($shib_logout == null)
        $personal_urls['logout'] = null;
    else
        $personal_urls['logout']['href'] = $shib_logout;

    if ($shib_RN && $shib_map_info)
        $personal_urls['userpage']['text'] = $shib_RN;

    return true;
}

function ShibAutoAuthenticate(&$user) {
    ShibUserLoadAfterLoadFromSession($user);
}


After login

If the user only has persistent-id, and it is the first time to login, he is given a temporary username, so he will be supposed to change it. To change username mediawiki needs an extension, called RenameUser.

We have to make a small modification on the extension. You can see the patch below, and download from here for the SpecialRenameuser_body.php.

77,78c77,78
< <td class='mw-input'><i>" . $wgUser->mName . "</i>" .
<        Xml::input( 'oldusername', 20, $wgUser->mName, array( 'type' => 'hidden' ) ) . ' ' .
---
> <td class='mw-input'>" .
> 	 Xml::input( 'oldusername', 20, $oun, array( 'type' => 'text', 'tabindex' => '1' ) ) . ' ' .
86c86,94
< 	 Xml::input( 'newusername', 20, $nun, array( 'type' => 'text', 'tabindex' => '1' ) ) .
---
> 	 Xml::input( 'newusername', 20, $nun, array( 'type' => 'text', 'tabindex' => '2' ) ) .
>  "</td>
>  </tr>
>  <tr>
>   <td class='mw-label'>" .
> 	 Xml::label( wfMsg( 'renameuserreason' ), 'reason' ) .
>  "</td>
>  <td class='mw-input'>" .
> 	 Xml::input( 'reason', 40, $reason, array( 'type' => 'text', 'tabindex' => '3', 'maxlength' => 255 ) ) .


ToDo

  • Testing - I tested only on MediaWiki 1.15.3, it worked properly
  • Bugfix - If I modify UserLoadFromSession to UserLoadAfterLoadFromSession, the "reload bug" is still with us :S
  • Cleaning the code
  • Write SQL install script
  • Anything else?