Index: trac/web/auth.py
===================================================================
--- trac/web/auth.py	(revision 2037)
+++ trac/web/auth.py	(working copy)
@@ -22,13 +22,35 @@
 from __future__ import generators
 
 import time
+import re
 
 from trac import util
 from trac.core import *
 from trac.web.chrome import INavigationContributor
+from trac.web.main import IRequestHandler
 
+class IAuthenticationProvider(Interface):
+    """
+    Extension point interface for adding authentication provider to the
+    Trac.
+    """
+    
+    def authenticate(self, req):
+        """
+        Authenticate user. If user is authenticated, this method must return
+        true value.
+        """
 
-class Authenticator:
+class Authenticator(Component):
+
+    auth_providers = ExtensionPoint(IAuthenticationProvider)
+    
+    def authorize(self, req):
+        for authenticator in self.auth_providers:
+            if authenticator.authenticate(req):
+                break
+    
+class LoginModule(Component):
     """Implements user authentication based on HTTP authentication provided by
     the web-server, combined with cookies for communicating the login
     information across the whole site.
@@ -40,30 +62,45 @@
     to identify the user in subsequent requests to non-protected resources.
     """
 
-    def __init__(self, db, req, check_ip=True, ignore_case=False):
-        self.db = db
-        self.authname = 'anonymous'
-        self.ignore_case = ignore_case
+    implements(INavigationContributor, IAuthenticationProvider, IRequestHandler)
 
-        if req.incookie.has_key('trac_auth'):
-            cookie = req.incookie['trac_auth'].value
-            cursor = db.cursor()
-            if check_ip:
-                cursor.execute("SELECT name FROM auth_cookie "
-                               "WHERE cookie=%s AND ipnr=%s",
-                               (cookie, req.remote_addr))
-            else:
-                cursor.execute("SELECT name FROM auth_cookie WHERE cookie=%s",
-                               (cookie,))
-            row = cursor.fetchone()
-            if row:
-                self.authname = row[0]
-            else:
-                # Tell the user to drop any auth_cookie for which no
-                # corresponding entry in our cookie table exists.
-                self.expire_auth_cookie(req)
+    # INavigationContributor methods
+    def get_active_navigation_item(self, req):
+        return 'login'
+        
+    def get_navigation_items(self, req):
+        if req.authname and req.authname != 'anonymous':
+            yield 'metanav', 'login', 'logged in as %s' \
+                  % util.escape(req.authname)
+            yield 'metanav', 'logout', '<a href="%s">Logout</a>' \
+                  % util.escape(self.env.href.logout())
+        else:
+            yield 'metanav', 'login', '<a href="%s">Login</a>' \
+                  % util.escape(self.env.href.login())
+    
+    # IRequestProvider methods
+    def match_request(self, req):
+        match = re.match(r'^/(login|logout)', req.path_info)
+        if match:
+            if match.group(1):
+                req.args['action'] = match.group(1)
+            return 1
 
-    def login(self, req):
+    def process_request(self, req): 
+        db = self.env.get_db_cnx()
+        
+        action = req.args.get('action') or ''
+        if action == 'login':
+            self._login(req, db)
+        else:
+            self._logout(req, db)
+
+    # IAuthenticationProvider methods
+    def authenticate(self, req):
+        return standard_authenticator(self.env, req)
+
+    # Internal methods
+    def _login(self, req, db):
         """Log the remote user in.
         
         This function expects to be called when the remote user name is
@@ -80,63 +117,113 @@
         assert req.remote_user, 'Authentication information not available.'
         
         remote_user = req.remote_user
-        if self.ignore_case:
+        ignore_case = self.env.config.get('trac', 'ignore_auth_case')
+        ignore_case = ignore_case.strip().lower() in util.TRUE
+        if ignore_case:
             remote_user = remote_user.lower()
 
-        if self.authname == remote_user:
-            # Already logged in with the same user name
-            return
-        assert self.authname == 'anonymous', \
-               'Already logged in as %s.' % self.authname
+        if req.authname != remote_user:
+            assert req.authname == 'anonymous', \
+               'Already logged in as %s.' % req.authname
 
-        cookie = util.hex_entropy()
-        cursor = self.db.cursor()
-        cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) "
-                       "VALUES (%s, %s, %s, %s)",
-                       (cookie, remote_user, req.remote_addr, int(time.time())))
-        self.db.commit()
-        self.authname = remote_user
-        req.outcookie['trac_auth'] = cookie
-        req.outcookie['trac_auth']['path'] = util.quote_cookie_value(req.cgi_location)
+            req.authname = remote_user
+        
+            standard_login(req, db)
 
-    def logout(self, req):
+        referer = req.get_header('Referer')
+        if referer and not referer.startswith(req.base_url):
+            # only redirect to referer if the latter is from the
+            # same instance
+            referer = None
+        req.redirect(referer or self.env.href.wiki())
+
+    def _logout(self, req, db):
         """Log the user out.
         
         Simply deletes the corresponding record from the auth_cookie table.
         """
-        if self.authname == 'anonymous':
-            # Not logged in
-            return
+        if req.authname and req.authname != 'anonymous':
+            standard_logoff(req, db)    
+            req.authname = 'anonymous'
+            
+        referer = req.get_header('Referer')
+        if referer and not referer.startswith(req.base_url):
+            # only redirect to referer if the latter is from the same
+            # instance
+            referer = None
+        req.redirect(referer or self.env.href.wiki())
 
-        cursor = self.db.cursor()
-        # While deleting this cookie we also take the opportunity to delete
-        # cookies older than 10 days
-        cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s",
-                       (self.authname, int(time.time()) - 86400 * 10))
-        self.db.commit()
-        self.expire_auth_cookie(req)
+def standard_authenticator(env, req):
+    """
+    Does standard authentication check from cookie.
+    
+    Optionally checks that IP of requester matches one in db.
+    """
+    
+    db = env.get_db_cnx()
+    authname = 'anonymous'
+    
+    check_ip = env.config.get('trac', 'check_auth_ip')
+    check_ip = check_ip.strip().lower() in util.TRUE
+    
+    if req.incookie.has_key('trac_auth'):
+        cookie = req.incookie['trac_auth'].value
+        cursor = db.cursor()
+        if check_ip:
+            cursor.execute("SELECT name FROM auth_cookie "
+                           "WHERE cookie=%s AND ipnr=%s",
+                           (cookie, req.remote_addr))
+        else:
+            cursor.execute("SELECT name FROM auth_cookie WHERE cookie=%s",
+                           (cookie,))
+        row = cursor.fetchone()
+        if row:
+            authname = row[0]
+        else:
+            # Tell the user to drop any auth_cookie for which no
+            # corresponding entry in our cookie table exists.
+            
+            expire_auth_cookie(req)
 
-    def expire_auth_cookie(self, req):
-        """Instruct the user agent to drop the auth cookie by setting the
-        "expires" property to a date in the past.
-        """
-        req.outcookie['trac_auth'] = ''
-        req.outcookie['trac_auth']['path'] = util.quote_cookie_value(req.cgi_location)
-        req.outcookie['trac_auth']['expires'] = -10000
+    req.authname = authname
+    
+    return req.authname != 'anonymous'
 
+def standard_login(req, db):
+    """
+    Does standard login by saving authname, sessionid and time to session table.    
+    """
 
-class LoginModule(Component):
+    cookie = util.hex_entropy()
+    cursor = db.cursor()
+    cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) "
+                   "VALUES (%s, %s, %s, %s)",
+                   (cookie, req.authname, req.remote_addr, int(time.time())))
+    db.commit()
+    
+    req.outcookie['trac_auth'] = cookie
+    req.outcookie['trac_auth']['path'] = util.quote_cookie_value(req.cgi_location)
 
-    implements(INavigationContributor)
+def standard_logoff(req, db):
+    """
+    Does standard logoff by deleting cookie data from session table.
+    
+    Also deletes cookies older than 10 days.
+    """
+    
+    cursor = db.cursor()
+    # While deleting this cookie we also take the opportunity to delete
+    # cookies older than 10 days
+    cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s",
+                   (req.authname, int(time.time()) - 86400 * 10))
+    db.commit()
+    expire_auth_cookie(req)
+        
+def expire_auth_cookie(req):
+    """Instruct the user agent to drop the auth cookie by setting the
+    "expires" property to a date in the past.
+    """
+    req.outcookie['trac_auth'] = ''
+    req.outcookie['trac_auth']['path'] = util.quote_cookie_value(req.cgi_location)
+    req.outcookie['trac_auth']['expires'] = -10000
 
-    # INavigationContributor methods
-
-    def get_navigation_items(self, req):
-        if req.authname and req.authname != 'anonymous':
-            yield 'metanav', 'login', 'logged in as %s' \
-                  % util.escape(req.authname)
-            yield 'metanav', 'logout', '<a href="%s">Logout</a>' \
-                  % util.escape(self.env.href.logout())
-        else:
-            yield 'metanav', 'login', '<a href="%s">Login</a>' \
-                  % util.escape(self.env.href.login())
Index: trac/web/main.py
===================================================================
--- trac/web/main.py	(revision 2037)
+++ trac/web/main.py	(working copy)
@@ -419,29 +419,10 @@
     try:
         try:
             from trac.web.auth import Authenticator
-            check_ip = env.config.get('trac', 'check_auth_ip')
-            check_ip = check_ip.strip().lower() in TRUE
-            ignore_case = env.config.get('trac', 'ignore_auth_case')
-            ignore_case = ignore_case.strip().lower() in TRUE
-            authenticator = Authenticator(db, req, check_ip, ignore_case)
-            if path_info == '/logout':
-                authenticator.logout(req)
-                referer = req.get_header('Referer')
-                if referer and not referer.startswith(req.base_url):
-                    # only redirect to referer if the latter is from the same
-                    # instance
-                    referer = None
-                req.redirect(referer or env.href.wiki())
-            elif req.remote_user:
-                authenticator.login(req)
-                if path_info == '/login':
-                    referer = req.get_header('Referer')
-                    if referer and not referer.startswith(req.base_url):
-                        # only redirect to referer if the latter is from the
-                        # same instance
-                        referer = None
-                    req.redirect(referer or env.href.wiki())
-            req.authname = authenticator.authname
+            
+            authenticator = Authenticator(env)
+            authenticator.authorize(req)
+                
             req.perm = PermissionCache(env, req.authname)
 
             newsession = req.args.has_key('newsession')

