Edgewall Software

Ticket #791: auth_form2.diff

File auth_form2.diff, 10.1 KB (added by adeason@…, 8 years ago)

A patch for logging in using a form instead of direct HTTP auth, and for actual logouts to work. Uploaded the wrong version of this patch previously; this one should work.

  • trac/db_default.py

     
    450450                      'trac.versioncontrol.web_ui', 
    451451                      'trac.versioncontrol.svn_fs', 
    452452                      'trac.wiki.macros', 'trac.wiki.web_ui', 
    453                       'trac.web.auth') 
     453                      'trac.web.auth_form') 
  • trac/web/auth_form.py

     
     1# -*- coding: iso8859-1 -*- 
     2# 
     3# Copyright (C) 2003-2005 Edgewall Software 
     4# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com> 
     5# All rights reserved. 
     6# 
     7# This software is licensed as described in the file COPYING, which 
     8# you should have received as part of this distribution. The terms 
     9# are also available at http://trac.edgewall.com/license.html. 
     10# 
     11# This software consists of voluntary contributions made by many 
     12# individuals. For the exact contribution history, see the revision 
     13# history and logs, available at http://projects.edgewall.com/trac/. 
     14# 
     15# Author: Jonas Borgström <jonas@edgewall.com  
     16# Author: Andrew Deason <adeason@tjhsst.edu> 
     17 
     18from __future__ import generators 
     19import re 
     20import time 
     21import urllib2,base64 
     22 
     23from trac.core import * 
     24from trac.web.api import IAuthenticator, IRequestHandler 
     25from trac.web.chrome import INavigationContributor 
     26from trac.util import escape, hex_entropy, TRUE 
     27 
     28 
     29class LoginFormModule(Component): 
     30    """Implements user authentication based on HTTP authentication provided by 
     31    the web-server, combined with cookies for communicating the login 
     32    information across the whole site. This module makes an internal request to 
     33    the webserver using HTTP authentication, instead of using HTTP 
     34    authentication directly from the user, so the user has the ability to 
     35    logout. 
     36 
     37    This mechanism expects that the web-server is setup so that a request to the 
     38    path '/login' requires authentication (just Basic authentication for now). 
     39    When a user attempts to login, this module attempts to use that information 
     40    to authenticate to the page https://trac.company.com/login, using HTTP 
     41    authentication. If it is successful, a 'trac_auth' cookie is stored in the 
     42    user's browser. This cookie is used to identify the user in subsequent 
     43    requests, until it is destroyed when the user logs out. 
     44    """ 
     45 
     46    implements(IAuthenticator, INavigationContributor, IRequestHandler) 
     47 
     48    # IAuthenticator methods 
     49 
     50    def authenticate(self, req): 
     51        authname = None 
     52        if req.incookie.has_key('trac_auth'): 
     53            authname = self._get_name_for_cookie(req, req.incookie['trac_auth']) 
     54 
     55        if not authname: 
     56            return None 
     57 
     58        ignore_case = self.env.config.get('trac', 'ignore_auth_case') 
     59        ignore_case = ignore_case.strip().lower() in TRUE 
     60        if ignore_case: 
     61            authname = authname.lower() 
     62        return authname 
     63 
     64    # INavigationContributor methods 
     65 
     66    def get_active_navigation_item(self, req): 
     67        return 'login' 
     68 
     69    def get_navigation_items(self, req): 
     70        if req.authname and req.authname != 'anonymous': 
     71            yield 'metanav', 'login', 'logged in as %s' % escape(req.authname) 
     72            yield 'metanav', 'logout', '<a href="%s">Logout</a>' \ 
     73                  % escape(self.env.href.logout()) 
     74        else: 
     75            yield 'metanav', 'login', '<a href="%s">Login</a>' \ 
     76                  % escape(self.env.href.login()) 
     77 
     78    # IRequestHandler methods 
     79 
     80    def match_request(self, req): 
     81        return re.match('/(login|logout)/?', req.path_info) 
     82 
     83    def process_request(self, req): 
     84        if req.path_info.startswith('/login'): 
     85            if not self._do_login(req): 
     86                return 'login.cs', None 
     87        elif req.path_info.startswith('/logout'): 
     88            self._do_logout(req) 
     89        self._redirect_back(req) 
     90 
     91    # Internal methods 
     92 
     93    def _do_login(self, req): 
     94        """Log the remote user in. 
     95         
     96        This function displays a form to the user to log themselves in, and 
     97        verifies the information when the user submits that form. If the 
     98        authentication is successful, the user name is inserted into the 
     99        `auth_cookie` table and a cookie identifying the user on subsequent 
     100        requests is sent back to the client. 
     101 
     102        If the Authenticator was created with `ignore_case` set to true, then  
     103        the authentication name passed from the web form 'username' variable 
     104        will be converted to lower case before being used. This is to avoid 
     105        problems on installations authenticating against Windows which is not 
     106        case sensitive regarding user names and domain names 
     107        """ 
     108 
     109        if req.args.get('username'): 
     110                assert req.args.get('password'), 'No password' 
     111                # Test authentication 
     112 
     113                try: 
     114                        self._try_http_auth(req.base_url[:req.base_url.find('/',8)] + '/login',req.args.get('username'),req.args.get('password')) 
     115                except IOError, e: 
     116                        # Incorrect password 
     117                        req.hdf['title'] = 'Login Failed' 
     118                        req.hdf['login.action'] = self.env.href() + '/login' 
     119                        req.hdf['login.referer'] = req.args.get('ref') 
     120                        req.hdf['login.error'] = 'Invalid username or password' 
     121                        return None 
     122 
     123                # Successful authentication, set cookies and stuff 
     124                remote_user = req.args.get('username') 
     125                ignore_case = self.env.config.get('trac', 'ignore_auth_case') 
     126                ignore_case = ignore_case.strip().lower() in TRUE 
     127                if ignore_case: 
     128                    remote_user = remote_user.lower() 
     129         
     130                assert req.authname in ('anonymous', remote_user), \ 
     131                       'Already logged in as %s.' % req.authname 
     132         
     133                cookie = hex_entropy() 
     134                db = self.env.get_db_cnx() 
     135                cursor = db.cursor() 
     136                cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) " 
     137                               "VALUES (%s, %s, %s, %s)", (cookie, remote_user, 
     138                               req.remote_addr, int(time.time()))) 
     139                db.commit() 
     140         
     141                req.authname = remote_user 
     142                req.outcookie['trac_auth'] = cookie 
     143                req.outcookie['trac_auth']['path'] = self.env.href() 
     144                req.redirect(req.args.get('ref')) 
     145        else: 
     146                # No authentication information passed, display a form 
     147                req.hdf['title'] = 'Login' 
     148                req.hdf['login.action'] = self.env.href() + '/login' 
     149                req.hdf['login.referer'] = req.get_header('Referer') 
     150                return None 
     151         
     152    def _do_logout(self, req): 
     153        """Log the user out. 
     154 
     155        Simply deletes the corresponding record from the auth_cookie table. 
     156        """ 
     157        if req.authname == 'anonymous': 
     158            # Not logged in 
     159            return 
     160 
     161        # While deleting this cookie we also take the opportunity to delete 
     162        # cookies older than 10 days 
     163        db = self.env.get_db_cnx() 
     164        cursor = db.cursor() 
     165        cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s", 
     166                       (req.authname, int(time.time()) - 86400 * 10)) 
     167        db.commit() 
     168        self._expire_cookie(req) 
     169        req.remote_user = 'anonymous' 
     170        req.remote_pass = '' 
     171 
     172    def _try_http_auth(self, uri, user, passw): 
     173        authreq = urllib2.Request(uri) 
     174        base64string = base64.encodestring('%s:%s' % (user, passw))[:-1] 
     175        authheader = "Basic %s" % base64string 
     176        authreq.add_header("Authorization", authheader) 
     177        handle = urllib2.urlopen(authreq) 
     178 
     179    def _expire_cookie(self, req): 
     180        """Instruct the user agent to drop the auth cookie by setting the 
     181        "expires" property to a date in the past. 
     182        """ 
     183        req.outcookie['trac_auth'] = '' 
     184        req.outcookie['trac_auth']['path'] = self.env.href() 
     185        req.outcookie['trac_auth']['expires'] = -10000 
     186 
     187    def _get_name_for_cookie(self, req, cookie): 
     188        check_ip = self.env.config.get('trac', 'check_auth_ip') 
     189        check_ip = check_ip.strip().lower() in TRUE 
     190 
     191        db = self.env.get_db_cnx() 
     192        cursor = db.cursor() 
     193        if check_ip: 
     194            cursor.execute("SELECT name FROM auth_cookie " 
     195                           "WHERE cookie=%s AND ipnr=%s", 
     196                           (cookie.value, req.remote_addr)) 
     197        else: 
     198            cursor.execute("SELECT name FROM auth_cookie WHERE cookie=%s", 
     199                           (cookie.value,)) 
     200        row = cursor.fetchone() 
     201        if not row: 
     202            # The cookie is invalid (or has been purged from the database), so 
     203            # tell the user agent to drop it as it is invalid 
     204            self._expire_cookie(req) 
     205            return None 
     206 
     207        return row[0] 
     208 
     209    def _redirect_back(self, req): 
     210        """Redirect the user back to the URL she came from.""" 
     211        referer = req.get_header('Referer') 
     212        if referer and not referer.startswith(req.base_url): 
     213            # only redirect to referer if the latter is from the same 
     214            # instance 
     215            referer = None 
     216        req.redirect(referer or self.env.abs_href()) 
  • templates/login.cs

     
     1<?cs include "header.cs"?> 
     2 
     3<div id="ctxtnav" class="nav"></div> 
     4 
     5<div id="content" class="login"> 
     6        <?cs if:login.error ?> 
     7                <p class="message"> 
     8                <?cs var:login.error ?> 
     9                </p> 
     10        <?cs /if ?> 
     11        <form id="login" action="<?cs var:login.action ?>" method="POST"> 
     12        <input type="hidden" name="ref" value="<?cs var:login.referer ?>" /> 
     13        <fieldset id="properties"> 
     14                <legend>Enter your username and password</legend> 
     15                <table><tr> 
     16                        <th class="col1"><label for="username">Username:</label></th> 
     17                         
     18                        <td><input type="text" name="username" id="username" class="textwidget" /></td></tr> 
     19                        <tr><th class="col1"><label for="password">Password:</label></th> 
     20                        <td><input type="password" name="password" id="password" class="textwidget" /></td></tr> 
     21                </table> 
     22        </fieldset> 
     23        <div class="buttons"> 
     24                <input type="submit" name="login" value="Login" />&nbsp; 
     25                <input type="reset" name="reset" value="Clear" /> 
     26        </div> 
     27        </form> 
     28</div> 
     29<?cs include "footer.cs"?>