comparison examples/trac/trac/web/auth.py @ 39:93b4dcbafd7b trunk

Copy Trac to main branch.
author cmlenz
date Mon, 03 Jul 2006 18:53:27 +0000
parents
children
comparison
equal deleted inserted replaced
38:ee669cb9cccc 39:93b4dcbafd7b
1 # -*- coding: utf-8 -*-
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
17 try:
18 from base64 import b64decode
19 except ImportError:
20 from base64 import decodestring as b64decode
21 import md5
22 import re
23 import sys
24 import time
25 import urllib2
26
27 from trac.config import BoolOption
28 from trac.core import *
29 from trac.web.api import IAuthenticator, IRequestHandler
30 from trac.web.chrome import INavigationContributor
31 from trac.util import hex_entropy, md5crypt
32 from trac.util.markup import escape, html
33
34
35 class LoginModule(Component):
36 """Implements user authentication based on HTTP authentication provided by
37 the web-server, combined with cookies for communicating the login
38 information across the whole site.
39
40 This mechanism expects that the web-server is setup so that a request to the
41 path '/login' requires authentication (such as Basic or Digest). The login
42 name is then stored in the database and associated with a unique key that
43 gets passed back to the user agent using the 'trac_auth' cookie. This cookie
44 is used to identify the user in subsequent requests to non-protected
45 resources.
46 """
47
48 implements(IAuthenticator, INavigationContributor, IRequestHandler)
49
50 check_ip = BoolOption('trac', 'check_auth_ip', 'true',
51 """Whether the IP address of the user should be checked for
52 authentication (''since 0.9'').""")
53
54 ignore_case = BoolOption('trac', 'ignore_auth_case', 'false',
55 """Whether case should be ignored for login names (''since 0.9'').""")
56
57 # IAuthenticator methods
58
59 def authenticate(self, req):
60 authname = None
61 if req.remote_user:
62 authname = req.remote_user
63 elif req.incookie.has_key('trac_auth'):
64 authname = self._get_name_for_cookie(req, req.incookie['trac_auth'])
65
66 if not authname:
67 return None
68
69 if self.ignore_case:
70 authname = authname.lower()
71
72 return authname
73
74 # INavigationContributor methods
75
76 def get_active_navigation_item(self, req):
77 return 'login'
78
79 def get_navigation_items(self, req):
80 if req.authname and req.authname != 'anonymous':
81 yield ('metanav', 'login', 'logged in as %s' % req.authname)
82 yield ('metanav', 'logout',
83 html.A('Logout', href=req.href.logout()))
84 else:
85 yield ('metanav', 'login',
86 html.A('Login', href=req.href.login()))
87
88 # IRequestHandler methods
89
90 def match_request(self, req):
91 return re.match('/(login|logout)/?', req.path_info)
92
93 def process_request(self, req):
94 if req.path_info.startswith('/login'):
95 self._do_login(req)
96 elif req.path_info.startswith('/logout'):
97 self._do_logout(req)
98 self._redirect_back(req)
99
100 # Internal methods
101
102 def _do_login(self, req):
103 """Log the remote user in.
104
105 This function expects to be called when the remote user name is
106 available. The user name is inserted into the `auth_cookie` table and a
107 cookie identifying the user on subsequent requests is sent back to the
108 client.
109
110 If the Authenticator was created with `ignore_case` set to true, then
111 the authentication name passed from the web server in req.remote_user
112 will be converted to lower case before being used. This is to avoid
113 problems on installations authenticating against Windows which is not
114 case sensitive regarding user names and domain names
115 """
116 assert req.remote_user, 'Authentication information not available.'
117
118 remote_user = req.remote_user
119 if self.ignore_case:
120 remote_user = remote_user.lower()
121
122 assert req.authname in ('anonymous', remote_user), \
123 'Already logged in as %s.' % req.authname
124
125 cookie = hex_entropy()
126 db = self.env.get_db_cnx()
127 cursor = db.cursor()
128 cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) "
129 "VALUES (%s, %s, %s, %s)", (cookie, remote_user,
130 req.remote_addr, int(time.time())))
131 db.commit()
132
133 req.authname = remote_user
134 req.outcookie['trac_auth'] = cookie
135 req.outcookie['trac_auth']['path'] = req.href()
136
137 def _do_logout(self, req):
138 """Log the user out.
139
140 Simply deletes the corresponding record from the auth_cookie table.
141 """
142 if req.authname == 'anonymous':
143 # Not logged in
144 return
145
146 # While deleting this cookie we also take the opportunity to delete
147 # cookies older than 10 days
148 db = self.env.get_db_cnx()
149 cursor = db.cursor()
150 cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s",
151 (req.authname, int(time.time()) - 86400 * 10))
152 db.commit()
153 self._expire_cookie(req)
154
155 def _expire_cookie(self, req):
156 """Instruct the user agent to drop the auth cookie by setting the
157 "expires" property to a date in the past.
158 """
159 req.outcookie['trac_auth'] = ''
160 req.outcookie['trac_auth']['path'] = req.href()
161 req.outcookie['trac_auth']['expires'] = -10000
162
163 def _get_name_for_cookie(self, req, cookie):
164 db = self.env.get_db_cnx()
165 cursor = db.cursor()
166 if self.check_ip:
167 cursor.execute("SELECT name FROM auth_cookie "
168 "WHERE cookie=%s AND ipnr=%s",
169 (cookie.value, req.remote_addr))
170 else:
171 cursor.execute("SELECT name FROM auth_cookie WHERE cookie=%s",
172 (cookie.value,))
173 row = cursor.fetchone()
174 if not row:
175 # The cookie is invalid (or has been purged from the database), so
176 # tell the user agent to drop it as it is invalid
177 self._expire_cookie(req)
178 return None
179
180 return row[0]
181
182 def _redirect_back(self, req):
183 """Redirect the user back to the URL she came from."""
184 referer = req.get_header('Referer')
185 if referer and not referer.startswith(req.base_url):
186 # only redirect to referer if it is from the same site
187 referer = None
188 req.redirect(referer or req.abs_href())
189
190
191 class HTTPAuthentication(object):
192
193 def do_auth(self, environ, start_response):
194 raise NotImplementedError
195
196
197 class BasicAuthentication(HTTPAuthentication):
198
199 def __init__(self, htpasswd, realm):
200 self.hash = {}
201 self.realm = realm
202 try:
203 import crypt
204 self.crypt = crypt.crypt
205 except ImportError:
206 self.crypt = None
207 self.load(htpasswd)
208
209 def load(self, filename):
210 fd = open(filename, 'r')
211 for line in fd:
212 u, h = line.strip().split(':')
213 if '$' in h or self.crypt:
214 self.hash[u] = h
215 else:
216 print >>sys.stderr, 'Warning: cannot parse password for ' \
217 'user "%s" without the "crypt" module' % u
218
219 if self.hash == {}:
220 print >> sys.stderr, "Warning: found no users in file:", filename
221
222 def test(self, user, password):
223 the_hash = self.hash.get(user)
224 if the_hash is None:
225 return False
226
227 if not '$' in the_hash:
228 return self.crypt(password, the_hash[:2]) == the_hash
229
230 magic, salt = the_hash[1:].split('$')[:2]
231 magic = '$' + magic + '$'
232 return md5crypt(password, salt, magic) == the_hash
233
234 def do_auth(self, environ, start_response):
235 header = environ.get('HTTP_AUTHORIZATION')
236 if header and header.startswith('Basic'):
237 auth = b64decode(header[6:]).split(':')
238 if len(auth) == 2:
239 user, password = auth
240 if self.test(user, password):
241 return user
242
243 start_response('401 Unauthorized',
244 [('WWW-Authenticate', 'Basic realm="%s"'
245 % self.realm)])('')
246
247
248 class DigestAuthentication(HTTPAuthentication):
249 """A simple HTTP digest authentication implementation (RFC 2617)."""
250
251 MAX_NONCES = 100
252
253 def __init__(self, htdigest, realm):
254 self.active_nonces = []
255 self.hash = {}
256 self.realm = realm
257 self.load_htdigest(htdigest, realm)
258
259 def load_htdigest(self, filename, realm):
260 """Load account information from apache style htdigest files, only
261 users from the specified realm are used
262 """
263 fd = open(filename, 'r')
264 for line in fd.readlines():
265 u, r, a1 = line.strip().split(':')
266 if r == realm:
267 self.hash[u] = a1
268 if self.hash == {}:
269 print >> sys.stderr, "Warning: found no users in realm:", realm
270
271 def parse_auth_header(self, authorization):
272 values = {}
273 for value in urllib2.parse_http_list(authorization):
274 n, v = value.split('=', 1)
275 if v[0] == '"' and v[-1] == '"':
276 values[n] = v[1:-1]
277 else:
278 values[n] = v
279 return values
280
281 def send_auth_request(self, environ, start_response, stale='false'):
282 """Send a digest challange to the browser. Record used nonces
283 to avoid replay attacks.
284 """
285 nonce = hex_entropy()
286 self.active_nonces.append(nonce)
287 if len(self.active_nonces) > self.MAX_NONCES:
288 self.active_nonces = self.active_nonces[-self.MAX_NONCES:]
289 start_response('401 Unauthorized',
290 [('WWW-Authenticate',
291 'Digest realm="%s", nonce="%s", qop="auth", stale="%s"'
292 % (self.realm, nonce, stale))])('')
293
294 def do_auth(self, environ, start_response):
295 header = environ.get('HTTP_AUTHORIZATION')
296 if not header or not header.startswith('Digest'):
297 self.send_auth_request(environ, start_response)
298 return None
299
300 auth = self.parse_auth_header(header[7:])
301 required_keys = ['username', 'realm', 'nonce', 'uri', 'response',
302 'nc', 'cnonce']
303 # Invalid response?
304 for key in required_keys:
305 if not auth.has_key(key):
306 self.send_auth_request(environ, start_response)
307 return None
308 # Unknown user?
309 if not self.hash.has_key(auth['username']):
310 self.send_auth_request(environ, start_response)
311 return None
312
313 kd = lambda x: md5.md5(':'.join(x)).hexdigest()
314 a1 = self.hash[auth['username']]
315 a2 = kd([environ['REQUEST_METHOD'], auth['uri']])
316 # Is the response correct?
317 correct = kd([a1, auth['nonce'], auth['nc'],
318 auth['cnonce'], auth['qop'], a2])
319 if auth['response'] != correct:
320 self.send_auth_request(environ, start_response)
321 return None
322 # Is the nonce active, if not ask the client to use a new one
323 if not auth['nonce'] in self.active_nonces:
324 self.send_auth_request(environ, start_response, stale='true')
325 return None
326 self.active_nonces.remove(auth['nonce'])
327 return auth['username']
Copyright (C) 2012-2017 Edgewall Software