changeset 2009:1b14cc05a54a

refactor authentication and split out session handling
author Johannes Berg <johannes AT sipsolutions DOT net>
date Fri, 20 Apr 2007 15:35:14 +0200
parents 52c474b8c02f
children d234621dbf2f
files MoinMoin/action/login.py MoinMoin/action/logout.py MoinMoin/auth/__init__.py MoinMoin/auth/http.py MoinMoin/auth/interwiki.py MoinMoin/auth/ldap_login.py MoinMoin/auth/log.py MoinMoin/auth/mysql_group.py MoinMoin/auth/php_session.py MoinMoin/auth/smb_mount.py MoinMoin/auth/sslclientcert.py MoinMoin/config/multiconfig.py MoinMoin/request/__init__.py MoinMoin/session.py MoinMoin/theme/__init__.py MoinMoin/userform.py MoinMoin/xmlrpc/__init__.py contrib/auth_externalcookie/wikiconfig.py
diffstat 18 files changed, 1106 insertions(+), 903 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/action/login.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/action/login.py	Fri Apr 20 15:35:14 2007 +0200
@@ -12,6 +12,7 @@
 
 from MoinMoin import user, userform
 from MoinMoin.Page import Page
+from MoinMoin.widget import html
 
 def execute(pagename, request):
     return LoginHandler(pagename, request).handle()
@@ -24,6 +25,33 @@
         self.pagename = pagename
         self.page = Page(request, pagename)
 
+    def handle_multistage(self):
+        """Handle a multistage request.
+
+        If the auth handler wants a multistage request, we
+        now set up the login form for that.
+        """
+        _ = self._
+        request = self.request
+        form = html.FORM(method='POST', name='logincontinue')
+        form.append(html.INPUT(type='hidden', name='login', value='login'))
+        form.append(html.INPUT(type='hidden', name='stage',
+                               value=request._login_multistage_name))
+
+        request.emit_http_headers()
+        request.theme.send_title(_("Login"), pagename=self.pagename)
+        # Start content (important for RTL support)
+        request.write(request.formatter.startContent("content"))
+
+        extra = request._login_multistage(request, form)
+        request.write(unicode(form))
+        if extra:
+            request.write(extra)
+
+        request.write(request.formatter.endContent())
+        request.theme.send_footer(self.pagename)
+        request.theme.send_closing_html()
+
     def handle(self):
         _ = self._
         request = self.request
@@ -34,30 +62,14 @@
         islogin = form.get('login', [''])[0]
 
         if islogin: # user pressed login button
-            # Trying to login with a user name and a password
-            # Require valid user name
-            name = form.get('name', [''])[0]
-            if not user.isValidName(request, name):
-                error = _("""Invalid user name {{{'%s'}}}.
-Name may contain any Unicode alpha numeric character, with optional one
-space between words. Group page name is not allowed.""") % name
-
-            # we do NOT check this, we don't want to disclose whether a user
-            # exists or not to not help an attacker.
-            # Check that user exists
-            #elif not user.getUserId(request, name):
-            #    error = _('Unknown user name: {{{"%s"}}}. Please enter'
-            #                 ' user name and password.') % name
-
-            # Require password
-            else:
-                password = form.get('password', [None])[0]
-                if not password:
-                    error = _("Missing password. Please enter user name and password.")
-                else:
-                    if not request.user.valid:
-                        error = _("Sorry, login failed.")
-
+            if request._login_multistage:
+                return self.handle_multistage()
+            error = []
+            if hasattr(request, '_login_messages'):
+                for msg in request._login_messages:
+                    error.append('<p>')
+                    error.append(msg)
+                error = ''.join(error)
             return self.page.send_page(msg=error)
 
         else: # show login form
--- a/MoinMoin/action/logout.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/action/logout.py	Fri Apr 20 15:35:14 2007 +0200
@@ -22,6 +22,12 @@
 
     def handle(self):
         _ = self._
-        message = _("You are now logged out.")
+        # if the user really was logged out say so,
+        # but if the user manually added ?action=logout
+        # and that isn't really supported, then don't
+        if not self.request.user.valid:
+            message = _("You are now logged out.")
+        else:
+            message = None
         return self.page.send_page(msg=message)
 
--- a/MoinMoin/auth/__init__.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/__init__.py	Fri Apr 20 15:35:14 2007 +0200
@@ -1,45 +1,88 @@
 # -*- coding: iso-8859-1 -*-
 """
-    MoinMoin - modular authentication and session code
-
-    Here are some methods moin can use in cfg.auth authentication method list.
-    The methods from that list get called (from request.py) in that sequence.
-    They get request as first argument and also some more kw arguments:
-       name: the value we did get from a POST of the UserPreferences page
-             in the "name" form field (or None)
-       password: the value of the password form field (or None)
-       login: True if user has clicked on Login button
-       logout: True if user has clicked on Logout button
-       user_obj: the user_obj we have until now (user_obj returned from
-                 previous auth method or None for first auth method)
-       cookie: a Cookie.SimpleCookie instance containing the cookies for
-               this request, or None if no (valid) cookies were set
-       (we maybe add some more here)
+    MoinMoin - modular authentication handling
 
-    Use code like this to get them:
-        name = kw.get('name') or ''
-        password = kw.get('password') or ''
-        login = kw.get('login')
-        logout = kw.get('logout')
-        cookie = kw.get('cookie')
-        request.log("got name=%s len(password)=%d login=%r logout=%r" % (name, len(password), login, logout))
+    Each authentication method is an object instance containing
+    three methods:
+      * login(request, user_obj, **kw)
+      * logout(request, user_obj, **kw)
+      * request(request, user_obj, **kw)
     
-    The called auth method then must return a tuple (user_obj, continue_flag).
-    user_obj can be one of:
-    * a (newly created) User object
-    * None if we want to inhibit log in from previous auth methods
-    * what we got as kw argument user_obj (meaning: no change).
-    continue_flag is a boolean indication whether the auth loop shall continue
-    trying other auth methods (or not).
+    The kw arguments that are passed in are currently:
+       username: the value of the 'username' form field (or None)
+                 [login only]
+       password: the value of the 'password' form field (or None)
+                 [login only]
+       multistage: boolean indicating multistage login continuation
+                   [may not be present, login only]
 
-    The methods give a kw arg "auth_attribs" to User.__init__ that tells
-    which user attribute names are DETERMINED and set by this auth method and
-    must not get changed by the user using the UserPreferences form.
-    It also gives a kw arg "auth_method" that tells the name of the auth
-    method that authentified the user.
+    More may be added.
 
-    The moin_session method also defines request.session for both logged-in
-    as well as not logged-in users.
+    The request method is called for each request except login/logout.
+
+    The 'request' and 'logout' methods must return a tuple (user_obj, continue)
+    where 'user_obj' can be
+      * None, to throw away any previous user_obj from previous auth methods
+      * the passed in user_obj for no changes
+      * a newly created MoinMoin.user.User instance
+    and 'continue' is a boolean to indicate whether the next authentication
+    method should be tried.
+
+    The 'login' method must return a tuple
+        (user_obj, continue, multistage, message).
+
+    The user_obj and continue values have the same semantics as for the request
+    and logout methods.
+
+    The messages that are returned by the various auth methods will be
+    displayed to the user, since they will all be displayed usually auth
+    methods will use the message feature only along with returning False for
+    the continue flag.
+
+    The multistage item in the tuple must evaluate to false or be callable.
+    If it is callable, this indicates that the authentication method requires
+    a second login stage. In that case, the multistage item will be called
+    with the request as the only parameter. It should return an instance of
+    MoinMoin.widget.html.FORM and the generic code will append some required
+    hidden fields to it. It is also permissible to return some valid HTML,
+    but that feature has very limited use since it breaks the authentication
+    method chain.
+
+    Note that because multistage login does not depend on anonymous session
+    support, it is possible that users jump directly into the second stage
+    by giving the appropriate parameters to the login action. Hence, auth
+    methods should take care to recheck everything and not assume the user
+    has gone through all previous stages.
+
+    After the user has submitted the required form, execution of the auth
+    login methods resumes with the auth item that requested the multistage
+    login and its login method is called with the 'multistage' keyword
+    parameter set to True.
+
+    Each authentication method instance must also contain the members
+     * login_inputs: a list of required inputs, currently supported are
+                      - 'username': username entry field
+                      - 'password': password entry field
+     * logout_possible: boolean indicating whether this auth methods
+                        supports logging out
+     * name: name of the auth method, must be the same as given as the
+             user object's auth_method keyword parameter.
+
+    To simplify creating new authentication methods you can inherit from
+    MoinMoin.auth.BaseAuth that does nothing for all three methods, but
+    allows you to override only some methods.
+
+    cfg.auth is a list of authentication object instances whose methods
+    are called in the order they are listed. The session method is called
+    for every request, when logging in or out these are called before the
+    session method.
+
+    When creating a new MoinMoin.user.User object, you can give a keyword
+    argument "auth_attribs" to User.__init__ containing a list of user
+    attributes that are determined and fixed by this auth method and may
+    not be changed by the user in UserPreferences.
+    You also have to give the keyword argument "auth_method" containing the
+    name of the authentication method.
 
     @copyright: 2005-2006 Bastian Blank, Florian Festi,
                           MoinMoin:AlexanderSchremmer, Nick Phillips,
@@ -50,458 +93,53 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-import time, Cookie
-import hmac, sha, random
-
-from MoinMoin import user, caching
-
-# cookie names
-MOIN_SESSION = 'MOIN_SESSION'
-
-# maximum number of stored secrets, i.e. maximum number of different machines
-# a user can use concurrently without having to log in again
-MAX_STORED_SECRETS = 20
-
-class UserSecurityStringCache:
-    """ UserSecurityStringCache -- cache a list of secrets for user cookies
-
-    In order to avoid cookie stealing even after a user has logged out we
-    keep a list of secrets (in the cache) associated with a user and verify
-    that the cookie matches the right one.
-
-    This class manages the secrets and their LRU expiry.
-    """
-    def __init__(self, request, userid):
-        # we use 'farm' scope but hash the user_dir into the secret cache name
-        # to make both shared and non-shared user_dir in a farm work properly
-        cache_name = sha.sha(userid + request.cfg.user_dir).hexdigest()
-        self.ce = caching.CacheEntry(request, 'ussc', cache_name, 'farm', use_pickle=True)
-        self.request = request
-
-    def _load(self):
-        """ Internal: load string dict and LRU list from cache """
-        if self.ce.exists():
-            return self.ce.content()
-        return {}, []
-
-    def update(self, secidx):
-        """ tell the secret string cache that the secret identified was used
-
-        @param secidx: the index of that secret or None if a new one
-                       shall be assigned
-        """
-        secrets, lru = self._load()
-        # just move this secret to the front of the LRU queue
-        lru.remove(secidx)
-        lru.insert(0, secidx)
-        self.ce.update((secrets, lru))
-
-    def insert(self, secstring):
-        """ insert a new secret string into the cache
-
-        @param secstring: the new secret string
-        @rtype: int
-        @return: the new secret index
-        """
-        secrets, lru = self._load()
-        # find a new unused index
-        # try one that we'll expire first
-        if len(lru) >= MAX_STORED_SECRETS:
-            secidx = lru[-1]
-        else:
-            # select an unused index
-            secidx = random.randint(0, MAX_STORED_SECRETS*5)
-            while secidx in lru:
-                secidx = random.randint(0, MAX_STORED_SECRETS*5)
-        for idx in lru[MAX_STORED_SECRETS-1:]:
-            data = SessionData(self.request, secrets[idx], 0)
-            data.delete()
-            del secrets[idx]
-        lru = lru[:MAX_STORED_SECRETS-1]
-        lru.insert(0, secidx)
-        secrets[secidx] = secstring
-        self.ce.update((secrets, lru))
-        return secidx
-
-    def remove(self, secidx):
-        """ remove a given secret from the cache
-
-        @param secidx: the index of the secret to be removed
-        """
-        secrets, lru = self._load()
-        del secrets[secidx]
-        lru.remove(secidx)
-        self.ce.update((secrets, lru))
-
-    def getsecret(self, secidx):
-        """ get a secret from the cache
-
-        @param secidx: the index of the secret to get
-        """
-        secrets, lru = self._load()
-        if secidx in secrets:
-            return secrets[secidx]
-        return ''
-
-class SessionData:
-    """ SessionData -- store data for a session
-
-    This stores session data in memory and also maintains a cache of it on
-    disk, so the same data will be loaded from disk cache in the next request
-    of the same session.
-    
-    Once in a while, expired session's cache files will be automatically cleaned up.
-    """
-    def __init__(self, request, name, expires):
-        # we can use farm scope since the session name is totally random
-        # this means that the session is kept over multiple wikis in a farm
-        # when they share user_dir and cookies
-        self.ce = caching.CacheEntry(request, 'session', name, 'farm', use_pickle=True)
-        self.request = request
-        if self.ce.exists():
-            self._data = self.ce.content()
-        else:
-            self._data = {'expires': expires + 3600}
-        # Set 'expires' an hour later than it should actually expire.
-        # That way, the expiry code will delete the item an hour later
-        # than it has actually expired, but that is acceptable and we
-        # don't need to update the file all the time
-        if expires and (not 'expires' in self or self['expires'] < expires):
-            self['expires'] = expires + 3600
-
-        # every once a while, clean up deleted sessions:
-        if random.randint(0, 999) == 0:
-            self._cleanup()
-
-    def _cleanup(self):
-        cachelist = caching.get_cache_list(self.request, 'session', 'farm')
-        tnow = time.time()
-        for name in cachelist:
-            entry = caching.CacheEntry(self.request, 'session', name, 'farm', use_pickle=True)
-            try:
-                data = entry.content()
-                if 'expires' in data and data['expires'] < tnow:
-                    entry.remove()
-            except caching.CacheError:
-                pass
-
-    def __setitem__(self, name, value):
-        self._data[name] = value
-        # if we have only one item it must be 'expires'
-        if len(self._data) > 1:
-            self.ce.update(self._data)
-
-    def __getitem__(self, name):
-        return self._data[name]
-
-    def __contains__(self, name):
-        return name in self._data
-
-    def __delitem__(self, name):
-        del self._data[name]
-        # if just one item is left it'll be 'expires'
-        if len(self._data) == 1:
-            self.ce.remove()
-        else:
-            self.ce.update(self._data)
+from MoinMoin import user
 
-    def get(self, name, default=None):
-        return self._data.get(name, default)
-
-    def delete(self):
-        if self.ce.exists():
-            self.ce.remove()
-
-    def rename(self, newname):
-        self.ce.remove()
-        self.ce = caching.CacheEntry(self.request, 'session', newname, 'farm', use_pickle=True)
-        if len(self._data):
-            self.ce.update(self._data)
-
-
-def generate_security_string(length):
-    """ generate a random length (length/2 .. length) string with random content """
-    random_length = random.randint(length/2, length)
-    safe = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-'
-    return ''.join([random.choice(safe) for i in range(random_length)])
-
-def sign_cookie_data(request, data, securitystring):
-    """ generate a hash string based the securitystring and the data """
-    return hmac.new(securitystring, data).hexdigest()
-
-def makeCookie(request, cookie_name, cookie_string, maxage, expires):
-    """ create an appropriate cookie """
-    c = Cookie.SimpleCookie()
-    cfg = request.cfg
-    c[cookie_name] = cookie_string
-    c[cookie_name]['max-age'] = maxage
-    if cfg.cookie_domain:
-        c[cookie_name]['domain'] = cfg.cookie_domain
-    if cfg.cookie_path:
-        c[cookie_name]['path'] = cfg.cookie_path
-    else:
-        path = request.getScriptname()
-        if not path:
-            path = '/'
-        c[cookie_name]['path'] = path
-    # Set expires for older clients
-    c[cookie_name]['expires'] = request.httpDate(when=expires, rfc='850')
-    return c.output()
-
-def getCookieLifetime(request, u):
-    """ Get cookie lifetime for the user object u """
-    lifetime = int(request.cfg.cookie_lifetime) * 3600
-    forever = 10 * 365 * 24 * 3600 # 10 years
-    if not lifetime:
-        return forever
-    elif lifetime > 0:
-        if u.remember_me:
-            return forever
-        return lifetime
-    elif lifetime < 0:
-        return -lifetime
-    return lifetime
-
-def setCookie(request, cookie_name, cookie_string, maxage, expires):
-    """ Set cookie, raw helper. """
-    cookie = makeCookie(request, cookie_name, cookie_string, maxage, expires)
-    # Set cookie
-    request.setHttpHeader(cookie)
-    # IMPORTANT: Prevent caching of current page and cookie
-    request.disableHttpCaching()
-
-def setSessionCookie(request, u, secret=None, securitystringcache=None,
-                     secidx=None, session=None):
-    """ Set moin_session cookie for user obj u
+class BaseAuth:
+    name = None
+    login_inputs = []
+    logout_possible = False
+    def __init__(self):
+        pass
+    def login(self, request, user_obj, **kw):
+        return user_obj, True, None, None
+    def request(self, request, user_obj, **kw):
+        return user_obj, True
+    def logout(self, request, user_obj, **kw):
+        if self.name and user_obj and user_obj.auth_method == self.name:
+            user_obj.valid = False
+        return user_obj, True
 
-    cfg.cookie_lifetime and the user 'remember_me' setting set the
-    lifetime of the cookie. lifetime in in hours, see table:
-    
-    value   cookie lifetime
-    ----------------------------------------------------------------
-     = 0    forever, ignoring user 'remember_me' setting
-     > 0    n hours, or forever if user checked 'remember_me'
-     < 0    -n hours, ignoring user 'remember_me' setting
-    """
-    import base64
-    maxage = getCookieLifetime(request, u)
-    expires = time.time() + maxage
-    enc_username = base64.encodestring(u.auth_username).replace('\n', '')
-    enc_id = base64.encodestring(u.id).replace('\n', '')
-    if secret is None and secidx is None:
-        secret = generate_security_string(32)
-    if securitystringcache is None:
-        securitystringcache = UserSecurityStringCache(request, u.id)
-    if secret is None:
-        # secidx must be assigned
-        securitystringcache.update(secidx)
-        secret = securitystringcache.getsecret(secidx)
-    else:
-        secidx = securitystringcache.insert(secret)
-    cookie_body = "username=%s:id=%s:expires=%d:secidx=%d" % (enc_username, enc_id, expires, secidx)
-    cookie_hash = sign_cookie_data(request, cookie_body, secret)
-    cookie_string = ':'.join([cookie_hash, cookie_body])
-    setCookie(request, MOIN_SESSION, cookie_string, maxage, expires)
-
-    # move session data to new identifier
-    if session:
-        session.rename(secret)
-    else:
-        session = SessionData(request, secret, expires)
-    request.session = session
-
-def deleteCookie(request, cookie_name):
-    """ Delete the user cookie by sending expired cookie with null value
-
-    According to http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.2
-    Deleted cookie should have Max-Age=0. We also have expires attribute,
-    which is probably needed for older browsers.
+class MoinLogin(BaseAuth):
+    """ handle login from moin login form """
+    def __init__(self, verbose=False):
+        BaseAuth.__init__(self)
+        self.verbose = verbose
 
-    Finally, delete the saved cookie and create a new user based on the new settings.
-    """
-    cookie_string = ''
-    maxage = 0
-    # Set expires to one year ago for older clients
-    expires = time.time() - 3600 * 24 * 365 # 1 year ago
-    cookie = makeCookie(request, cookie_name, cookie_string, maxage, expires)
-    # Set cookie
-    request.setHttpHeader(cookie)
-    # IMPORTANT: Prevent caching of current page and cookie        
-    request.disableHttpCaching()
-
-def setAnonCookie(request, session_name):
-    """ Set moin_session cookie for anon user
+    login_inputs = ['username', 'password']
+    name = 'moin_login'
+    logout_possible = True
 
-    cfg.anonymous_cookie_lifetime [h] sets the lifetime of the cookie, if
-    defined. if not defined, we do not set the cookie.
-    """
-    if not hasattr(request.cfg, 'anonymous_cookie_lifetime'):
-        return
-    lifetime = request.cfg.anonymous_cookie_lifetime * 3600
-    expires = time.time() + lifetime
-    request.session = SessionData(request, session_name, expires)
-    setCookie(request, MOIN_SESSION, session_name, lifetime, expires)
-
+    def login(self, request, user_obj, **kw):
+        username = kw.get('username')
+        password = kw.get('password')
 
-def moin_login(request, **kw):
-    """ handle login from moin login form, session has to be established later by moin_session """
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    #logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
+        if not username and not password:
+            return user_obj, True, None, None
 
-    cfg = request.cfg
-    verbose = False
-    if hasattr(cfg, 'moin_login_verbose'):
-        verbose = cfg.moin_login_verbose
+        _ = request.getText
 
-    #request.log("auth.moin_login: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
+        verbose = self.verbose
 
-    if login:
         if verbose: request.log("moin_login performing login action")
-        u = user.User(request, name=username, password=password, auth_method='moin_login')
+
+        if username and not password:
+            return user_obj, True, None, _('Missing password. Please enter user name and password.')
+
+        u = user.User(request, name=username, password=password, auth_method=self.name)
         if u.valid:
             if verbose: request.log("moin_login got valid user...")
-            user_obj = u
+            return u, True, None, None
         else:
             if verbose: request.log("moin_login not valid, previous valid=%d." % user_obj.valid)
-
-    return user_obj, True
-
-def moin_session(request, **kw):
-    """ Authenticate via cookie.
-    
-    We don't handle initial logins (except to set the appropriate cookie), just
-    ongoing sessions, and logout. Use another method for initial login.
-    """
-    import base64
-
-    username = kw.get('name')
-    login = kw.get('login')
-    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-
-    cfg = request.cfg
-    verbose = False
-    if hasattr(cfg, 'moin_session_verbose'):
-        verbose = cfg.moin_session_verbose
-
-    cookie_name = MOIN_SESSION
-
-    # load up our cookie
-    cookie = kw.get('cookie')
-    if cookie is not None and cookie_name in cookie:
-        cookievalue = cookie[cookie_name].value
-        cookieitems = cookievalue.split(':', 1)
-    else:
-        cookievalue = None
-
-    if verbose: request.log("auth.moin_session: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
-
-    if login:
-        if verbose: request.log("moin_session performing login action")
-
-        # Has any other method successfully authenticated?
-        if user_obj is not None and user_obj.valid:
-            # Yes - set up session cookie
-            if verbose: request.log("moin_session got valid user from previous auth method, setting cookie...")
-            if verbose: request.log("moin_session got auth_username %s." % user_obj.auth_username)
-            sessiondata = None
-            if cookievalue and len(cookieitems) == 1:
-                # we have an anonymous session so migrate the data, since we
-                # will migrate it we don't need a proper expiry value
-                sessiondata = SessionData(request, cookievalue, 0)
-            setSessionCookie(request, user_obj, session=sessiondata)
-            return user_obj, True # we make continuing possible, e.g. for smbmount
-        else:
-            # No other method succeeded, so allow continuation...
-            # XXX Cookie clear here???
-            if verbose: request.log("moin_session did not get valid user from previous auth method, doing nothing")
-            if cookievalue and len(cookieitems) == 1:
-                # keep non-logged in session
-                setAnonCookie(request, cookieitems[0])
-            return user_obj, True
-
-    if cookievalue is None:
-        # No valid cookie
-        if verbose: request.log("either no cookie or no %s key" % cookie_name)
-        return user_obj, True
-
-    if len(cookieitems) == 1:
-        # non-logged in session
-        setAnonCookie(request, cookieitems[0])
-        return user_obj, True
-
-    # otherwise we have a signed cookie
-    cookie_hash, cookie_body = cookieitems
-
-    # Parse cookie, be careful
-    params = {'username': '', 'id': '', 'expires': 0, 'secidx': -1, }
-    cookie_pairs = cookie_body.split(":")
-    for key, value in [pair.split("=", 1) for pair in cookie_pairs]:
-        try:
-            if isinstance(params[key], str):
-                params[key] = base64.decodestring(value)
-            elif isinstance(params[key], int):
-                params[key] = int(value)
-        except Exception:
-            # ignore any errors from parsing the values
-            pass
-    # This may seem odd, but checking expiry is cheaper
-    # than checking the signature.
-    if params['expires'] < time.time():
-        # XXX Cookie clear here???
-        if verbose: request.log("cookie expired")
-        return user_obj, True
-
-    secidx = params['secidx']
-
-    ussc = UserSecurityStringCache(request, params['id'])
-    secstring = ussc.getsecret(secidx)
-    if cookie_hash != sign_cookie_data(request, cookie_body, secstring):
-        # XXX Cookie clear here???
-        if verbose: request.log("cookie recovered had invalid hash")
-        return user_obj, True
-
-    if verbose: request.log("Cookie OK, authenticated.")
-
-    # XXX Should name be in auth_attribs?
-    u = user.User(request,
-                  id=params['id'],
-                  auth_username=params['username'],
-                  auth_method='moin_session',
-                  auth_attribs=(),
-                  )
-
-    if logout:
-        if verbose: request.log("Logout requested, setting u invalid and 'deleting' cookie")
-        u.valid = 0 # just make user invalid, but remember him
-        # delete secret for this cookie
-        ussc.remove(secidx)
-        deleteCookie(request, cookie_name)
-        session = SessionData(request, secstring, 0)
-        session.delete()
-        return u, True # we return a invalidated user object, so that
-                       # following auth methods can get the name of
-                       # the user who logged out
-    # refresh cookie lifetime
-    setSessionCookie(request, u, securitystringcache=ussc, secidx=secidx)
-    return u, True # use True to get other methods called, too
-
-def moin_anon_session(request, **kw):
-    """ Anonymous session support.
-
-    If you need sessions for anonymous users add this to the config.auth list
-    and set config.anonymous_cookie_lifetime (in hours, can be fractional.)
-    """
-    user_obj = kw.get('user_obj')
-
-    if request.session != {} or not hasattr(request.cfg, 'anonymous_cookie_lifetime'):
-        return user_obj, True
-
-    # moin_session can handle this cookie and migrate
-    # the session to a known one when you log in
-    session_name = generate_security_string(32)
-    setAnonCookie(request, session_name)
-    return user_obj, True
+            return user_obj, True, None, _("Invalid username or password.")
--- a/MoinMoin/auth/http.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/http.py	Fri Apr 20 15:35:14 2007 +0200
@@ -4,50 +4,76 @@
 
     You need either your webserver configured for doing HTTP auth (like Apache
     reading some .htpasswd file) or Twisted (will accept HTTP auth against
-    password stored in moin user profile, but currently will NOT ask for auth).
+    password stored in moin user profile, but currently will NOT ask for auth)
+    or Standalone (in which case it will ask for auth and accept auth against
+    stored user profile.)
 
     @copyright: 2006 MoinMoin:ThomasWaldmann
+                2007 MoinMoin:JohannesBerg
     @license: GNU GPL, see COPYING for details.
 """
 from MoinMoin import config, user
-from MoinMoin.request import TWISTED, CLI
-
-def http(request, **kw):
-    """ authenticate via http basic/digest/ntlm auth """
-    user_obj = kw.get('user_obj')
-    u = None
-    # check if we are running Twisted
-    if isinstance(request, TWISTED.Request):
-        username = request.twistd.getUser().decode(config.charset)
-        password = request.twistd.getPassword().decode(config.charset)
-        # when using Twisted http auth, we use username and password from
-        # the moin user profile, so both can be changed by user.
-        u = user.User(request, auth_username=username, password=password,
-                      auth_method='http', auth_attribs=())
+from MoinMoin.request import TWISTED, CLI, STANDALONE
+from MoinMoin.auth import BaseAuth
+from base64 import decodestring
 
-    elif not isinstance(request, CLI.Request):
-        env = request.env
-        auth_type = env.get('AUTH_TYPE', '')
-        if auth_type in ['Basic', 'Digest', 'NTLM', 'Negotiate', ]:
-            username = env.get('REMOTE_USER', '').decode(config.charset)
-            if auth_type in ('NTLM', 'Negotiate',):
-                # converting to standard case so the user can even enter wrong case
-                # (added since windows does not distinguish between e.g.
-                #  "Mike" and "mike")
-                username = username.split('\\')[-1] # split off domain e.g.
-                                                    # from DOMAIN\user
-                # this "normalizes" the login name from {meier, Meier, MEIER} to Meier
-                # put a comment sign in front of next line if you don't want that:
-                username = username.title()
-            # when using http auth, we have external user name and password,
-            # we don't use the moin user profile for those attributes.
-            u = user.User(request, auth_username=username,
-                          auth_method='http', auth_attribs=('name', 'password'))
+class HTTPAuth(BaseAuth):
+    """ authenticate via http basic/digest/ntlm auth """
+    name = 'http'
 
-    if u:
-        u.create_or_update()
-    if u and u.valid:
-        return u, True # True to get other methods called, too
-    else:
-        return user_obj, True
+    def request(self, request, user_obj, **kw):
+        u = None
+        _ = request.getText
+        # always revalidate auth
+        if user_obj and user_obj.auth_method == self.name:
+            user_obj = None
+        # something else authenticated before us
+        if user_obj:
+            return user_obj, True
 
+        # for standalone, request authorization and verify it,
+        # deny access if it isn't verified
+        if isinstance(request, STANDALONE.Request):
+            request.setHttpHeader('WWW-Authenticate: Basic realm="MoinMoin"')
+            auth = request.headers.get('Authorization')
+            if auth:
+                auth = auth.split()[-1]
+                info = decodestring(auth).split(':', 1)
+                if len(info) == 2:
+                    u = user.User(request, auth_username=info[0], password=info[1],
+                                  auth_method=self.name, auth_attribs=[])
+            if not u:
+                request.makeForbidden(401, _('You need to log in.'))
+        # for Twisted, just check
+        elif isinstance(request, TWISTED.Request):
+            username = request.twistd.getUser().decode(config.charset)
+            password = request.twistd.getPassword().decode(config.charset)
+            # when using Twisted http auth, we use username and password from
+            # the moin user profile, so both can be changed by user.
+            u = user.User(request, auth_username=username, password=password,
+                          auth_method=self.name, auth_attribs=())
+        elif not isinstance(request, CLI.Request):
+            env = request.env
+            auth_type = env.get('AUTH_TYPE', '')
+            if auth_type in ['Basic', 'Digest', 'NTLM', 'Negotiate', ]:
+                username = env.get('REMOTE_USER', '').decode(config.charset)
+                if auth_type in ('NTLM', 'Negotiate',):
+                    # converting to standard case so the user can even enter wrong case
+                    # (added since windows does not distinguish between e.g.
+                    #  "Mike" and "mike")
+                    username = username.split('\\')[-1] # split off domain e.g.
+                                                        # from DOMAIN\user
+                    # this "normalizes" the login name from {meier, Meier, MEIER} to Meier
+                    # put a comment sign in front of next line if you don't want that:
+                    username = username.title()
+                # when using http auth, we have external user name and password,
+                # we don't use the moin user profile for those attributes.
+                u = user.User(request, auth_username=username,
+                              auth_method=self.name, auth_attribs=('name', 'password'))
+
+        if u:
+            u.create_or_update()
+        if u and u.valid:
+            return u, True # True to get other methods called, too
+        else:
+            return user_obj, True
--- a/MoinMoin/auth/interwiki.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/interwiki.py	Fri Apr 20 15:35:14 2007 +0200
@@ -11,38 +11,44 @@
 
 import xmlrpclib
 from MoinMoin import auth, wikiutil, user
+from MoinMoin.auth import BaseAuth
 
-def interwiki(request, **kw):
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    user_obj = kw.get('user_obj')
+class InterwikiAuth(BaseAuth):
+    name = 'interwiki'
+    logout_possible = True
+    login_inputs = ['username', 'password']
 
-    if login:
+    def __init__(self, trusted_wikis):
+        BaseAuth.__init__(self)
+        self.trusted_wikis = trusted_wikis
+
+    def login(self, request, user_obj, **kw):
+        username = kw.get('username')
+        password = kw.get('password')
+
+        if not username or not password:
+            return user_obj, True, None, None
+
         if verbose: request.log("interwiki auth: trying to auth %r" % username)
         username = username.replace(' ', ':', 1) # Hack because ':' is not allowed in name field
         wikitag, wikiurl, name, err = wikiutil.resolve_wiki(request, username)
 
         if verbose: request.log("interwiki auth: resolve wiki returned: %r %r %r %r" % (wikitag, wikiurl, name, err))
-        if err or wikitag not in request.cfg.trusted_wikis:
-            return user_obj, True
-
-        if password:
-            homewiki = xmlrpclib.Server(wikiurl + "?action=xmlrpc2")
-            account_data = homewiki.getUser(name, password)
-            if isinstance(account_data, str):
-                # e.g. "Authentication failed", TODO: show error message
-                if verbose: request.log("interwiki auth: %r wiki said: %s" % (wikitag, account_data))
-                return user_obj, True
+        if err or wikitag not in self.trusted_wikis:
+            return user_obj, True, None, None
 
-            # TODO: check auth_attribs items
-            u = user.User(request, name=name, auth_method='interwiki', auth_attribs=('name', 'aliasname', 'password', 'email', ))
-            for key, value in account_data.iteritems():
-                if key not in request.cfg.user_transient_fields:
-                    setattr(u, key, value)
-            u.valid = 1
-            u.create_or_update(True)
-            if verbose: request.log("interwiki: successful auth for %r" % name)
-            return u, True # moin_session has to set the cookie
+        homewiki = xmlrpclib.Server(wikiurl + "?action=xmlrpc2")
+        account_data = homewiki.getUser(name, password)
+        if isinstance(account_data, str):
+            if verbose: request.log("interwiki auth: %r wiki said: %s" % (wikitag, account_data))
+            return user_obj, True, None, account_data
 
-    return user_obj, True
+        # TODO: check remote auth_attribs
+        u = user.User(request, name=name, auth_method=self.name, auth_attribs=('name', 'aliasname', 'password', 'email', ))
+        for key, value in account_data.iteritems():
+            if key not in request.cfg.user_transient_fields:
+                setattr(u, key, value)
+        u.valid = True
+        u.create_or_update(True)
+        if verbose: request.log("interwiki: successful auth for %r" % name)
+        return u, True, None, None
--- a/MoinMoin/auth/ldap_login.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/ldap_login.py	Fri Apr 20 15:35:14 2007 +0200
@@ -4,7 +4,11 @@
 
     This code only creates a user object, the session has to be established by
     the auth.moin_session auth plugin.
-    
+
+    TODO: migrate configuration items to constructor parameters,
+          allow more configuration (alias name, ...) by using
+          callables as parameters
+
     @copyright: 2006 MoinMoin:ThomasWaldmann, Nick Phillips
     @license: GNU GPL, see COPYING for details.
 """
@@ -12,126 +16,124 @@
 import ldap
 
 from MoinMoin import user
+from MoinMoin.auth import BaseAuth
 
-def ldap_login(request, **kw):
+class LDAPAuth(BaseAuth):
     """ get authentication data from form, authenticate against LDAP (or Active Directory),
         fetch some user infos from LDAP and create a user profile for that user that must
         be used by subsequent auth plugins (like moin_cookie) as we never return a user
         object from ldap_login.
     """
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-
-    cfg = request.cfg
-    verbose = cfg.ldap_verbose
-
-    if verbose: request.log("got name=%s login=%r logout=%r" % (username, login, logout))
-
-    # we just intercept login for ldap, other requests have to be
-    # handled by another auth handler
-    if not login:
-        return user_obj, True
-
-    # we require non-empty password as ldap bind does a anon (not password
-    # protected) bind if the password is empty and SUCCEEDS!
-    if not password:
-        return None, False
-
-    try:
-        try:
-            u = None
-            dn = None
-            coding = cfg.ldap_coding
-            if verbose: request.log("LDAP: Setting misc. options...")
-            # needed for Active Directory:
-            ldap.set_option(ldap.OPT_REFERRALS, 0)
-
-            server = cfg.ldap_uri
-            if server.startswith('ldaps:'):
-                # this is needed for self-signed ssl certs:
-                ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
-                # more stuff to try:
-                #ldap.set_option(ldap.OPT_X_TLS_ALLOW, 1)
-                #ldap.set_option(ldap.OPT_X_TLS_CERTFILE, LDAP_CACERTFILE)
-                #ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,'/etc/httpd/ssl.crt/myCA-cacerts.pem')
-
-            if verbose: request.log("LDAP: Trying to initialize %s." % server)
-            l = ldap.initialize(server)
-            if verbose: request.log("LDAP: Connected to LDAP server %s." % server)
-
-            # you can use %(username)s and %(password)s here to get the stuff entered in the form:
-            ldap_binddn = cfg.ldap_binddn % locals()
-            ldap_bindpw = cfg.ldap_bindpw % locals()
-            l.simple_bind_s(ldap_binddn.encode(coding), ldap_bindpw.encode(coding))
-            if verbose: request.log("LDAP: Bound with binddn %s" % ldap_binddn)
 
-            # you can use %(username)s here to get the stuff entered in the form:
-            filterstr = cfg.ldap_filter % locals()
-            if verbose: request.log("LDAP: Searching %s" % filterstr)
-            lusers = l.search_st(cfg.ldap_base, cfg.ldap_scope, filterstr.encode(coding),
-                                 attrlist=[cfg.ldap_email_attribute,
-                                           cfg.ldap_aliasname_attribute,
-                                           cfg.ldap_surname_attribute,
-                                           cfg.ldap_givenname_attribute,
-                                 ], timeout=cfg.ldap_timeout)
-            # we remove entries with dn == None to get the real result list:
-            lusers = [(dn, ldap_dict) for dn, ldap_dict in lusers if dn is not None]
-            if verbose:
-                for dn, ldap_dict in lusers:
-                    request.log("LDAP: dn:%s" % dn)
-                    for key, val in ldap_dict.items():
-                        request.log("    %s: %s" % (key, val))
-
-            result_length = len(lusers)
-            if result_length != 1:
-                if result_length > 1:
-                    request.log("LDAP: Search found more than one (%d) matches for %s." % (result_length, filterstr))
-                if result_length == 0:
-                    if verbose: request.log("LDAP: Search found no matches for %s." % (filterstr, ))
-                return None, False # if ldap returns unusable results, we veto the user and don't let him in
-
-            dn, ldap_dict = lusers[0]
-            if verbose: request.log("LDAP: DN found is %s, trying to bind with pw" % dn)
-            l.simple_bind_s(dn, password.encode(coding))
-            if verbose: request.log("LDAP: Bound with dn %s (username: %s)" % (dn, username))
-
-            email = ldap_dict.get(cfg.ldap_email_attribute, [''])[0]
-            email = email.decode(coding)
+    login_inputs = ['username', 'password']
+    logout_possible = True
+    name = 'ldap'
 
-            aliasname = ''
-            try:
-                aliasname = ldap_dict[cfg.ldap_aliasname_attribute][0]
-            except (KeyError, IndexError):
-                sn = ldap_dict.get(cfg.ldap_surname_attribute, [''])[0]
-                gn = ldap_dict.get(cfg.ldap_givenname_attribute, [''])[0]
-                if sn and gn:
-                    aliasname = "%s, %s" % (sn, gn)
-                elif sn:
-                    aliasname = sn
-            aliasname = aliasname.decode(coding)
+    def login(self, request, user_obj, **kw):
+        username = kw.get('username')
+        password = kw.get('password')
 
-            u = user.User(request, auth_username=username, password="{SHA}NotStored", auth_method='ldap', auth_attribs=('name', 'password', 'email', 'mailto_author',))
-            u.name = username
-            u.aliasname = aliasname
-            u.email = email
-            u.remember_me = 0 # 0 enforces cookie_lifetime config param
-            if verbose: request.log("LDAP: creating userprefs with name %s email %s alias %s" % (username, email, aliasname))
-
-        except ldap.INVALID_CREDENTIALS, err:
-            request.log("LDAP: invalid credentials (wrong password?) for dn %s (username: %s)" % (dn, username))
-            return None, False # if ldap says no, we veto the user and don't let him in
+        cfg = request.cfg
+        verbose = cfg.ldap_verbose
 
-        if u:
-            u.create_or_update(True)
-        return u, True # moin_session has to set the cookie
+        # we require non-empty password as ldap bind does a anon (not password
+        # protected) bind if the password is empty and SUCCEEDS!
+        if not password:
+            return user_obj, True, None, None
 
-    except:
-        import traceback
-        info = sys.exc_info()
-        request.log("LDAP: caught an exception, traceback follows...")
-        request.log(''.join(traceback.format_exception(*info)))
-        return None, False # something went completely wrong, in doubt we veto the login
+        try:
+            try:
+                u = None
+                dn = None
+                coding = cfg.ldap_coding
+                if verbose: request.log("LDAP: Setting misc. options...")
+                # needed for Active Directory:
+                ldap.set_option(ldap.OPT_REFERRALS, 0)
 
+                server = cfg.ldap_uri
+                if server.startswith('ldaps:'):
+                    # TODO: refactor into LDAPAuth() constructor arguments!
+                    # this is needed for self-signed ssl certs:
+                    ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
+                    # more stuff to try:
+                    #ldap.set_option(ldap.OPT_X_TLS_ALLOW, 1)
+                    #ldap.set_option(ldap.OPT_X_TLS_CERTFILE, LDAP_CACERTFILE)
+                    #ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,'/etc/httpd/ssl.crt/myCA-cacerts.pem')
+
+                if verbose: request.log("LDAP: Trying to initialize %s." % server)
+                l = ldap.initialize(server)
+                if verbose: request.log("LDAP: Connected to LDAP server %s." % server)
+
+                # you can use %(username)s and %(password)s here to get the stuff entered in the form:
+                ldap_binddn = cfg.ldap_binddn % locals()
+                ldap_bindpw = cfg.ldap_bindpw % locals()
+                l.simple_bind_s(ldap_binddn.encode(coding), ldap_bindpw.encode(coding))
+                if verbose: request.log("LDAP: Bound with binddn %s" % ldap_binddn)
+
+                # you can use %(username)s here to get the stuff entered in the form:
+                filterstr = cfg.ldap_filter % locals()
+                if verbose: request.log("LDAP: Searching %s" % filterstr)
+                lusers = l.search_st(cfg.ldap_base, cfg.ldap_scope, filterstr.encode(coding),
+                                     attrlist=[cfg.ldap_email_attribute,
+                                               cfg.ldap_aliasname_attribute,
+                                               cfg.ldap_surname_attribute,
+                                               cfg.ldap_givenname_attribute,
+                                     ], timeout=cfg.ldap_timeout)
+                # we remove entries with dn == None to get the real result list:
+                lusers = [(dn, ldap_dict) for dn, ldap_dict in lusers if dn is not None]
+                if verbose:
+                    for dn, ldap_dict in lusers:
+                        request.log("LDAP: dn:%s" % dn)
+                        for key, val in ldap_dict.items():
+                            request.log("    %s: %s" % (key, val))
+
+                result_length = len(lusers)
+                if result_length != 1:
+                    if result_length > 1:
+                        request.log("LDAP: Search found more than one (%d) matches for %s." % (result_length, filterstr))
+                    if result_length == 0:
+                        if verbose: request.log("LDAP: Search found no matches for %s." % (filterstr, ))
+                    return None, False, None # if ldap returns unusable results, we veto the user and don't let him in
+
+                dn, ldap_dict = lusers[0]
+                if verbose: request.log("LDAP: DN found is %s, trying to bind with pw" % dn)
+                l.simple_bind_s(dn, password.encode(coding))
+                if verbose: request.log("LDAP: Bound with dn %s (username: %s)" % (dn, username))
+
+                email = ldap_dict.get(cfg.ldap_email_attribute, [''])[0]
+                email = email.decode(coding)
+
+                aliasname = ''
+                try:
+                    aliasname = ldap_dict[cfg.ldap_aliasname_attribute][0]
+                except (KeyError, IndexError):
+                    sn = ldap_dict.get(cfg.ldap_surname_attribute, [''])[0]
+                    gn = ldap_dict.get(cfg.ldap_givenname_attribute, [''])[0]
+                    if sn and gn:
+                        aliasname = "%s, %s" % (sn, gn)
+                    elif sn:
+                        aliasname = sn
+                aliasname = aliasname.decode(coding)
+
+                u = user.User(request, auth_username=username, password="{SHA}NotStored", auth_method=self.name, auth_attribs=('name', 'password', 'email', 'mailto_author',))
+                u.name = username
+                u.aliasname = aliasname
+                u.email = email
+                u.remember_me = 0 # 0 enforces cookie_lifetime config param
+                if verbose: request.log("LDAP: creating userprefs with name %s email %s alias %s" % (username, email, aliasname))
+
+            except ldap.INVALID_CREDENTIALS, err:
+                request.log("LDAP: invalid credentials (wrong password?) for dn %s (username: %s)" % (dn, username))
+                return None, False, None, None # if ldap says no, we veto the user and don't let him in
+
+            if u:
+                u.create_or_update(True)
+            return u, True, None, None
+
+        except:
+            import traceback
+            info = sys.exc_info()
+            request.log("LDAP: caught an exception, traceback follows...")
+            request.log(''.join(traceback.format_exception(*info)))
+            return None, False, None, None # something went completely wrong, in doubt we veto the login
+
--- a/MoinMoin/auth/log.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/log.py	Fri Apr 20 15:35:14 2007 +0200
@@ -9,13 +9,23 @@
     @license: GNU GPL, see COPYING for details.
 """
 
-def log(request, **kw):
+from MoinMoin.auth import BaseAuth
+
+class AuthLog(BaseAuth):
     """ just log the call, do nothing else """
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-    request.log("auth.log: name=%s login=%r logout=%r user_obj=%r" % (username, login, logout, user_obj))
-    return user_obj, True
+    name = "log"
 
+    def log(self, request, action, user_obj, kw):
+        request.log('auth.log: %s: user_obj=%r kw=%r' % (action, user_obj, kw))
+
+    def login(self, request, user_obj, **kw):
+        self.log(request, 'login', user_obj, kw)
+        return user_obj, True, None, None
+
+    def request(self, request, user_obj, **kw):
+        self.log(request, 'session', user_obj, kw)
+        return user_obj, True
+
+    def logout(self, request, user_obj, **kw):
+        self.log(request, 'logout', user_obj, kw)
+        return user_obj, True
--- a/MoinMoin/auth/mysql_group.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/mysql_group.py	Fri Apr 20 15:35:14 2007 +0200
@@ -3,67 +3,74 @@
     MoinMoin - auth plugin doing a check against MySQL group db
 
     @copyright: 2006 Nick Phillips
+                2007 MoinMoin:JohannesBerg
     @license: GNU GPL, see COPYING for details.
 """
 
 import MySQLdb
+from MoinMoin.auth import BaseAuth
 
-def mysql_group(request, **kw):
+class MysqlGroupAuth(BaseAuth):
     """ Authorize via MySQL group DB.
     
-    We require an already-authenticated user_obj.
-    We don't worry about the type of request (login, logout, neither).
-    We just check user is part of authorized group.
+    We require an already-authenticated user_obj and
+    check that the user is part of an authorized group.
     """
-
-    username = kw.get('name')
-#    login = kw.get('login')
-#    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
+    def __init__(self, host, user, passwd, dbname, query, verbose=False):
+        BaseAuth.__init__(self)
+        self.mysql_group_query = query
+        self.host = host
+        self.user = user
+        self.passwd = passwd
+        self.dbname = dbname
+        self.verbose = verbose
 
-    cfg = request.cfg
-    verbose = False
-
-    if hasattr(cfg, 'mysql_group_verbose'):
-        verbose = cfg.mysql_group_verbose
+    def login(self, request, user_obj, **kw):
+        verbose = False
+        _ = request.getText
 
-    if verbose: request.log("auth.mysql_group: name=%s user_obj=%r" % (username, user_obj))
+        verbose = self.verbose
 
-    # Has any other method successfully authenticated?
-    if user_obj is not None and user_obj.valid:
-        # Yes - we can do stuff!
-        if verbose: request.log("mysql_group got valid user from previous auth method, trying authz...")
-        if verbose: request.log("mysql_group got auth_username %s." % user_obj.auth_username)
+        if verbose: request.log("auth.mysql_group: user_obj=%r" % user_obj)
+
+        if not (user_obj and user_obj.valid):
+            # No other method succeeded, so we cannot authorize
+            # but maybe some following auth methods can still "fix" that.
+            if verbose: request.log("auth.mysql_group did not get valid user from previous auth method")
+            return user_obj, True, None, None
+
+        # Got a valid user object - we can do stuff!
+        if verbose:
+            request.log("auth.mysql_group got valid user (name=%s) from previous auth method" % user_obj.auth_username)
 
         # XXX Check auth_username for dodgy chars (should be none as it is authenticated, but...)
+        # shouldn't really be necessary since execute() quotes them all...
 
         # OK, now check mysql!
         try:
-            m = MySQLdb.connect(host=cfg.mysql_group_dbhost,
-                                user=cfg.mysql_group_dbuser,
-                                passwd=cfg.mysql_group_dbpass,
-                                db=cfg.mysql_group_dbname,
-                                )
+            m = MySQLdb.connect(host=self.host, user=self.user,
+                                passwd=self.passwd, db=self.dbname)
         except:
             import sys
             import traceback
             info = sys.exc_info()
-            request.log("mysql_group: authorization failed due to exception connecting to DB, traceback follows...")
+            request.log("auth.mysql_group: authorization failed due to exception connecting to DB, traceback follows...")
             request.log(''.join(traceback.format_exception(*info)))
-            return None, False
+            return None, False, None, _('Failed to connect to database.')
 
         c = m.cursor()
-        c.execute(cfg.mysql_group_query, user_obj.auth_username)
+        c.execute(self.mysql_group_query, user_obj.auth_username)
         results = c.fetchall()
         if results:
             # Checked out OK
-            if verbose: request.log("mysql_group got %d results -- authorized!" % len(results))
-            return user_obj, True # we make continuing possible, e.g. for smbmount
+            if verbose: request.log("auth.mysql_group got %d results -- authorized!" % len(results))
+            return user_obj, True, None, None # we make continuing possible, e.g. for smbmount
         else:
-            if verbose: request.log("mysql_group did not get match from DB -- not authorized")
-            return None, False
-    else:
-        # No other method succeeded, so we cannot authorize -- must fail
-        if verbose: request.log("mysql_group did not get valid user from previous auth method, cannot authorize")
-        return None, False
+            if verbose: request.log("auth.mysql_group did not get match from DB -- not authorized")
+            return None, False, None, None
 
+    # XXX do we really want this? could it be enough to check when they log in?
+    # of course then when you change the DB people who are logged in can still do stuff...
+    def request(self, request, user_obj, **kw):
+        u, cont, multi, msg = self.login(request, user_obj, **kw)
+        return u, cont
--- a/MoinMoin/auth/php_session.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/php_session.py	Fri Apr 20 15:35:14 2007 +0200
@@ -14,22 +14,25 @@
 
 import Cookie, urllib
 from MoinMoin import user
-from MoinMoin.auth import _PHPsessionParser
+from MoinMoin.auth import _PHPsessionParser, BaseAuth
 
-class php_session:
+class PHPSessionAuth(BaseAuth):
     """ PHP session cookie authentication """
+
+    name = 'php_session'
+
     def __init__(self, apps=['egw'], s_path="/tmp", s_prefix="sess_"):
         """ @param apps: A list of the enabled applications. See above for
             possible keys.
             @param s_path: The path where the PHP sessions are stored.
             @param s_prefix: The prefix of the session files.
         """
-
+        BaseAuth.__init__(self)
         self.s_path = s_path
         self.s_prefix = s_prefix
         self.apps = apps
 
-    def __call__(self, request, **kw):
+    def request(self, request, user_obj, **kw):
         def handle_egroupware(session):
             """ Extracts name, fullname and email from the session. """
             username = session['egw_session']['session_lid'].split("@", 1)[0]
@@ -46,8 +49,6 @@
 
             return dec(username), dec(email), dec(name)
 
-        user_obj = kw.get('user_obj')
-        cookie = kw.get('cookie')
         if not cookie is None:
             for cookiename in cookie:
                 cookievalue = urllib.unquote(cookie[cookiename].value).decode('iso-8859-1')
@@ -59,7 +60,8 @@
             else:
                 return user_obj, True
 
-            user = user.User(request, name=username, auth_username=username)
+            user = user.User(request, name=username, auth_username=username,
+                             auth_method=self.name)
 
             changed = False
             if name != user.aliasname:
--- a/MoinMoin/auth/smb_mount.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/smb_mount.py	Fri Apr 20 15:35:14 2007 +0200
@@ -7,47 +7,58 @@
     to files on some share via the wiki, but needs more code to be useful.
 
     @copyright: 2006 MoinMoin:ThomasWaldmann
+                2007 MoinMoin:JohannesBerg
     @license: GNU GPL, see COPYING for details.
 """
 
+from MoinMoin.auth import BaseAuth
 
-def smb_mount(request, **kw):
+class SMBMount(BaseAuth):
     """ auth plugin for (un)mounting an smb share """
-    username = kw.get('name')
-    password = kw.get('password')
-    login = kw.get('login')
-    logout = kw.get('logout')
-    user_obj = kw.get('user_obj')
-    cfg = request.cfg
-    verbose = cfg.smb_verbose
-    if verbose: request.log("got name=%s login=%r logout=%r" % (username, login, logout))
+    def __init__(self, smb_dir_user, smb_mountpoint_fn, smb_domain, smb_server,
+                 smb_share, smb_dir_mode, smb_file_mode, smb_iocharset,
+                 smb_log, smb_coding, verbose=False):
+        BaseAuth.__init__(self)
+        self.verbose = verbose
+        self.smb_dir_user = smb_dir_user
+        self.smb_mountpoint_fn = smb_mountpoint_fn
+        self.smb_domain = smb_domain
+        self.smb_server = smb_server
+        self.smb_share = smb_share
+        self.smb_dir_mode = smb_dir_mode
+        self.smb_file_mode = smb_file_mode
+        self.smb_iocharset = smb_iocharset
+        self.smb_log = smb_log
+        self.smb_coding = smb_coding
 
-    # we just intercept login to mount and logout to umount the smb share
-    if login or logout:
+    def do_smb(self, request, username, password, login):
+        verbose = self.verbose
+        if verbose: request.log("SMBMount login=%s logout=%s: got name=%s" % (login, not login, username))
+
         import os, pwd, subprocess
-        web_username = cfg.smb_dir_user
+        web_username = self.smb_dir_user
         web_uid = pwd.getpwnam(web_username)[2] # XXX better just use current uid?
-        if logout and user_obj: # logout -> we don't have username in form
-            username = user_obj.name # so we take it from previous auth method (moin_cookie e.g.)
-        mountpoint = cfg.smb_mountpoint % {
-            'username': username,
-        }
+
+        if not login: # logout -> we don't have username in form
+            username = user_obj.name # so we take it from previous auth method
+
+        mountpoint = self.smb_mountpoint_fn(username)
         if login:
             cmd = u"sudo mount -t cifs -o user=%(user)s,domain=%(domain)s,uid=%(uid)d,dir_mode=%(dir_mode)s,file_mode=%(file_mode)s,iocharset=%(iocharset)s //%(server)s/%(share)s %(mountpoint)s >>%(log)s 2>&1"
-        elif logout:
+        else:
             cmd = u"sudo umount %(mountpoint)s >>%(log)s 2>&1"
 
         cmd = cmd % {
             'user': username,
             'uid': web_uid,
-            'domain': cfg.smb_domain,
-            'server': cfg.smb_server,
-            'share': cfg.smb_share,
+            'domain': self.smb_domain,
+            'server': self.smb_server,
+            'share': self.smb_share,
             'mountpoint': mountpoint,
-            'dir_mode': cfg.smb_dir_mode,
-            'file_mode': cfg.smb_file_mode,
-            'iocharset': cfg.smb_iocharset,
-            'log': cfg.smb_log,
+            'dir_mode': self.smb_dir_mode,
+            'file_mode': self.smb_file_mode,
+            'iocharset': self.smb_iocharset,
+            'log': self.smb_log,
         }
         env = os.environ.copy()
         if login:
@@ -56,7 +67,17 @@
                     os.makedirs(mountpoint) # the dir containing the mountpoint must be writeable for us!
             except OSError:
                 pass
-            env['PASSWD'] = password.encode(cfg.smb_coding)
-        subprocess.call(cmd.encode(cfg.smb_coding), env=env, shell=True)
-    return user_obj, True
+            env['PASSWD'] = password.encode(self.smb_coding)
+        subprocess.call(cmd.encode(self.smb_coding), env=env, shell=True)
 
+    def login(self, request, user_obj, **kw):
+        username = kw.get('username')
+        password = kw.get('password')
+        if user_obj and user_obj.valid:
+            do_smb(request, username, password, True)
+        return user_obj, True, None, None
+
+    def logout(self, request, user_obj, **kw):
+        if user_obj and not user_obj.valid:
+            do_smb(request, None, None, False)
+        return user_obj, True
--- a/MoinMoin/auth/sslclientcert.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/auth/sslclientcert.py	Fri Apr 20 15:35:14 2007 +0200
@@ -12,60 +12,84 @@
 
 from MoinMoin import config, user
 from MoinMoin.request import TWISTED
+from MoinMoin.auth import BaseAuth
 
-def sslclientcert(request, **kw):
+class SSLClientCertAuth(BaseAuth):
     """ authenticate via SSL client certificate """
-    user_obj = kw.get('user_obj')
-    u = None
-    changed = False
-    # check if we are running Twisted
-    if isinstance(request, TWISTED.Request):
-        return user_obj, True # not supported if we run twisted
-        # Addendum: this seems to need quite some twisted insight and coding.
-        # A pointer i got on #twisted: divmod's vertex.sslverify
-        # If you really need this, feel free to implement and test it and
-        # submit a patch if it works.
-    else:
-        env = request.env
-        if env.get('SSL_CLIENT_VERIFY', 'FAILURE') == 'SUCCESS':
-            # if we only want to accept some specific CA, do a check like:
-            # if env.get('SSL_CLIENT_I_DN_OU') == "http://www.cacert.org"
-            email = env.get('SSL_CLIENT_S_DN_Email', '').decode(config.charset)
-            email_lower = email.lower()
-            commonname = env.get('SSL_CLIENT_S_DN_CN', '').decode(config.charset)
-            commonname_lower = commonname.lower()
-            if email_lower or commonname_lower:
-                for uid in user.getUserList(request):
-                    u = user.User(request, uid,
-                                  auth_method='sslclientcert', auth_attribs=())
-                    if email_lower and u.email.lower() == email_lower:
-                        u.auth_attribs = ('email', 'password')
-                        #this is only useful if same name should be used, as
-                        #commonname is likely no CamelCase WikiName
-                        #if commonname_lower != u.name.lower():
-                        #    u.name = commonname
-                        #    changed = True
-                        #u.auth_attribs = ('email', 'name', 'password')
-                        break
-                    if commonname_lower and u.name.lower() == commonname_lower:
+
+    name = 'sslclientcert'
+
+    def __init__(self, authorities=None,
+                 email_key=True, name_key=True,
+                 use_email=False, use_name=False):
+        self.use_email = use_email
+        self.authorities = authorities
+        self.email_key = email_key
+        self.name_key = name_key
+        self.use_email = use_email
+        self.use_name = use_name
+        BaseAuth.__init__(self)
+
+    def request(self, request, user_obj, **kw):
+        u = None
+        changed = False
+        # check if we are running Twisted
+        if isinstance(request, TWISTED.Request):
+            return user_obj, True # not supported if we run twisted
+            # Addendum: this seems to need quite some twisted insight and coding.
+            # A pointer i got on #twisted: divmod's vertex.sslverify
+            # If you really need this, feel free to implement and test it and
+            # submit a patch if it works.
+        else:
+            env = request.env
+            if env.get('SSL_CLIENT_VERIFY', 'FAILURE') == 'SUCCESS':
+
+                # check authority list if given
+                if self.authorities and env.get('SSL_CLIENT_I_DN_OU') in self.authorities:
+                    return user_obj, True
+
+                email_lower = None
+                if self.email_key:
+                    email = env.get('SSL_CLIENT_S_DN_Email', '').decode(config.charset)
+                    email_lower = email.lower()
+                commonname_lower = None
+                if self.name_key:
+                    commonname = env.get('SSL_CLIENT_S_DN_CN', '').decode(config.charset)
+                    commonname_lower = commonname.lower()
+                if email_lower or commonname_lower:
+                    for uid in user.getUserList(request):
+                        u = user.User(request, uid,
+                                      auth_method=self.name, auth_attribs=())
+                        if self.email_key and email_lower and u.email.lower() == email_lower:
+                            u.auth_attribs = ('email', 'password')
+                            if self.use_name and commonname_lower != u.name.lower():
+                                u.name = commonname
+                                changed = True
+                                u.auth_attribs = ('email', 'name', 'password')
+                            break
+                        if self.name_key and commonname_lower and u.name.lower() == commonname_lower:
+                            u.auth_attribs = ('name', 'password')
+                            if self.use_email and email_lower != u.email.lower():
+                                u.email = email
+                                changed = True
+                                u.auth_attribs = ('name', 'email', 'password')
+                            break
+                    else:
+                        u = None
+                    if u is None:
+                        # user wasn't found, so let's create a new user object
+                        u = user.User(request, name=commonname_lower, auth_username=commonname_lower,
+                                      auth_method=self.name)
                         u.auth_attribs = ('name', 'password')
-                        #this is only useful if same email should be used as
-                        #specified in certificate.
-                        #if email_lower != u.email.lower():
-                        #    u.email = email
-                        #    changed = True
-                        #u.auth_attribs = ('name', 'email', 'password')
-                        break
-                else:
-                    u = None
-                if u is None:
-                    # user wasn't found, so let's create a new user object
-                    u = user.User(request, name=commonname_lower, auth_username=commonname_lower)
-
-    if u:
-        u.create_or_update(changed)
-    if u and u.valid:
-        return u, True
-    else:
-        return user_obj, True
-
+                        if self.use_email:
+                            u.email = email
+                            u.auth_attribs = ('name', 'email', 'password')
+            elif user_obj and user_obj.auth_method == self.name:
+                user_obj.valid = False
+                return user_obj, False
+        if u:
+            u.create_or_update(changed)
+        if u and u.valid:
+            return u, True
+        else:
+            return user_obj, True
--- a/MoinMoin/config/multiconfig.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/config/multiconfig.py	Fri Apr 20 15:35:14 2007 +0200
@@ -14,6 +14,7 @@
 
 from MoinMoin import config, error, util, wikiutil
 import MoinMoin.auth as authmodule
+from MoinMoin import session
 from MoinMoin.packages import packLine
 from MoinMoin.security import AccessControlList
 
@@ -138,6 +139,25 @@
     'err': err,
 }
         raise error.ConfigurationError(msg)
+
+    # postprocess configuration
+    # 'setuid' special auth method auth method can log out
+    cfg.auth_can_logout = ['setuid']
+    cfg.auth_login_inputs = []
+    found_names = []
+    for auth in cfg.auth:
+        if not auth.name:
+            raise error.ConfigurationError("Auth methods must have a name.")
+        if auth.name in found_names:
+            raise error.ConfigurationError("Auth method names must be unique.")
+        found_names.append(auth.name)
+        if auth.logout_possible and auth.name:
+            cfg.auth_can_logout.append(auth.name)
+        for input in auth.login_inputs:
+            if not input in cfg.auth_login_inputs:
+                cfg.auth_login_inputs.append(input)
+    cfg.auth_have_login = len(cfg.auth_login_inputs) > 0
+
     return cfg
 
 
@@ -197,7 +217,8 @@
     allow_xslt = False
     antispam_master_url = "http://moinmaster.wikiwikiweb.de:8000/?action=xmlrpc2"
     attachments = None # {'dir': path, 'url': url-prefix}
-    auth = [authmodule.moin_login, authmodule.moin_session, ]
+    auth = [authmodule.MoinLogin()]
+    session_handler = session.DefaultSessionHandler()
 
     backup_compression = 'gz'
     backup_users = []
@@ -403,7 +424,6 @@
     shared_intermap = None # can be string or list of strings (filenames)
     show_hosts = True
     show_interwiki = False
-    show_login = True
     show_names = True
     show_section_numbers = 0
     show_timings = False
--- a/MoinMoin/request/__init__.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/request/__init__.py	Fri Apr 20 15:35:14 2007 +0200
@@ -157,10 +157,6 @@
         self.page = None
         self._dicts = None
 
-        # session handling. users cannot rely on a session being
-        # created, but we should always set request.session
-        self.session = {}
-
         # setuid handling requires an attribute in the request
         # that stores the real user
         self._setuid_real_user = None
@@ -221,13 +217,24 @@
             lang = i18n.requestLanguage(self, try_user=False)
             self.getText = lambda text, i18n=self.i18n, request=self, lang=lang, **kv: i18n.getText(text, request, lang, kv.get('formatted', True))
 
-            self.user = self.get_user_from_form()
-            # setuid handling
-            if self.session and 'setuid' in self.session:
+            # session handler start, auth
+            self.parse_cookie()
+            user_obj = self.cfg.session_handler.start(self, self._cookie)
+            shfinisher = lambda request: self.cfg.session_handler.finish(request, self._cookie, request.user)
+            self.add_finisher(shfinisher)
+            self.user = self._handle_auth_form(user_obj)
+            del user_obj
+            self.cfg.session_handler.after_auth(self, self._cookie, self.user)
+            if not self.user:
+                self.user = user.User(self, auth_method='request:invalid')
+
+            # setuid handling, check isSuperUser() because the user
+            # might have lost the permission between requests
+            if 'setuid' in self.session and self.user.isSuperUser():
                 self._setuid_real_user = self.user
                 uid = self.session['setuid']
-                self.user = user.User(self, uid)
-                self.user.disabled = None
+                self.user = user.User(self, uid, auth_method='setuid')
+                self.user.disabled = False
 
             if self.action != 'xmlrpc':
                 if not self.forbidden and self.isForbidden():
@@ -582,45 +589,65 @@
             path, query = uri, ''
         return wikiutil.url_unquote(path, want_unicode=False), query
 
-    def get_user_from_form(self):
-        """ read the maybe present UserPreferences form and call get_user with the values """
-        name = self.form.get('name', [None])[0]
+    def _handle_auth_form(self, user_obj):
+        username = self.form.get('name', [None])[0]
         password = self.form.get('password', [None])[0]
         login = 'login' in self.form
         logout = 'logout' in self.form
-        u = self.get_user_default_unknown(name=name, password=password,
-                                          login=login, logout=logout,
-                                          user_obj=None)
-        return u
+        stage = self.form.get('stage', [None])[0]
+        return self.handle_auth(user_obj, username=username, password=password,
+                                login=login, logout=logout, stage=stage)
+
+    def handle_auth(self, user_obj, **kw):
+        username = kw.get('username')
+        password = kw.get('password')
+        login = kw.get('login')
+        logout = kw.get('logout')
+        stage = kw.get('stage')
+        extra = {}
+        if login:
+            extra['username'] = username
+            extra['password'] = password
+            if stage:
+                extra['multistage'] = True
+        login_msgs = []
+        self._login_multistage = None
+
+        if logout and 'setuid' in self.session:
+            del self.session['setuid']
+            return user_obj
+
+        for auth in self.cfg.auth:
+            if logout:
+                user_obj, cont = auth.logout(self, user_obj, **extra)
+            elif login:
+                if stage and auth.name != stage:
+                    continue
+                user_obj, cont, multistage, msg = auth.login(self,
+                                                             user_obj,
+                                                             **extra)
+                if stage:
+                    stage = None
+                    del extra['multistage']
+                if multistage:
+                    self._login_multistage = multistage
+                    self._login_multistage_name = auth.name
+                    return user_obj
+                if msg and not msg in login_msgs:
+                    login_msgs.append(msg)
+            else:
+                user_obj, cont = auth.request(self, user_obj, **extra)
+            if not cont:
+                break
+
+        self._login_messages = login_msgs
+        return user_obj
 
     def parse_cookie(self):
         try:
-            return Cookie.SimpleCookie(self.saved_cookie)
+            self._cookie = Cookie.SimpleCookie(self.saved_cookie)
         except Cookie.CookieError:
-            return None
-
-    def get_user_default_unknown(self, **kw):
-        """ call do_auth and if it doesnt return a user object, make some "Unknown User" """
-        user_obj = self.get_user_default_None(**kw)
-        if user_obj is None:
-            user_obj = user.User(self, auth_method="request:427")
-        return user_obj
-
-    def get_user_default_None(self, **kw):
-        """ loop over auth handlers, return a user obj or None """
-        name = kw.get('name')
-        password = kw.get('password')
-        login = kw.get('login')
-        logout = kw.get('logout')
-        user_obj = kw.get('user_obj')
-        cookie = self.parse_cookie()
-        for auth in self.cfg.auth:
-            user_obj, continue_flag = auth(self, name=name, password=password,
-                                           login=login, logout=logout, user_obj=user_obj,
-                                           cookie=cookie)
-            if not continue_flag:
-                break
-        return user_obj
+            self._cookie = None
 
     def reset(self):
         """ Reset request state.
@@ -1034,6 +1061,7 @@
 
     def makeForbidden(self, resultcode, msg):
         statusmsg = {
+            401: 'Authorization required',
             403: 'FORBIDDEN',
             404: 'Not found',
             503: 'Service unavailable',
@@ -1122,18 +1150,8 @@
                 self.setHttpHeader("Vary: Cookie,User-Agent,Accept-Language")
 
             # Handle request. We have these options:
-            # 1. If user has a bad user name, delete its bad cookie and
-            # send him to UserPreferences to make a new account.
-            if not user.isValidName(self, self.user.name):
-                msg = _("""Invalid user name {{{'%s'}}}.
-Name may contain any Unicode alpha numeric character, with optional one
-space between words. Group page name is not allowed.""") % self.user.name
-                self.user = self.get_user_default_unknown(name=self.user.name, logout=True)
-                page = wikiutil.getLocalizedPage(self, 'UserPreferences')
-                page.send_page(msg=msg)
-
-            # 2. Or jump to page where user left off
-            elif not pagename and self.user.remember_last_visit:
+            # 1. jump to page where user left off
+            if not pagename and self.user.remember_last_visit:
                 pagetrail = self.user.getTrail()
                 if pagetrail:
                     # Redirect to last page visited
@@ -1148,7 +1166,7 @@
                 self.http_redirect(url)
                 return self.finish()
 
-            # 3. Or handle action
+            # 2. handle action
             else:
                 # pagename could be empty after normalization e.g. '///' -> ''
                 # Use localized FrontPage if pagename is empty
@@ -1188,12 +1206,13 @@
                     else:
                         handler(self.page.page_name, self)
 
-            # every action that didn't use to raise MoinMoinNoFooter must call this now:
+            # every action that didn't use to raise MoinMoinFinish must call this now:
             # self.theme.send_closing_html()
 
         except MoinMoinFinish:
             pass
         except Exception, err:
+            self.finish()
             self.fail(err)
 
         return self.finish()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/session.py	Fri Apr 20 15:35:14 2007 +0200
@@ -0,0 +1,397 @@
+"""
+    MoinMoin - session handling
+
+    Session handling in MoinMoin is done mostly by the request
+    with help from a SessionHandler instance (see below.)
+
+
+    @copyright: 2007      MoinMoin:JohannesBerg
+
+    @license: GNU GPL, see COPYING for details.
+"""
+
+import Cookie
+from MoinMoin import caching
+from MoinMoin.user import User
+import random
+import time
+
+class SessionData(object):
+    """
+        MoinMoin session data base class
+
+        An object of this class must be assigned to
+        request.session by the SessionHandler's start
+        method.
+
+        Instances conform to the dict protocol (__setitem__, __getitem__,
+        __contains__, __delitem__, get) and have the additional methods
+        is_stored and is_new.
+    """
+    def __init__(self, request):
+        self.is_stored = False
+        self.is_new = True
+        self.request = request
+
+    def __setitem__(self, name, value):
+        raise NotImplementedError
+
+    def __getitem__(self, name):
+        raise NotImplementedError
+
+    def __contains__(self, name):
+        raise NotImplementedError
+
+    def __delitem__(self, name):
+        raise NotImplementedError
+
+    def get(self, name, default=None):
+        raise NotImplementedError
+
+
+class DefaultSessionData(SessionData):
+    """ DefaultSessionData -- session data for DefaultSessionHandler
+
+    If you wish to override just the session storage then you can
+    inherit from this class, implement all methods and assign the
+    class to the dataclass keyword parameter to the DefaultSessionHandler
+    constructor.
+
+    Newly created objects should have be marked as expiring right away
+    until set_expiry() is called.
+    """
+    def __init__(self, request, name):
+        """create session object
+
+        @param request: the request
+        @param name: the session name
+        """
+        SessionData.__init__(self, request)
+        self.name = name
+
+    def set_expiry(self, expires):
+        """reset expiry for this session object"""
+        raise NotImplementedError
+
+    def delete(self):
+        """clear session data and remove from it storage"""
+        raise NotImplementedError
+
+class CacheSessionData(DefaultSessionData):
+    """ SessionData -- store data for a session
+
+    This stores session data in memory and also maintains a cache of it on
+    disk, so the same data will be loaded from disk cache in the next request
+    of the same session.
+
+    Once in a while, expired session's cache files will be automatically cleaned up.
+    """
+    def __init__(self, request, name):
+        DefaultSessionData.__init__(self, request, name)
+
+        # we can use farm scope since the session name is totally random
+        # this means that the session is kept over multiple wikis in a farm
+        # when they share user_dir and cookies
+        self._ce = caching.CacheEntry(request, 'session', name, 'farm',
+                                      use_pickle=True)
+        try:
+            self._data = self._ce.content()
+            if self['expires'] <= time.time():
+                self._ce.remove()
+                self._data = {'expires': 0}
+        except caching.CacheError:
+            self._data = {'expires': 0}
+
+    def __setitem__(self, name, value):
+        self._data[name] = value
+        if len(self._data) > 1 and self['expires'] > time.time():
+            self._ce.update(self._data)
+
+    def __getitem__(self, name):
+        return self._data[name]
+
+    def __contains__(self, name):
+        return name in self._data
+
+    def __delitem__(self, name):
+        del self._data[name]
+        if len(self._data) <= 1:
+            self._ce.remove()
+        elif self['expires'] > time.time():
+            self._ce.update(self._data)
+
+    def get(self, name, default=None):
+        return self._data.get(name, default)
+
+    def set_expiry(self, expires):
+        # Set 'expires' an hour later than it should actually expire.
+        # That way, the expiry code will delete the item an hour later
+        # than it has actually expired, but that is acceptable and we
+        # don't need to update the file all the time
+        if expires and self['expires'] < expires:
+            self['expires'] = expires + 3600
+
+    def delete(self):
+        try:
+            self._ce.remove()
+        except caching.CacheError:
+            pass
+        self._data = {'expires': 0}
+
+
+def cleanup_session_data_cache(request):
+    cachelist = caching.get_cache_list(request, 'session', 'farm')
+    tnow = time.time()
+    for name in cachelist:
+        entry = caching.CacheEntry(request, 'session', name, 'farm',
+                                   use_pickle=True)
+        try:
+            data = entry.content()
+            if 'expires' in data and data['expires'] < tnow:
+                entry.remove()
+        except caching.CacheError:
+            pass
+
+
+class SessionHandler(object):
+    """
+        MoinMoin session handler base class
+
+        SessionHandler is an abstract method defining the interface
+        to a session handler object.
+
+        Session handling in MoinMoin works as follows:
+
+        When a request is received, first the cookie is read into a
+        Cookie.SimpleCookie instance, this is passed to the selected
+        session handler's (cfg.session_handler) start method (see below)
+        which must return a MoinMoin.user.User instance (or None).
+
+        Then, all authentication methods are called with this user object,
+        they can modify it or return a different one.
+
+        After they have changed the user object suitably, the session
+        handler's after_auth method is invoked to set the cookie.
+
+        Then, the request is executed and finally the session handler's
+        finish method is invoked.
+    """
+    def __init__(self):
+        """
+           Session handler initialisation
+
+           Only provided for future compatibility.
+        """
+        pass
+
+    def start(self, request, cookie):
+        """
+           Session handler start
+
+           Invoked very early during request handling to preload
+           a user object from the session (if any.)
+           This method must also assign to request.session an object
+           derived from SessionDataInterface.
+
+           @param request: the request instance
+           @param cookie: a Cookie.SimpleCookie with the request cookie
+           @return: a MoinMoin.user.User instance or None
+        """
+        raise NotImplementedError
+
+    def after_auth(self, request, cookie, user_obj):
+        """
+           Session handler auth chain callback
+
+           Invoked after all auth items have run (or multistage was
+           requested by one), but before the request is actually
+           handled and output is made. Should set the cookie.
+
+           @param request: the request instance
+           @param cookie: a Cookie.SimpleCookie with the request cookie
+           @param user_obj: the user object returned from the auth methods
+                            (or None)
+        """
+        raise NotImplementedError
+
+    def finish(self, request, cookie, user_obj):
+        """
+           Session handler request finish callback
+
+           Invoked after the request is completely finished.
+
+           @param request: the request instance
+           @param cookie: a Cookie.SimpleCookie with the request cookie
+           @param user_obj: the user object that was used in this request
+        """
+        raise NotImplementedError
+
+_MOIN_SESSION = 'MOIN_SESSION'
+
+_SECURITY_STRING_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_-'
+
+def _generate_security_string(length):
+    """ generate a random length (length/2 .. length)
+        string with random content
+
+        @param length: the maximum length
+        @return: the random string
+    """
+    random_length = random.randint(length/2, length)
+    return ''.join([random.choice(_SECURITY_STRING_CHARS)
+                    for i in range(random_length)])
+
+
+def _make_cookie(request, cookie_name, cookie_string, maxage, expires):
+    """ create an appropriate cookie """
+    cookie = Cookie.SimpleCookie()
+    cfg = request.cfg
+    cookie[cookie_name] = cookie_string
+    cookie[cookie_name]['max-age'] = maxage
+    if cfg.cookie_domain:
+        cookie[cookie_name]['domain'] = cfg.cookie_domain
+    if cfg.cookie_path:
+        cookie[cookie_name]['path'] = cfg.cookie_path
+    else:
+        path = request.getScriptname()
+        if not path:
+            path = '/'
+        cookie[cookie_name]['path'] = path
+    # Set expires for older clients
+    cookie[cookie_name]['expires'] = request.httpDate(when=expires, rfc='850')
+    return cookie.output()
+
+
+def _get_cookie_lifetime(request, user_obj):
+    """ Get cookie lifetime for the user object user_obj """
+    lifetime = int(request.cfg.cookie_lifetime) * 3600
+    forever = 10 * 365 * 24 * 3600 # 10 years
+    if not lifetime:
+        return forever
+    elif lifetime > 0:
+        if user_obj.remember_me:
+            return forever
+        return lifetime
+    elif lifetime < 0:
+        return -lifetime
+    return lifetime
+
+
+def _set_cookie(request, cookie_string, maxage, expires):
+    """ Set cookie, raw helper. """
+    cookie = _make_cookie(request, _MOIN_SESSION, cookie_string,
+                          maxage, expires)
+    # Set cookie
+    request.setHttpHeader(cookie)
+    # IMPORTANT: Prevent caching of current page and cookie
+    request.disableHttpCaching()
+
+def _delete_cookie(request):
+    """ Delete the user cookie by sending expired cookie with null value
+
+    According to http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.2
+    Deleted cookie should have Max-Age=0. We also have expires attribute,
+    which is probably needed for older browsers.
+
+    Finally, delete the saved cookie and create a new user based on the new settings.
+    """
+    cookie_string = ''
+    maxage = 0
+    # Set expires to one year ago for older clients
+    expires = time.time() - 3600 * 24 * 365 # 1 year ago
+    cookie = _make_cookie(request, _MOIN_SESSION, cookie_string,
+                          maxage, expires)
+    # Set cookie
+    request.setHttpHeader(cookie)
+    # IMPORTANT: Prevent caching of current page and cookie
+    request.disableHttpCaching()
+
+
+def _set_session_cookie(request, session_name, lifetime):
+    """ Set moin_session cookie """
+    expires = time.time() + lifetime
+    request.session.set_expiry(expires)
+    _set_cookie(request, session_name, lifetime, expires)
+
+
+def _get_session_name(cookie):
+    session_name = None
+    if _MOIN_SESSION in cookie:
+        session_name = cookie[_MOIN_SESSION].value
+        session_name = ''.join([c for c in session_name
+                                if c in _SECURITY_STRING_CHARS])
+    return session_name
+
+
+def _set_anon_cookie(request, session_name):
+    if hasattr(request.cfg, 'anonymous_cookie_lifetime'):
+        lifetime = request.cfg.anonymous_cookie_lifetime * 3600
+        _set_session_cookie(request, session_name, lifetime)
+
+
+class DefaultSessionHandler(SessionHandler):
+    """MoinMoin default session handler
+
+    This session handler uses the MOIN_SESSION cookie and a configurable
+    session data class.
+    """
+    def __init__(self, dataclass=CacheSessionData):
+        """DefaultSessionHandler constructor
+
+        @param dataclass: class derived from DefaultSessionData or a callable
+                          that takes parameters (request, name, expires)
+                          and returns a DefaultSessionData instance.
+        """
+        SessionHandler.__init__(self)
+        self.dataclass = dataclass
+
+    def start(self, request, cookie):
+        user_obj = None
+        session_name = _get_session_name(cookie)
+        if session_name:
+            sessiondata = self.dataclass(request, session_name)
+            sessiondata.is_new = False
+            sessiondata.is_stored = True
+            request.session = sessiondata
+            if 'user.id' in sessiondata:
+                uid = sessiondata['user.id']
+                method = sessiondata['user.auth_method']
+                # Only allow valid methods that are still in the auth list.
+                # This is necessary to kick out clients who authenticated in
+                # the past # with a method that was removed from the auth
+                # list!
+                if method:
+                    for auth in request.cfg.auth:
+                        if auth.name == method:
+                            user_obj = User(request, id=uid,
+                                            auth_method=method)
+                            if user_obj:
+                                sessiondata.is_stored = True
+            else:
+                store = hasattr(request.cfg, 'anonymous_cookie_lifetime')
+                sessiondata.is_stored = store
+        else:
+            session_name = _generate_security_string(32)
+            store = hasattr(request.cfg, 'anonymous_cookie_lifetime')
+            sessiondata = self.dataclass(request, session_name)
+            sessiondata.is_new = True
+            sessiondata.is_stored = store
+            request.session = sessiondata
+        return user_obj
+
+    def after_auth(self, request, cookie, user_obj):
+        session = request.session
+        if user_obj and user_obj.valid:
+            session['user.id'] = user_obj.id
+            session['user.auth_method'] = user_obj.auth_method
+            lifetime = _get_cookie_lifetime(request, user_obj)
+            _set_session_cookie(request, session.name, lifetime)
+        else:
+            if 'user.id' in session:
+                session.delete()
+            _set_anon_cookie(request, session.name)
+
+    def finish(self, request, cookie, user_obj):
+        # every once a while, clean up deleted sessions:
+        if random.randint(0, 999) == 0:
+            cleanup_session_data_cache(request)
--- a/MoinMoin/theme/__init__.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/theme/__init__.py	Fri Apr 20 15:35:14 2007 +0200
@@ -277,11 +277,12 @@
             userlinks.append(d['page'].link_to(request, text=_('Preferences'),
                                                querystr={'action': 'userprefs'}, id='userprefs', rel='nofollow'))
 
-        if request.cfg.show_login:
-            if request.user.valid:
+        if request.user.valid:
+            if request.user.auth_method in request.cfg.auth_can_logout:
                 userlinks.append(d['page'].link_to(request, text=_('Logout', formatted=False),
                                                    querystr={'action': 'logout', 'logout': 'logout'}, id='logout', rel='nofollow'))
-            else:
+        else:
+            if request.cfg.auth_have_login:
                 userlinks.append(d['page'].link_to(request, text=_("Login", formatted=False),
                                                    querystr={'action': 'login'}, id='login', rel='nofollow'))
 
--- a/MoinMoin/userform.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/userform.py	Fri Apr 20 15:35:14 2007 +0200
@@ -78,10 +78,7 @@
         if self.request.request_method != 'POST':
             return _("Use UserPreferences to change your settings or create an account.")
         # Create user profile
-        if form.has_key('create'):
-            theuser = self.request.get_user_from_form()
-        else:
-            theuser = user.User(self.request, auth_method="request:152")
+        theuser = user.User(self.request, auth_method="new-user")
 
         # Require non-empty name
         try:
@@ -161,13 +158,13 @@
                 self.request.user = self.request._setuid_real_user
                 self.request._setuid_real_user = None
             else:
-                theuser = user.User(self.request, uid)
+                theuser = user.User(self.request, uid, auth_method='setuid')
                 theuser.disabled = None
                 self.request.session['setuid'] = uid
                 self.request._setuid_real_user = self.request.user
                 # now continue as the other user
                 self.request.user = theuser
-            return  _("Use UserPreferences to change settings of the selected user account")
+            return  _("Use UserPreferences to change settings of the selected user account, log out to get back to your account.")
         else:
             return _("Use UserPreferences to change your settings or create an account.")
 
@@ -177,7 +174,9 @@
 
         if self.request.request_method != 'POST':
             return _("Use UserPreferences to change your settings or create an account.")
-        theuser = self.request.get_user_from_form()
+        theuser = self.request.user
+        if not theuser:
+            return
 
         if not 'name' in theuser.auth_attribs:
             # Require non-empty name
@@ -424,9 +423,15 @@
     def _user_select(self):
         options = []
         users = user.getUserList(self.request)
+        realuid = None
+        if hasattr(self.request, '_setuid_real_user') and self.request._setuid_real_user:
+            realuid = self.request._setuid_real_user.id
+        else:
+            realuid = self.request.user.id
         for uid in users:
-            name = user.User(self.request, id=uid).name # + '/' + uid # for debugging
-            options.append((name, name))
+            if uid != realuid:
+                name = user.User(self.request, id=uid).name # + '/' + uid # for debugging
+                options.append((name, name))
         options.sort()
 
         size = min(5, len(options))
@@ -681,17 +686,20 @@
         self._form.append(html.P().append(hint))
         self._form.append(html.Raw("</div>"))
 
-        self.make_row(_('Name'), [
-            html.INPUT(
-                type="text", size="32", name="name",
-            ),
-        ])
+        cfg = request.cfg
+        if 'username' in cfg.auth_login_inputs:
+            self.make_row(_('Name'), [
+                html.INPUT(
+                    type="text", size="32", name="name",
+                ),
+            ])
 
-        self.make_row(_('Password'), [
-            html.INPUT(
-                type="password", size="32", name="password",
-            ),
-        ])
+        if 'password' in cfg.auth_login_inputs:
+            self.make_row(_('Password'), [
+                html.INPUT(
+                    type="password", size="32", name="password",
+                ),
+            ])
 
         self.make_row('', [
             html.INPUT(
--- a/MoinMoin/xmlrpc/__init__.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/MoinMoin/xmlrpc/__init__.py	Fri Apr 20 15:35:14 2007 +0200
@@ -572,7 +572,8 @@
             If it succeeds, it returns a dict of items from user profile.
             If it fails, it returns a str with an error msg.
         """
-        u = self.request.get_user_default_None(name=username, password=password, login=1)
+        u = self.request.handle_auth(None, username=username,
+                                     password=password, login=True)
         if u is None:
             return "Authentication failed"
         else:
@@ -587,8 +588,9 @@
         """ Returns a token which can be used for authentication
             in other XMLRPC calls. If the token is empty, the username
             or the password were wrong. """
-        u = user.User(self.request, name=username, password=password, auth_method='xmlrpc_gettoken')
-        if u.valid:
+        u = self.request.handle_auth(None, username=username,
+                                     password=password, login=True)
+        if u and u.valid:
             return u.id
         else:
             return ""
--- a/contrib/auth_externalcookie/wikiconfig.py	Fri Apr 20 15:34:50 2007 +0200
+++ b/contrib/auth_externalcookie/wikiconfig.py	Fri Apr 20 15:35:14 2007 +0200
@@ -4,11 +4,13 @@
 # See the +++ places for customizing it to your needs. You need to put this
 # code into your farmconfig.py or wikiconfig.py.
 
-# HINT: this code is slightly outdated, if you fix it to work with 1.6, please send us a copy.
+# HINT: this code is slightly outdated, if you fix it to work with 1.7, please send us a copy.
 from MoinMoin.config.multiconfig import DefaultConfig
+from MoinMoin.auth import BaseAuth
 
-class FarmConfig(DefaultConfig):
-    def external_cookie(request, **kw):
+class ExternalCookie(BaseAuth):
+    name = 'external_cookie'
+    def request(request, user_obj, **kw):
         """ authenticate via external cookie """
         import Cookie
         user = None
@@ -44,7 +46,7 @@
 
             from MoinMoin.user import User
             # giving auth_username to User constructor means that authentication has already been done.
-            user = User(request, name=auth_username, auth_username=auth_username)
+            user = User(request, name=auth_username, auth_username=auth_username, auth_method=self.name)
 
             changed = False
             if aliasname != user.aliasname: # was the aliasname externally updated?
@@ -58,10 +60,10 @@
                 try_next = False # stop processing auth method list
         return user, try_next
 
-    from MoinMoin.auth import moin_login, moin_session
-    from MoinMoin.auth.http import http
-    # first try the external_cookie, then http basic auth, then the usual moin_cookie
-    auth = [external_cookie, http, moin_login, moin_session]
+class FarmConfig(DefaultConfig):
+    from MoinMoin.auth import MoinLogin
+    # use ExternalCookie, also allow the usual moin login
+    auth = [ExternalCookie(), MoinLogin()]
 
     # ... (rest of your config follows here) ...