Authenticate Via External Cookie

Requirements

The external cookie authentication alternative may be a good choice for your installation if your MoinMoin wiki topic is closely associated with some other application. You may be using MoinMoin for application documentation, popup help functions, or bug tracking. Several of your application pages may have hyperlinks to wiki pages. Wiki pages may contain custom macros that present data from your application database. Wiki users must login before they can update wiki pages (or you wish that were the case). Everyone with an application login ID also has a wiki login ID. Your users are annoyed that they frequently must login twice, first to the other application and then to MoinMoin. Your administrators are annoyed because they must maintain login IDs in two places.

To authenticate a MoinMoin user with an external cookie, your other application must be modified to create a cookie with each successful login. It must delete the cookie as part of the logout process.

In order to prevent hackers from easily creating their own cookie, MoinMoin transactions need to authenticate the cookie. One way to do this is to modify your other application to store a hash of the cookie value in a database, file, or other secure shared storage area that can be accessed by MoinMoin.

Strategy

The MoinMoin wiki code will be customized in two places. wikiconfig.py will be modified to override the external_auth method of the Config class. Logging in to your other application will effectively log you in to the wiki. Logging off of your other application will log you out of the wiki. Several parameters within wikiconfig.py will be overridden to customize the User Preferences page and to automatically create new user records when required.

Your wiki themes will be modified to override the username method of the Theme class. This method generates the login and logout hyperlinks within the wiki navigation area. These hyperlinks will be customized to point to the login and logout pages of your other application.

You must modify your other application as outlined above to create/delete a cookie and (optionally) add/delete entries to a shared storage area. If your wiki users can read the wiki without logging in, you may want to modify your other application login page so a wiki reader (logging in from a wiki page) will be returned to their referred-from page after login is complete (similar to the way MoinMoin login works).

It is probably best to not synchronize inactivity timeouts between the other application and the wiki. If a logged in user on the other application times out after an hour of inactivity, he can continue to edit and save pages on the wiki provided the other application timeout process is not modified to delete the entry in the shared storage area. If the other application timed-out user logs in a again, a new entry in the shared storage area will be created and a new MoinAuth cookie generated -- subsequent wiki transactions will use the new cookie. The example wikiconfig.py contains an example showing how inactive entries might be removed from a MySQL table soon after expiration by a MoinMoin transaction. However, most installations will probably choose to clear obsolete entries in the shared storage area with a process embedded in the other application.

Alternative Strategies Not Implemented

One alternative to the use of a shared storage area is to encrypt the cookie in the other application and decrypt the cookie in the external_auth method. If this method were implemented, the addition of a timestamp to the cookie value could force cookies to timeout with the addition of an aging check. Without the aging check, any cookie generated would be valid forever (a minor issue) or until the encryption keys were changed. Brief tests with the ezPyCrypto example programs were discouraging because of the amount of compute time required. MySQL was faster -- the measured wall time to validate the cookie was usually 0 seconds and always less than 0.02 seconds.

Another possibility to thwart the use of stolen cookies is to add the authenticated user's IP address to the cookie value and then check it against the IP address of the incoming MoinMoin transaction. But this has marginal value because some ISPs (AOL) change the low order bits of the IP address with each transaction and in other cases several users could appear to be coming from the same IP address.

external_auth method

The first step is to override the external_auth method of the Config class by adding the code snippet below to your wikiconfig.py.

If you use the Firefox browser and have the Web Developer addon extension installed, initial testing will be fast and painless. Log on to your other application and click on Tools... Web Developer... Cookies... View Cookie Information. You will probably find at least one cookie that looks like a session identifier created by your application at login, usually these will have a value with a large random number and perhaps a time stamp.

Next, click on Tools... Web Developer... Cookies... Add Cookie. Give the cookie a name of MoinAuth (case sensitive). Set the value to "yourname#youremail@yourprovider.com" without the quotes, no spaces, and be sure to use the # character as a separator. If your other application and the wiki run on the same host, set the host to the same value used by your other application (if not, omit the host from the domain name to share cookies across hosts). Set the path value to "/" without the quotes. Click the Session Cookie check box and click save.

Open a second browser window and load your starting wiki page. The wiki should recognize you as a logged in user. Do a quick test to verify that you can edit and save a wiki page and verify that you can change your User Preferences.

The code below is for version of Moin 1.6.0 beta 2. If you are on version 1.5.8, check the comments for modifications. To install:

   1 # +++++++++++++++ beginning of external_cookie  example
   2 
   3 # This is some sample code you might find useful when you want to use some
   4 # external cookie (made by some other program, not moin) with moin.
   5 # See the +++ places for customizing it to your needs.  Copy this
   6 # code into your farmconfig.py or wikiconfig.py by pasting it over the current:
   7 #
   8 #    class Config(DefaultConfig):
   9 # OR
  10 #    class FarmConfig(DefaultConfig):
  11 
  12 
  13 from MoinMoin.config.multiconfig import DefaultConfig # +++ Moin 1.6.0 beta 2
  14 #~ from MoinMoin.multiconfig import DefaultConfig # +++ Moin 1.5.8 
  15     
  16 # This is included in case you want to create a log file during testing
  17 import time
  18 def writeLog(*args): 
  19     '''Write an entry in a log file with a timestamp and all of the args.'''
  20     s = time.strftime('%Y-%m-%d %H:%M:%S ',time.localtime())
  21     for a in args:
  22         s = '%s %s;' % (s,a)
  23     log = open('/home/some/path/cookie.log', 'a') # +++ location for log file
  24     log.write('\n' + s + '\n')
  25     log.close()
  26     return
  27     
  28 # these 2 methods are examples of how to "authenticate" the other application cookie
  29 import MySQLdb
  30 def verifySession(sidHash): # +++ to use this, uncomment a procedure call below in external_cookie
  31     """Return True if sidHash value exists (meaning user is currently logged on), false otherwise.  
  32     
  33     If you are not a MySQL user, find another way to store this information.  ActiveSession is a two-column in-memory table
  34     containing a hashed cookie value (sidHash) plus a date-time stamp (tStamp).
  35     Your other application must add an entry to this table each time a user logs on and delete the entry when the user logs off.
  36     """
  37     db = MySQLdb.connect(db='mydb',user='myuid',passwd='mypw') #+++ user ID needs read access
  38     c = db.cursor()
  39     q = 'select sidHash from ActiveSession where sidHash="%s"' % sidHash
  40     result = c.execute(q)
  41     c.close()
  42     if result == 1:
  43         return True
  44     return False
  45     
  46 def verifySessionPlus(sidHash,timeout=3600*4): # +++ to use this, uncomment a procedure call below in external_cookie
  47     """Return True if sidHash value exists (meaning user is currently logged on), false otherwise.  
  48     
  49     This version of verifySession deletes entries inactive for more than 4 hours and
  50     updates the tStamp field with each moin transaction.  If performance is 
  51     important, find another way to delete inactive sessions.
  52     """
  53     db = MySQLdb.connect(db='mydb',user='myuid',passwd='mypw') # +++ user ID needs write access
  54     c = db.cursor()
  55     q = 'delete from ActiveSession where tStamp<"%s"' % int(time.time() - timeout) # delete inactive entries
  56     result = c.execute(q)
  57     q = 'update ActiveSession set tStamp=%s where sidHash="%s"' % (int(time.time()),sidHash)
  58     result = c.execute(q)
  59     c.close()
  60     if result == 1:
  61         return True
  62     return False
  63     
  64 
  65 class Config(DefaultConfig):
  66 #~ class FarmConfig(DefaultConfig):
  67  
  68     def external_cookie(request, **kw):
  69         """Return (user-obj,False) if user is authenticated, else return (None,True). """
  70         # login = kw.get('login') # +++ example does not use this; login is expected in other application
  71         # user_obj = kw.get('user_obj')  # +++ example does not use this
  72         # username = kw.get('name') # +++ example does not use this
  73         # logout = kw.get('logout') # +++ example does not use this; logout is expected in other application
  74         import Cookie
  75         user = None  # user is not authenticated
  76         try_next = True  # if True, moin tries the next auth method in auth list
  77         
  78         otherAppCookie = "MoinAuth" # +++ username, optional email,useralias, session ID separated by #
  79         moinCookie = 'MOIN_SESSION'
  80         try:
  81             cookie = Cookie.SimpleCookie(request.saved_cookie)
  82         except Cookie.CookieError:
  83             # ignore invalid cookies
  84             cookie = None
  85 
  86         if cookie and otherAppCookie in cookie: # having this cookie means user auth has already been done in other application
  87             import urllib
  88             cookievalue = cookie[otherAppCookie].value
  89             # +++ now we decode and parse the cookie value - edit this to fit your needs.
  90             cookievalue = urllib.unquote(cookievalue) # cookie value is urlencoded, decode it
  91             cookievalue = cookievalue.decode('iso-8859-1') # decode cookie charset to unicode
  92             cookievalue = cookievalue.split('#') # cookie has format loginname#email#aliasname#sessionid
  93 
  94             email = aliasname = sessionid = ''
  95             try:  # extract fields from other app cookie
  96                 auth_username = cookievalue[0] # the wiki username
  97                 email = cookievalue[1] # email is required for user to change and save userpreferences
  98                 aliasname = cookievalue[2] # aliasname is useful only if auth_username is not wiki-like
  99                 sessionid = cookievalue[3] # optional other app session ID  -- a unique timestamp or random number 
 100             except IndexError: 
 101                 pass  # do not need aliasname or sessionid unless you uncomment lines below
 102             #~ writeLog('auth_username',auth_username)
 103             #~ writeLog('email',email)
 104             #~ writeLog('aliasname',aliasname)
 105             #~ writeLog('sessionid',sessionid)
 106 
 107             # +++ any hacker can create a cookie, uncomment the these lines to verify the cookie was created by the other application
 108             # if auth_username:
 109                 # import hashlib  # +++ python 2.5; see http://code.krypto.org/python/hashlib for 2.3, 2.4 download
 110                 # sidHash = hashlib.md5(cookie[otherAppCookie].value).hexdigest() 
 111                 # sidOK = verifySession(sidHash) # verify user is currently logged on to the other application +++ or use verifySessionPlus
 112                 # if not sidOK:
 113                     # auth_username = None
 114                     
 115             if auth_username:
 116                 # we have an authorized user, create the moin user object
 117                 from MoinMoin.user import User
 118                 # giving auth_username to User constructor means that authentication has already been done.
 119                 user = User(request, name=auth_username, auth_username=auth_username)
 120 
 121                 changed = False
 122                 if email != user.email: # was the email addr externally updated?
 123                     user.email = email ; 
 124                     changed = True # yes -> update user profile
 125                 # if aliasname != user.aliasname: # +++ was the aliasname externally updated?
 126                     # user.aliasname = aliasname ; 
 127                     # changed = True # yes -> update user profile
 128 
 129                 if user:
 130                     user.create_or_update(changed)
 131                 if user and user.valid: 
 132                     try_next = False # have valid user; stop processing auth method list
 133                     
 134                     # +++ the following lines do not apply to 1.5.8
 135                     from MoinMoin.auth import setCookie, SessionData
 136                     # we need to set a moin session cookie to save page trails
 137                     # secrecy is maintained in other application cookie - here we can use user name for value
 138                     cookieValue = auth_username
 139                     # cookieValue = hashlib.md5(auth_username).hexdigest() # +++ Or use this to obscure user name
 140                     maxage = 3600 * 24 * 30 # +++ remember users page trail for a month
 141                     expires = time.time() + maxage
 142                     request.session = SessionData(request, cookieValue, expires)
 143                     setCookie(request, moinCookie, cookieValue, maxage, expires)
 144                     # +++ the above lines do not apply to 1.5.8
 145                 
 146         return user, try_next 
 147 
 148 
 149     # +++ to use the external_cookie method, you must override the auth list
 150     auth = [external_cookie]  # +++ external cookie only way to login;  works with 1.5.8 or 1.6.0 beta 2
 151     # +++ for 1.6.0 beta 2, this example external_cookie may be used with anonymous login feature
 152     # from MoinMoin.auth import moin_session, moin_anon_session # +++ 1.6.0 only
 153     # auth = [external_cookie, moin_session, moin_anon_session]  # +++ 1.6.0 only; external cookie or anonymous login
 154 
 155     user_autocreate = True # +++ Moin will autocreate new user ID if none exists
 156 
 157     # +++ these are suggested changes to the user preferences form if the  external_cookie  is the only auth method
 158     user_form_disable = ['name','email',] # don't let the user change these, but show them:
 159     user_form_remove = ['password', 'password2', 'logout', 'create', 'account_sendmail',] # remove completely:
 160 
 161     anonymous_cookie_lifetime = 2  # +++ anonymous sessions show page trail; save for 2 hours
 162 
 163 # +++++++++++++++ end of external_cookie  example,  your custom configurations continue below 

Changing the Other Application

The example external_cookie method expects the cookie value to contain several pieces of information separated with the # character:

Your application must store the MoinAuth cookie with a path set to '/'. Setting the path to '/' is normally not recommended because it presents a small security risk. It allows an application to read the cookies created by a different application. In this case, that is exactly our intent -- MoinMoin must be able to read the cookies set by the other application.

As you have already demonstrated to yourself above, any user with the skill to install the Firefox Web Developer addon could easily create a cookie and hack his way into the wiki using someone else's ID. To prevent this from happening, MoinMoin will need to validate the cookie value against an entry in a secure shared storage area created by the other application.

We want to avoid creating a potential new security issue by building a cross-reference of user ID to session ID or even a list of session IDs that might be of use to a hacker. A better way is for your application to hash (not encrypt) the MoinAuth cookie value and store the result in a secure shared storage area. Add a timestamp to each entry so obsolete entries can be removed later.

There is commented out code in the example wikiconfig.py to perform a hash of the cookie contents and validate the result against a MySQL table. In addition, there is alternative commented out code that will delete entries from the MySQL table that are more than 4 hours old, validate the hashed cookie value against the table and update the timestamp. A better method is to modify the other application to remove obsolete entries from the table, perhaps each time a user logs in.

As you begin to modify your application almost all of your testing can be done with the Firefox Web Developer extension. Before you login there should be no MoinAuth cookie, after login there should be a MoinAuth cookie, after logout there should be no cookie. In addition, the shared storage area should be updated with a new hashed cookie value after login and cleared after logout.

Modifying themes

The next step is to modify the login and logout links in the navigation area of wiki pages. If you limit your users to one theme, modify the code below to point to your other application's login and logoff pages. If you allow users to choose from among several themes, it may be easiest to modify the /MoinMoin/theme/init.py script directly.

See +++ comments for changes between 1.6.0 beta 2 and 1.5.8:

   1     def username(self, d):
   2         """ Assemble the username / userprefs link
   3         
   4         @param d: parameter dictionary
   5         @rtype: unicode
   6         @return: username html
   7         """
   8         request = self.request
   9         _ = request.getText
  10 
  11         userlinks = []
  12         # Add username/homepage link for registered users. We don't care
  13         # if it exists, the user can create it.
  14         if request.user.valid and request.user.name:
  15             interwiki = wikiutil.getInterwikiHomePage(request)
  16             name = request.user.name
  17             aliasname = request.user.aliasname
  18             if not aliasname:
  19                 aliasname = name
  20             title = "%s @ %s" % (aliasname, interwiki[0])
  21             # link to (interwiki) user homepage
  22             homelink = (request.formatter.interwikilink(1, title=title, id="userhome", generated=True, *interwiki) +
  23                         request.formatter.text(name) +
  24                         request.formatter.interwikilink(0, title=title, id="userhome", *interwiki))
  25             userlinks.append(homelink)
  26             # link to userprefs action
  27             userlinks.append(d['page'].link_to(request, text=_('Preferences', formatted=False),
  28                                                querystr={'action': 'userprefs'}, id='userprefs', rel='nofollow')) # +++ 1.6.0
  29                                                #~ querystr={'action': 'userprefs'}, id="userprefs")) # +++ 1.5.8
  30         if request.cfg.show_login:
  31             if request.user.valid:
  32                 # +++ point href below to other app logout page
  33                 userlinks.append('<a href="/MyOtherApp/Logout">%s</a>' % _('Logout', formatted=False)) 
  34             else:
  35                 # +++ point href below to other app login page
  36                 userlinks.append('<a  href="/MyOtherApp/Login">%s</a>' % _("Login", formatted=False)) 
  37 
  38         userlinks = [u'<li>%s</li>' % link for link in userlinks]
  39         html = u'<ul id="username">%s</ul>' % ''.join(userlinks)
  40         return html

Modifying the Application Login Page

As a final touch, you may want to consider modifying your login page for wiki users who decide to login from a wiki page. This depends upon your choice of web servers, but most will pass something similar to an HTTP_REFERER field which will contain the prior URL.

Check this value to determine if the referrer page was a wiki page, if so save the URL and at the end of a successful logon either redirect the current page back the referring wiki page or open a new window with the URL of the referring page.