~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to wiki/underlay/pages/HelpOnAuthentication(2f)ExternalCookie/revisions/00000001

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mto: This revision was merged to the branch mainline in revision 18.
  • Revision ID: james.westby@ubuntu.com-20080622211713-inlv5k4eifxckelr
ImportĀ upstreamĀ versionĀ 1.7.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
## Please edit system and help pages ONLY in the master wiki!
 
2
## For more information, please see MoinMoin:MoinDev/Translation.
 
3
##master-page:Unknown-Page
 
4
##master-date:Unknown-Date
 
5
#acl -All:write Default
 
6
#format wiki
 
7
#language en
 
8
<<TableOfContents>>
 
9
 
 
10
= Authenticate Via External Cookie =
 
11
 
 
12
== Requirements ==
 
13
 
 
14
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.
 
15
 
 
16
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.  
 
17
 
 
18
In order to prevent hackers from easily creating their own cookie, Moin``Moin 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.  
 
19
 
 
20
 
 
21
 
 
22
== Strategy ==
 
23
 
 
24
The MoinMoin wiki code will be customized in two places.  ''wikiconfig.py'' will be modified to create a new External``Cookie class that will be used to authenticate a user.  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 variables will be added to wikiconfig.py to customize the Settings:Preferences page and to automatically create new user records when required.
 
25
 
 
26
Your wiki themes may 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.
 
27
 
 
28
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). 
 
29
 
 
30
== Alternative Strategies Not Implemented ==
 
31
 
 
32
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 Moin``Auth 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 My``SQL table soon after expiration by a Moin``Moin transaction.  However, most installations will probably choose to clear obsolete entries in the shared storage area with a process embedded in the other application.
 
33
 
 
34
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 ezPy``Crypto example programs were discouraging because of the amount of compute time required.  My``SQL was faster -- the measured wall time to validate the cookie was usually 0 seconds and always less than 0.02 seconds.  
 
35
 
 
36
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 Moin``Moin 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.
 
37
 
 
38
 
 
39
== ExternalCookie Class ==
 
40
 
 
41
The first step is to override the external_auth method of the Config class by adding the code snippet below to your wikiconfig.py.
 
42
 
 
43
 
 
44
 
 
45
The code below is for Moin 1.7. 
 
46
  * edit your wikiconfig.py or farmconfig.py
 
47
  * find the line containing `class Config(DefaultConfig):` or `class FarmConfig(DefaultConfig):`
 
48
  * replace the above line with all of the code below
 
49
  * if using farmconfig.py find and comment/uncomment the appropriate `class` statements
 
50
 
 
51
{{{#!python
 
52
# +++++++++++++++ beginning of external_cookie  example
 
53
 
 
54
# This is some sample code you might find useful when you want to use some
 
55
# external cookie (made by some other program, not moin) with moin.
 
56
# See the +++ places for customizing it to your needs.  Copy this
 
57
# code into your farmconfig.py or wikiconfig.py by pasting it over the current:
 
58
#
 
59
#    class Config(DefaultConfig):
 
60
# OR
 
61
#    class FarmConfig(DefaultConfig):
 
62
 
 
63
 
 
64
from MoinMoin.config.multiconfig import DefaultConfig
 
65
from MoinMoin.auth import BaseAuth
 
66
    
 
67
# This is included in case you want to create a log file during testing
 
68
import time
 
69
def writeLog(*args): 
 
70
    '''Write an entry in a log file with a timestamp and all of the args.'''
 
71
    s = time.strftime('%Y-%m-%d %H:%M:%S ',time.localtime())
 
72
    for a in args:
 
73
        s = '%s %s;' % (s,a)
 
74
    log = open('/mywiki/cookie.log', 'a') # +++ location for log file
 
75
    log.write('\n' + s + '\n')
 
76
    log.close()
 
77
    return
 
78
    
 
79
# these 2 methods are examples of how to "authenticate" the other application cookie
 
80
import MySQLdb
 
81
def verifySession(sidHash): # +++ to use this, uncomment a procedure call below in ExternalCookie
 
82
    """Return True if sidHash value exists (meaning user is currently logged on), false otherwise.  
 
83
    
 
84
    If you are not a MySQL user, find another way to store this information.  ActiveSession is a two-column table
 
85
    containing a hashed cookie value (sidHash) plus a date-time stamp (tStamp).
 
86
    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.
 
87
    """
 
88
    db = MySQLdb.connect(db='mydb',user='myuserid',passwd='mypw') #+++ user ID needs read access
 
89
    c = db.cursor()
 
90
    q = 'select sidHash from ActiveSession where sidHash="%s"' % sidHash
 
91
    result = c.execute(q)
 
92
    c.close()
 
93
    if result == 1:
 
94
        return True
 
95
    return False
 
96
    
 
97
def verifySessionPlus(sidHash,timeout=3600*4): # +++ to use this, uncomment a procedure call below in ExternalCookie
 
98
    """Return True if sidHash value exists (meaning user is currently logged on), false otherwise.  
 
99
    
 
100
    This version of verifySession deletes entries inactive for more than 4 hours and
 
101
    updates the tStamp field with each moin transaction.  If performance is 
 
102
    important, find another way to delete inactive sessions.
 
103
    """
 
104
    db = MySQLdb.connect(db='mydb',user='myuserid',passwd='mypw') # +++ user ID needs write access
 
105
    c = db.cursor()
 
106
    q = 'delete from ActiveSession where tStamp<"%s"' % int(time.time() - timeout) # delete inactive entries
 
107
    result = c.execute(q)
 
108
    q = 'update ActiveSession set tStamp=%s where sidHash="%s"' % (int(time.time()),sidHash)
 
109
    result = c.execute(q)
 
110
    c.close()
 
111
    if result == 1:
 
112
        return True
 
113
    return False
 
114
    
 
115
 
 
116
class ExternalCookie(BaseAuth):
 
117
    name = 'external_cookie'
 
118
    # +++ The next 2 lines may be useful if you are overriding the username method in your themes.
 
119
    # +++  If commented out, wiki pages will not have login or logout hyperlinks
 
120
    login_inputs = ['username', 'password'] # +++ required to get a login hyperlink in wiki navigation area
 
121
    logout_possible = True # +++ required to get a logout hyperlink in wiki navigation area
 
122
 
 
123
    def request(self, request, user_obj, **kw):
 
124
        """Return (user-obj,False) if user is authenticated, else return (None,True). """
 
125
        # login = kw.get('login') # +++ example does not use this; login is expected in other application
 
126
        # user_obj = kw.get('user_obj')  # +++ example does not use this
 
127
        # username = kw.get('name') # +++ example does not use this
 
128
        # logout = kw.get('logout') # +++ example does not use this; logout is expected in other application
 
129
        import Cookie
 
130
        user = None  # user is not authenticated
 
131
        try_next = True  # if True, moin tries the next auth method in auth list
 
132
        
 
133
        otherAppCookie = "MoinAuth" # +++ username, email,useralias, session ID separated by #
 
134
        try:
 
135
            cookie = Cookie.SimpleCookie(request.saved_cookie)
 
136
        except Cookie.CookieError:
 
137
            cookie = None # ignore invalid cookies
 
138
 
 
139
        if cookie and otherAppCookie in cookie: # having this cookie means user auth has already been done in other application
 
140
            import urllib
 
141
            cookievalue = cookie[otherAppCookie].value
 
142
            # +++ decode and parse the cookie value - edit this to fit your needs.
 
143
            cookievalue = urllib.unquote(cookievalue) # cookie value is urlencoded, decode it
 
144
            cookievalue = cookievalue.decode('iso-8859-1') # decode cookie charset to unicode
 
145
            cookievalue = cookievalue.split('#') # cookie has format loginname#email#aliasname#sessionid
 
146
 
 
147
            email = aliasname = sessionid = ''
 
148
            try:  # extract fields from other app cookie
 
149
                auth_username = cookievalue[0] # the wiki username
 
150
                email = cookievalue[1] # email is required for user to change and save user preferences
 
151
                aliasname = cookievalue[2] # aliasname is useful only if auth_username is not wiki-like
 
152
                sessionid = cookievalue[3] # optional other app session ID  -- a unique timestamp or random number 
 
153
            except IndexError: 
 
154
                pass  # do not need aliasname or sessionid unless you uncomment lines below
 
155
            #~ writeLog('auth_username',auth_username)
 
156
            #~ writeLog('email',email)
 
157
            #~ writeLog('aliasname',aliasname)
 
158
            #~ writeLog('sessionid',sessionid)
 
159
 
 
160
            # +++ any hacker can create a cookie, uncomment these lines to verify the cookie was created by the other application
 
161
            # if auth_username:
 
162
                # import hashlib  # +++ python 2.5; see http://code.krypto.org/python/hashlib for 2.3, 2.4 download
 
163
                # sidHash = hashlib.md5(cookie[otherAppCookie].value).hexdigest() 
 
164
                # sidOK = verifySession(sidHash) # verify user is currently logged on to the other application +++ or use verifySessionPlus
 
165
                # if not sidOK:
 
166
                    # auth_username = None
 
167
                    
 
168
            if auth_username:
 
169
                # we have an authorized user, create the moin user object
 
170
                from MoinMoin.user import User
 
171
                # giving auth_username to User constructor means that authentication has already been done.
 
172
                user = User(request, name=auth_username, auth_username=auth_username, auth_method=self.name)
 
173
                changed = False
 
174
                if email != user.email: # was the email addr externally updated?
 
175
                    user.email = email ; 
 
176
                    changed = True # yes -> update user profile
 
177
                # if aliasname != user.aliasname: # +++ was the aliasname externally updated?
 
178
                    # user.aliasname = aliasname ; 
 
179
                    # changed = True # yes -> update user profile
 
180
 
 
181
                if user:
 
182
                    user.create_or_update(changed)
 
183
                if user and user.valid: 
 
184
                    try_next = False # have valid user; stop processing auth method list
 
185
        #~ writeLog(str(user), try_next)
 
186
        return user, try_next 
 
187
 
 
188
 
 
189
class Config(DefaultConfig):
 
190
#~ class FarmConfig(DefaultConfig):
 
191
 
 
192
    # from MoinMoin.auth import MoinAuth
 
193
    # auth = [ExternalCookie(), MoinAuth()] # try external_cookie, then normal moin login
 
194
    auth = [ExternalCookie()] # only way to login is external application
 
195
    
 
196
    # +++ these are suggested changes to the user preferences form if the  external_cookie  is the only auth method
 
197
    user_form_disable = ['name', 'aliasname', 'email',] # don't let the user change these, but show them
 
198
    user_form_remove = ['password', 'password2', 'css_url', 'logout', 'create', 'account_sendmail','jid'] # remove completely
 
199
 
 
200
    user_autocreate = True # +++ Moin will autocreate a new user ID if none exists
 
201
 
 
202
# +++++++++++++++ end of external_cookie  example,  your custom configurations continue below 
 
203
}}}
 
204
 
 
205
== Initial Testing ==
 
206
 
 
207
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.
 
208
 
 
209
Next, click on Tools... Web Developer... Cookies... Add Cookie.  Give the cookie a name of ''Moin``Auth'' (case sensitive). Set the value to `yourname#youremail@yourprovider.com` with 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.
 
210
 
 
211
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.  
 
212
 
 
213
== Changing the Other Application ==
 
214
  
 
215
The example external_cookie method expects the cookie value to contain several pieces of information separated with the '''#''' character:
 
216
  * the wiki user id -- always required
 
217
  * the user's email address -- needed if the user is to be able to update and save MoinMoin User Preferences
 
218
  * user alias -- usually not required, used to override user IDs that are not wiki-like user names
 
219
  * unique session ID -- a big random number or timestamp and random number
 
220
 
 
221
Your application must store the Moin``Auth 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.
 
222
 
 
223
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.    
 
224
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.   
 
225
 
 
226
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 Moin``Auth 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.  
 
227
 
 
228
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 My``SQL 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.
 
229
 
 
230
As you begin to modify your application almost all of your testing can be done with the Firefox Web Developer extension.  Before you login to the other application there should be no Moin``Auth cookie, after login there should be a Moin``Auth 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.  Accessing a wiki page after you are logged in will cause moin to create a MOIN_SESSION cookie.
 
231
 
 
232
 
 
233
 
 
234
== Modifying themes ==
 
235
 
 
236
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 /Moin``Moin/theme/__init__.py script directly.
 
237
 
 
238
If you always log on to your other application before accessing a wiki page, this modification and the one following are not required.  If you comment out the `login_inputs` and `logout_possible` variables near the top of the External``Cookie class, then your wiki pages will not contain login and logout hyperlinks.
 
239
 
 
240
 
 
241
The code below is for moin 1.7.
 
242
 
 
243
{{{#!python
 
244
    def username(self, d):
 
245
        """ Assemble the username / userprefs link
 
246
        
 
247
        @param d: parameter dictionary
 
248
        @rtype: unicode
 
249
        @return: username html
 
250
        """
 
251
        request = self.request
 
252
        _ = request.getText
 
253
 
 
254
        userlinks = []
 
255
        # Add username/homepage link for registered users. We don't care
 
256
        # if it exists, the user can create it.
 
257
        if request.user.valid and request.user.name:
 
258
            interwiki = wikiutil.getInterwikiHomePage(request)
 
259
            name = request.user.name
 
260
            aliasname = request.user.aliasname
 
261
            if not aliasname:
 
262
                aliasname = name
 
263
            title = "%s @ %s" % (aliasname, interwiki[0])
 
264
            # link to (interwiki) user homepage
 
265
            homelink = (request.formatter.interwikilink(1, title=title, id="userhome", generated=True, *interwiki) +
 
266
                        request.formatter.text(name) +
 
267
                        request.formatter.interwikilink(0, title=title, id="userhome", *interwiki))
 
268
            userlinks.append(homelink)
 
269
            # link to userprefs action
 
270
            if 'userprefs' not in self.request.cfg.actions_excluded:
 
271
                userlinks.append(d['page'].link_to(request, text=_('Settings'),
 
272
                                               querystr={'action': 'userprefs'}, id='userprefs', rel='nofollow'))
 
273
 
 
274
        if request.user.valid:  
 
275
            if request.user.auth_method in request.cfg.auth_can_logout:
 
276
                userlinks.append('<a href="/myapp/Logout">%s</a>' % _('Logout', formatted=False)) # +++ other app logout page
 
277
        else:
 
278
            query = {'action': 'login'}
 
279
            # special direct-login link if the auth methods want no input
 
280
            if request.cfg.auth_login_inputs == ['special_no_input']:
 
281
                query['login'] = '1'
 
282
            if request.cfg.auth_have_login:
 
283
                userlinks.append('<a  href="/myapp/Login">%s</a>' % _("Login", formatted=False)) # +++ other app login page
 
284
 
 
285
        userlinks = [u'<li>%s</li>' % link for link in userlinks]
 
286
        html = u'<ul id="username">%s</ul>' % ''.join(userlinks)
 
287
        return html
 
288
}}}
 
289
 
 
290
 
 
291
== Modifying the Application Login Page ==
 
292
 
 
293
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.  
 
294
 
 
295
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.