Thursday, December 11, 2008

Single Signon Between Mediawiki And Rails

I was fully anticipating this to be a really nasty endaevor. As it turns out, it really wasnt all that bad. Not that I got the gist of how to do this from a page on the case.edu wiki, which described how to do it between two php apps, and made it happen on Rails. First step is to make a media wiki authentication module. Something like:

<?php
  require_once('AuthPlugin.php');
  class RailsAuthPlugin extends AuthPlugin {

  //return whether $username is a valid username
  function userExists($username) {
  //since the username will be passed from our external source, this will probably 
  //always be true
  //however, the security paranoid says to check the data
  //you could do an LDAP verify here, just to be safe

  return true; //or return false if the username is invalid
  }

  //whether the given username and password authenticate
  function authenticate($username, $password) {
    //the external authentication actually handles this part, but we still need a security 
    //check
    //this form element will be set by our login script.  this security check is important!
    global $wgLoginFormKey;

    return isset($_POST[$wgLoginFormKey]);
  }

  //The authorization is external, so autocreate accounts as necessary
  function autoCreate() {
    return true;
  }

  //tell MediaWiki to not look in its database for user authentication and that our 
  //authentication method is all that counts
  function strict() {
    return true;
  }

  //this function gets called when the user is created
  //$user is an instance of the User class (see includes/User.php)
  function initUser(&$user) {
    //unless you want the person to be nameless, you should probably populate info about 
    //this user here
    //we do some LDAP queries to populate their name and e-mail

    $this_username = trim($user->getName()); 

    $theData = ""; # this could be some callback to Rails to get the email address of the user.

   $user->setEmail($theData);

    $theData = ""; # this could be some callback to Rails to get the name of the user

    $user->setRealName($theData);

    //if using MediaWiki 1.5, we can set some e-mail options
    $user->mEmailAuthenticated = wfTimestampNow();

    //turn on e-mail notifications by default
    $user->setOption('enotifwatchlistpages', 1);
    $user->setOption('enotifusertalkpages', 1);
    $user->setOption('enotifminoredits', 1);
    $user->setOption('enotifrevealaddr', 1);

  }

  //if using MediaWiki 1.5, we have a new function to modify the UI template!
  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", true);
  }
}
?>
Then add these lines to LocalSettings.php:

$wgCookieDomain = '.mydomain.com';
$wgLoginFormKey = "insert_secret_key_here"; 
require_once("extensions/RailsAuthPlugin.php"); # or whatever you named your extention
$wgAuth = new RailsAuthPlugin(); # or whatever you named the class
Then on the rails app, you want it to essentially authenticate on the backend whenever a user authenticates to the rails application. I have an after_filter on login that handles this, and here’s what it looks like:
def check_wiki
  if logged_in?
    begin
      require 'net/http'
      require 'uri'
      require 'cgi'

      cookie_domain = '.my_domain.com'
      #
      # Note that wpPassword can be anything, and the value of the my_secret_key
      # is irrelevant as well that parameter name needs to equal the value 
      # of $wgLoginFormKey from LocalSettings.php
      data = "wpName=#{CGI::escape(current_user.login)}" 
      data = data + "&\wpPassword=lygernoob" 
      data = data + "&wpLoginattempt=Log%20in" 
      data = data + "&my_secret_key=true" 

      headers = {
        'Content-Type' => 'application/x-www-form-urlencoded'
      }

      http = Net::HTTP.new('wiki.mydomain.com', 80)
      path = "/index.php?title=Special:UserLogin&returnto=Main_Page" 
      resp, data = http.post(path,data,headers)

      returned_cookies = resp['set-cookie'].split(',')
      returned_cookies.each do |b|
        b.strip!
        if b =~ /^([A-Za-z0-9_]+)\=([A-Za-z0-9_]+)/
          cookie_name, cookie_value = [$1, $2]
          cookies[cookie_name] = {:value => cookie_value, 
                                  :expires => 30.days.from_now, 
                                  :domain => cookie_domain, :path => '/'}
        end
      end
    rescue
    end
  end
end

You’ll also want to add some stuff to the rails logout routine:
cookies.delete 'wikiToken', {:domain => '.mydomain.com'}
cookies.delete 'wiki_session', {:domain => '.mydomain.com'}
cookies.delete 'wikiUserID', {:domain => '.mydomain.com'}
cookies.delete 'wikiUserName', {:domain => '.mydomain.com'}
And you’ll probably want to redirect the login and logout pages on your mediawiki to your Rails instance. But that’s really about all it takes. Single-signon between your Rails user database and Mediawiki. Fun fun fun.