Manual talk:Hooks/UserLoadFromSession

From mediawiki.org
Latest comment: 15 years ago by Andthepharaohs in topic Sample Code

Sample Code[edit]

Paul Lustgarten 16:10 2 April 2009 (UTC) Found this sample code very helpful - thanks! Posted some notes on it below, in Sample Code Notes.

And 15:01, 3 February 2009 (UTC) WARNING - there's a problem with user admin rights using this code - bug 17339 submitted and this page will be updated when it's fixed.Reply

And 16:21, 21 January 2009 (UTC) I had some problems getting this to work and would have appreciated a sample, so now that I've done it, here's a bowdlerised version of my code.Reply

<?php 
function fnUserAuth($user, &$result) {

/*  This is a slightly modified version of the code that I have implemented.
		I had a particular problem with group membership, in that this code was based on 
		a modified version of User.php for MW 1.8.2 and it took a while to find out the problem,
		which was that you need to call saveToCache() after loadFromDatabase() as the group
		credentials are emptied in the latter. 

		There's code here that you won't need or will need amending, but I hope that it will 
		serve as a useful example.
		
 */
 
/*	This function performs the user authentication using our SSO.
		It requires the following fields to be added to the default {prefix}_user table in the database:
		
		user_employee_id	tinytext
		user_market_group	tinytext		

		Ref:	http://www.mediawiki.org/wiki/Manual:Hooks/UserLoadFromSession
					http://svn.wikimedia.org/doc/classUser.html
		
 */
 
$fname   = "UserAuthSample::fnUserAuth";
$SSO    = "https://xxxxxxxxxxxx.com/login/sso/SSOService?app=wiki&returnURL=";
$errpage = "http://wiki.xxxxxxx.com/wikiaccess.php";
$allowed = 300; # Five minutes diff is allowed.
	
	logFrodo("Page: " . $_SERVER['REQUEST_URI']);
	if (isset($_COOKIE['FrodoWiki'])) {
		$Frodocookie = $_COOKIE['FrodoWiki'];
		logFrodo("FrodoWiki cookie found");

		if (isset($_REQUEST['digest'])) {
			logFrodo("digest exists as well - ignoring it.");
		}

		loadFromDatabaseFrodo($user, $Frodocookie);
		$result = 1; // This causes the rest of the authentication process to be skipped.
		return(false);   // As should this, according to the internal error report:
		// Detected bug in an extension! Hook fnUserAuthFrodo failed to return a value; should return true to 
                // continue hook processing or false to abort.

	} elseif (isset($_REQUEST['digest'])) {
		# Just back from SSO - validate the result.
		
		$digest      = $_REQUEST['digest'];
		$empid       = $_REQUEST['uid'];
		$email       = $_REQUEST['email'];
		$firstname   = $_REQUEST['firstname'];
		$lastname    = $_REQUEST['lastname'];
		$marketgroup = $_REQUEST['marketgroup'];
		$intime      = $_REQUEST['time'];
		$returnurl   = $_REQUEST['returnURL'];
		$thispage    = curPageURL();
		$mytime      = date("Y:m:d:H:i:s", time());

		// Get the secret ...
		$file = fopen("/usr/local/apache2/htdocs/wiki/Frodo/conf/.SSOkey", "r") or exit("Unable to open file!");
		$secret = chop(fgets($file),"\n"); # Drop trailing LF
		fclose($file);

		$md5string = "$empid$intime$secret";
		$mydigest = md5($md5string);
		if ( "$digest" != "$mydigest" ) {
			// Problem! Log secure info to file:
			logFrodo("Digests don't match for $email - unable to proceed.");
			logFrodo("  digest=$digest");
			logFrodo("  mydigest=$mydigest");
			logFrodo("  md5string=>$md5string<");			
			// ... and tell the user something interesting:
			$errpage .= "?error=Digest%20mismatch";
			$errpage .= "&digest=$digest&empid=$empid&email=$email&first=$firstname&last=$lastname&mg= \
                                     $marketgroup&intime=$intime";
			$errpage .= "&mytime=$mytime&returnurl=$returnurl";
			// $errpage .= "&timediff=&timediff";
			// redirect to report error ...
			http_redirect($errpage, array(), true, HTTP_REDIRECT);
		}

		$diff = valtime($intime);
		if ( $diff > $allowed ) {
			logFrodo("Time difference ($diff) is too great for $email");
			logFrodo("  intime=$intime");
			logFrodo("  mytime=$mytime");
			// ... and tell the user something interesting:
			$errpage .= "?error=Time%20difference%20too%20great.";
			$errpage .= "&digest=$digest&empid=$empid&email=$email&first=$firstname&last=$lastname& \
                                    mg=$marketgroup&intime=$intime";
			$errpage .= "&mytime=$mytime&returnurl=$returnurl";
			$errpage .= "&timediff=&diff";
			// redirect to report error ...
			http_redirect($errpage, array(), true, HTTP_REDIRECT);
			// $result not set so that authentication continues - and should fail.
			return(true); // This should have the same effect.
		}
		
		logFrodo("SSO validation successful for $email");
		
		# Validation successful - create cookie with all SSO fields.
		$Frodocookie = "$empid|$email|$firstname|$lastname|$marketgroup"; 
		$Frodocookie = str_replace(' ', '%20', $Frodocookie); 
		# We'll start with a week ...
		if ( setrawcookie("FrodoWiki", $Frodocookie, time()+7*24*60*60, '/', $_SERVER['SERVER_NAME']) ) {
			logFrodo("  cookie set successfully.");
		} else {
			logFrodo("  cookie setting failed.");
		}
		// Now see if the user is already in the database ...
		
		$user = loadFromDatabaseFrodo($user, $Frodocookie);
		$result = 1; // This causes the rest of the authentication process to be skipped.
		return(false); // Ditto (see above)
		
	} else {	// No cookie, so we go to SSO.
//		logFrodo("$fname: No Frodocookie found - redirecting to SSO.");
		logFrodo("FrodoWiki cookie not found - redirecting to SSO.");

		$SSO .= curPageURL(); // Append this page's name
		http_redirect($SSO, array(), true, HTTP_REDIRECT);
	}	// No cookie
}

function loadFromDatabaseFrodo($user, $Frodocookie) {
	$fname   = "UserAuthFrodo::loadFromDatabaseFrodo";
	
	// Check whether user is in the database - if so, complete User.
	logFrodo("Entering $fname ...");
	// Explode the cookie:
	list ($empid, $email, $first, $last, $marketgroup) = explode("|", $Frodocookie, 5);
	logFrodo("Cookie exploded: $empid, $email, $first, $last, $marketgroup");
	
	// Now see if the user is known ...
	
	$dbr =& wfGetDB( DB_SLAVE );
	$s = $dbr->selectRow( 'user', array('user_id'), array('user_employee_id' => $empid), $fname);
	if ($s === false) {
		logFrodo("No entry found in db for employee id $empid - creating one ...");
		$user = new User();
		// MediaWiki requires names to start with a capital, so we have a stab at a reasonably formed name:
		$temp = explode(".", substr($email,0,strpos($email,'@')));
		$i   = 0;
		$lim = sizeof($temp);
		while ( $i < $lim) {
			$temp[$i] = ucwords($temp[$i]);
			$i++;
		}
		$userName = implode(".", $temp);
		$user->loadDefaults($userName);         // Added as it's done this way in CentralAuth.
		
		$user->mEmail              = $email;
		$user->mName               = $userName; // Redundant given use of loadDefaults...?
		$user->mEmployeeId         = $empid;
		$user->mRealName           = "$first $last";
		$user->mMarketGroup        = $marketgroup;
		$user->mEmailAuthenticated = wfTimestamp();
		$user->mTouched            = wfTimestamp();
		
		logFrodo("  mName               = $user->mName");
		logFrodo("  mEmployeeId         = $user->mEmployeeId");
		logFrodo("  mRealName           = $user->mRealName");
		logFrodo("  mMarketGroup        = $user->mMarketGroup");
		logFrodo("  mEmailAuthenticated = $user->mEmailAuthenticated");
		logFrodo("  mTouched            = $user->mTouched");
		
		$user->addToDatabase();
		logFrodo("User added to database with ID $user->mId.");
		
	} else {
		$user->mId = $s->user_id;
		logFrodo("DB entry found for employee id $empid with user id $user->mId");
	} 
	
	// Load the existing or newly-created user from the database ...
	
	if ( !$user->loadFromDatabase() ) {
		logFrodo("loadFromDatabase failed for user ID $user-mId");
	} else { // Additional debugging ...
		$user->saveToCache() ; # This loads the user's group membership!
		logFrodo("loadFromDatabase succeeded for user ID $user->mId");
		logFrodo("  real name: $user->mRealName");
		foreach( $user->mGroups as $group ) {
			logFrodo("  group: $group");
		}
	}
	return $user;
}

function curPageURL() {
 $pageURL = 'http';
 if (isset($_SERVER['HTTPS'])) {$pageURL .= "s";}
 $pageURL .= "://";
 if ($_SERVER['SERVER_PORT'] != "80") {
  $pageURL .= $_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI'];
 } else {
  $pageURL .= $_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
 }
 return $pageURL;
}

function curPageName() {
 return substr($_SERVER['SCRIPT_NAME'],strrpos($_SERVER['SCRIPT_NAME'],'/')+1);
}

function valtime($intime) {

	// Check whether supplied time is within five minutes of now.

	$time1   = implode(explode(":", $intime)); // Colonic irrigation!

	// Play at being a Time Lord here for testing.

	$inepoch = strtotime($time1); // Convert to epoch
	$myepoch = time();

	$diff = $myepoch - $inepoch;
	return($diff);
}

function logFrodo($message) {

	// Log significant events during authentication if the log file exists.

	$day = gmdate("Ymd");
	$authlog = "/xxxxxxx/local/log/FrodoWiki.authlog.$day";
	$now = gmdate("Y-m-d H:i:s ");
	
	if (file_exists($authlog)) {
		if ($file = fopen($authlog, "a")) {
			fputs($file, $now . $message . "\n");
			fclose($file);
			return(true);
		} else {
			return(false);
		}
	} else {
		return(true);
	}
}
?>

Sample Code Notes[edit]

Hoping to extend the utility of the sample code graciously contributed above, here are some comments I derived from my recent implementation of a similar extension, integrating MediaWiki into my corporation's internal global authentication and single sign-on infrastructure.

Redirects: My PHP installation does not include the (apparently optional) extension for http_redirect(). So, instead of the call to that function given above:

	http_redirect($SSO, array(), true, HTTP_REDIRECT);

I tracked down the MediaWiki's own internal functions for HTTP redirects. Using that instead, the above code would look something like the following:

	global $wgOut;
	...
	$wgOut->redirect( $SSO );

Sessions & Account creation: I'm not entirely sure how the originally offered code relates to the existing mechanism of sessions that MediaWiki maintains via the PHP SESSION mechanism & associated cookies. For my own version, I choose to preserve & engage that existing mechanism (and avoid introducing any new cookies), consulting the corporate authentication service only when there was no session active (e.g., once a day, rather than on every wiki-page access). This mostly entailed recasting most of the lines from User::loadFromSession into the initial section of my authentication function called by the UserLoadFromSession hook (to identify and honor any existing session), as well as calling a few key housekeeping routines to establish a new session (after creating a new wiki account for this user, if necessary).

Also, my account creation steps ended up looking a little different than in the originally offered code, (partly because I stayed closer to the native set of user attributes), so I include those steps here.

Thus, my main function starts as follows:

	global $wgUser, $wgCookieExpiration;
	
	# Ensure we have a PHP session in place.
	if( session_id() == '' ) {
		wfSetupSession();
	}

And it ends as follows (having already confirmed/ensured that we have valid corporate credentials for this user):

	# If no account exists, autocreate one.
	# Use the corpID (w/ leading cap) as the wiki user name (taken from $cookieCrumbs[5]).
	# Check validity just in case.
	$name = User::getCanonicalName( strtolower( $cookieCrumbs[5] ), 'creatable' );
	if ( $name == false) {
		$result = false;
		return true;
	}
	$user->mName = $name;
	$userId = $user->idForName();
	if ( 0 == $userId ) {
		XXXcreateAcct( $user );	# see below
	} else {
		$user->setId( $userId );
	}

	# Finally, automagically login based on the corporate credentials
	$user->loadfromDatabase();
	$user->saveToCache();		# this also loads the user's group membership
	$wgUser = $user;

	# here, use fixed offset, but elsewhere try to align with actual corporate credentials expiry
	$wgCookieExpiration = 13 * 60 * 60;
	$wgUser->setCookies();
	$result = true;				# user is logged in
	return true;

And my XXXcreateAcct function (referenced in the code above) looks like this:

# Automagically create a new wiki account for this user & return that user.
# Assumes $user->mName is already set to their (lowercase) corpID.
# Use corpID@corp.com as their email, and record it as already validated.
function XXXcreateAcct( $user ) {
	XXXparseHR( $user->mName, $realName );	# get real name of user from their HR cookie
	
	$user->loadDefaults( $user->mName );
	$user->mRealName		= $realName;
	$user->mEmail			= lcfirst( $user->mName ) . '@corp.com';
	$user->mEmailAuthenticated	= wfTimestampNow();
	$user->setOption( 'rememberpassword', 1 );			# implicitly loads other default options
	$user->addToDatabase();						# sets mId for us
	
	# Update user count
	$ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
	$ssUpdate->doUpdate();
}

-- Paul Lustgarten 19:06 3 April, 2009 (UTC)