Package wsgitools :: Module digest
[hide private]
[frames] | no frames]

Source Code for Module wsgitools.digest

  1  """ 
  2  This module contains an C{AuthDigestMiddleware} for authenticating HTTP 
  3  requests using the method described in RFC2617. The credentials are to be 
  4  provided using an C{AuthTokenGenerator} or a compatible instance. Furthermore 
  5  digest authentication has to preserve some state across requests, more 
  6  specifically nonces. There are three different C{NonceStoreBase} 
  7  implementations for different needs. While the C{StatelessNonceStore} has 
  8  minimal requirements it only prevents replay attacks in a limited way. If the 
  9  WSGI server uses threading or a single process the C{MemoryNonceStore} can be 
 10  used. If that is not possible the nonces can be stored in a DBAPI2 compatible 
 11  database using C{DBAPI2NonceStore}. 
 12  """ 
 13   
 14  __all__ = [] 
 15   
 16  import base64 
 17  import hashlib 
 18  import time 
 19  import os 
 20  try: 
 21      from secrets import randbits, compare_digest 
 22  except ImportError: 
 23      import random 
 24      sysrand = random.SystemRandom() 
 25      randbits = sysrand.getrandbits 
26 - def compare_digest(a, b):
27 return a == b
28 29 from wsgitools.internal import bytes2str, str2bytes, textopen 30 from wsgitools.authentication import AuthenticationRequired, \ 31 ProtocolViolation, AuthenticationMiddleware 32
33 -def md5hex(data):
34 """ 35 @type data: str 36 @rtype: str 37 """ 38 return hashlib.md5(str2bytes(data)).hexdigest()
39
40 -def gen_rand_str(bytesentropy=33):
41 """ 42 Generates a string of random base64 characters. 43 @param bytesentropy: is the number of random 8bit values to be used 44 @rtype: str 45 46 >>> gen_rand_str() != gen_rand_str() 47 True 48 """ 49 randnum = randbits(bytesentropy*8) 50 randstr = ("%%0%dX" % (2*bytesentropy)) % randnum 51 randbytes = str2bytes(randstr) 52 randbytes = base64.b16decode(randbytes) 53 randbytes = base64.b64encode(randbytes) 54 randstr = bytes2str(randbytes) 55 return randstr
56
57 -def parse_digest_response(data):
58 """internal 59 @raises ValueError: 60 61 >>> parse_digest_response('foo=bar') 62 {'foo': 'bar'} 63 >>> parse_digest_response('foo="bar"') 64 {'foo': 'bar'} 65 >>> sorted(parse_digest_response('foo="bar=qux",spam=egg').items()) 66 [('foo', 'bar=qux'), ('spam', 'egg')] 67 >>> try: 68 ... parse_digest_response('spam') 69 ... except ValueError: 70 ... print("ValueError") 71 ValueError 72 >>> try: 73 ... parse_digest_response('spam="egg"error') 74 ... except ValueError: 75 ... print("ValueError") 76 ValueError 77 >>> # backslashes: doc string, eval => two backslashes 78 >>> parse_digest_response('backslash="\\\\\\\\"') 79 {'backslash': '\\\\'} 80 >>> parse_digest_response('foo="quo\\\\"te"') 81 {'foo': 'quo"te'} 82 """ 83 assert isinstance(data, str) 84 result = dict() 85 while True: 86 data = data.strip() 87 key, data = data.split('=', 1) # raises ValueError 88 if data.startswith('"'): 89 data = data[1:] 90 value = "" 91 while True: 92 part, data = data.split('"', 1) # raises ValueError 93 # how many consecutive backslashes did we see? 94 escape = -1 # the first iteration does not indicate a backslash 95 for part in part.split('\\'): 96 escape += 1 # backslash before part 97 if escape > 2: 98 value += "\\" 99 escape -= 2 100 if part: 101 escape = 0 102 value += part 103 if escape == 2: 104 value += "\\" 105 elif escape == 1: 106 value += '"' 107 continue 108 break 109 result[key] = value 110 if not data: 111 return result 112 if data[0] != ",": 113 raise ValueError("invalid digest response") 114 data = data[1:] 115 else: 116 if ',' not in data: 117 result[key] = data 118 return result 119 value, data = data.split(',', 1) 120 result[key] = value
121
122 -def format_digest(mapping):
123 """internal 124 125 @type mapping: {str: (str, bool)} 126 @param mapping: a mapping of keys to values and a boolean that 127 determines whether the value needs quoting. 128 @rtype: str 129 @note: the RFC specifies which values must be quoted and which must not be 130 quoted. 131 """ 132 assert isinstance(mapping, dict) 133 result = [] 134 for key, (value, needsquoting) in mapping.items(): 135 assert isinstance(key, str) 136 assert isinstance(value, str) 137 if needsquoting: 138 value = '"%s"' % value.replace('\\', '\\\\').replace('"', '\\"') 139 else: 140 assert '"' not in value 141 assert ',' not in value 142 result.append("%s=%s" % (key, value)) 143 return ", ".join(result)
144
145 -class StaleNonce(AuthenticationRequired):
146 pass
147 148 __all__.append("AbstractTokenGenerator")
149 -class AbstractTokenGenerator(object):
150 """Interface class for generating authentication tokens for 151 L{AuthDigestMiddleware}. 152 153 @ivar realm: is a string according to RFC2617. 154 @type realm: str 155 """
156 - def __init__(self, realm):
157 """ 158 @type realm: str 159 """ 160 assert isinstance(realm, str) 161 self.realm = realm
162
163 - def __call__(self, username, algo="md5"):
164 """Generates an authentication token from a username. 165 @type username: str 166 @type algo: str 167 @param algo: currently the only value supported by 168 L{AuthDigestMiddleware} is "md5" 169 @rtype: str or None 170 @returns: a valid token or None to signal that authentication should 171 fail 172 """ 173 raise NotImplementedError
174
175 - def check_password(self, username, password, environ=None):
176 """ 177 This function implements the interface for verifying passwords 178 used by L{BasicAuthMiddleware}. It works by computing a token 179 from the user and comparing it to the token returned by the 180 __call__ method. 181 182 @type username: str 183 @type password: str 184 @param environ: ignored 185 @rtype: bool 186 """ 187 assert isinstance(username, str) 188 assert isinstance(password, str) 189 token = "%s:%s:%s" % (username, self.realm, password) 190 return compare_digest(md5hex(token), self(username))
191 192 __all__.append("AuthTokenGenerator")
193 -class AuthTokenGenerator(AbstractTokenGenerator):
194 """Generates authentication tokens for L{AuthDigestMiddleware}. The 195 interface consists of beeing callable with a username and having a 196 realm attribute being a string."""
197 - def __init__(self, realm, getpass):
198 """ 199 @type realm: str 200 @param realm: is a string according to RFC2617. 201 @type getpass: str -> (str or None) 202 @param getpass: this function is called with a username and password is 203 expected as result. C{None} may be used as an invalid password. 204 An example for getpass would be C{{username: password}.get}. 205 """ 206 AbstractTokenGenerator.__init__(self, realm) 207 self.getpass = getpass
208
209 - def __call__(self, username, algo="md5"):
210 assert isinstance(username, str) 211 assert algo.lower() in ["md5", "md5-sess"] 212 password = self.getpass(username) 213 if password is None: 214 return None 215 a1 = "%s:%s:%s" % (username, self.realm, password) 216 return md5hex(a1)
217 218 __all__.append("HtdigestTokenGenerator")
219 -class HtdigestTokenGenerator(AbstractTokenGenerator):
220 """Reads authentication tokens for L{AuthDigestMiddleware} from an 221 apache htdigest file. 222 """
223 - def __init__(self, realm, htdigestfile, ignoreparseerrors=False):
224 """ 225 @type realm: str 226 @type htdigestfile: str 227 @param htdigestfile: path to the .htdigest file 228 @type ignoreparseerrors: bool 229 @param ignoreparseerrors: passed to readhtdigest 230 @raises IOError: 231 @raises ValueError: 232 """ 233 AbstractTokenGenerator.__init__(self, realm) 234 self.users = {} 235 self.readhtdigest(htdigestfile, ignoreparseerrors)
236
237 - def readhtdigest(self, htdigestfile, ignoreparseerrors=False):
238 """ 239 @type htdigestfile: str 240 @type ignoreparseerrors: bool 241 @param ignoreparseerrors: do not raise ValueErrors for bad files 242 @raises IOError: 243 @raises ValueError: 244 """ 245 assert isinstance(htdigestfile, str) 246 self.users = {} 247 with textopen(htdigestfile, "r") as htdigest: 248 for line in htdigest: 249 parts = line.rstrip("\n").split(":") 250 if len(parts) != 3: 251 if ignoreparseerrors: 252 continue 253 raise ValueError("invalid number of colons in htdigest file") 254 user, realm, token = parts 255 if realm != self.realm: 256 continue 257 if user in self.users and not ignoreparseerrors: 258 raise ValueError("duplicate user in htdigest file") 259 self.users[user] = token
260
261 - def __call__(self, user, algo="md5"):
262 assert algo.lower() in ["md5", "md5-sess"] 263 return self.users.get(user)
264 265 __all__.append("UpdatingHtdigestTokenGenerator")
266 -class UpdatingHtdigestTokenGenerator(HtdigestTokenGenerator):
267 """Behaves like L{HtdigestTokenGenerator}, checks the htdigest file 268 for changes on each invocation. 269 """
270 - def __init__(self, realm, htdigestfile, ignoreparseerrors=False):
271 assert isinstance(htdigestfile, str) 272 # Need to stat the file before calling parent ctor to detect 273 # modifications. 274 try: 275 self.statcache = os.stat(htdigestfile) 276 except OSError as err: 277 raise IOError(str(err)) 278 HtdigestTokenGenerator.__init__(self, realm, htdigestfile, 279 ignoreparseerrors) 280 self.htdigestfile = htdigestfile 281 self.ignoreparseerrors = ignoreparseerrors
282
283 - def __call__(self, user, algo="md5"):
284 # The interface does not permit raising exceptions, so all we can do is 285 # fail by returning None. 286 try: 287 statcache = os.stat(self.htdigestfile) 288 except OSError: 289 return None 290 if self.statcache != statcache: 291 try: 292 self.readhtdigest(self.htdigestfile, self.ignoreparseerrors) 293 except IOError: 294 return None 295 except ValueError: 296 return None 297 return HtdigestTokenGenerator.__call__(self, user, algo)
298 299 __all__.append("NonceStoreBase")
300 -class NonceStoreBase(object):
301 """Nonce storage interface."""
302 - def __init__(self):
303 pass
304 - def newnonce(self, ident=None):
305 """ 306 This method is to be overriden and should return new nonces. 307 @type ident: str 308 @param ident: is an identifier to be associated with this nonce 309 @rtype: str 310 """ 311 raise NotImplementedError
312 - def checknonce(self, nonce, count=1, ident=None):
313 """ 314 This method is to be overridden and should do a check for whether the 315 given nonce is valid as being used count times. 316 @type nonce: str 317 @type count: int 318 @param count: indicates how often the nonce has been used (including 319 this check) 320 @type ident: str 321 @param ident: it is also checked that the nonce was associated to this 322 identifier when given 323 @rtype: bool 324 """ 325 raise NotImplementedError
326
327 -def format_time(seconds):
328 """ 329 internal method formatting a unix time to a fixed-length string 330 @type seconds: float 331 @rtype: str 332 """ 333 # the overflow will happen about 2112 334 return "%013X" % int(seconds * 1000000)
335 336 __all__.append("StatelessNonceStore")
337 -class StatelessNonceStore(NonceStoreBase):
338 """ 339 This is a stateless nonce storage that cannot check the usage count for 340 a nonce and thus cannot protect against replay attacks. It however can make 341 it difficult by posing a timeout on nonces and making it difficult to forge 342 nonces. 343 344 This nonce store is usable with L{scgi.forkpool}. 345 346 >>> s = StatelessNonceStore() 347 >>> n = s.newnonce() 348 >>> s.checknonce("spam") 349 False 350 >>> s.checknonce(n) 351 True 352 >>> s.checknonce(n) 353 True 354 >>> s.checknonce(n.rsplit(':', 1)[0] + "bad hash") 355 False 356 """
357 - def __init__(self, maxage=300, secret=None):
358 """ 359 @type maxage: int 360 @param maxage: is the number of seconds a nonce may be valid. Choosing a 361 large value may result in more memory usage whereas a smaller 362 value results in more requests. Defaults to 5 minutes. 363 @type secret: str 364 @param secret: if not given, a secret is generated and is therefore 365 shared after forks. Knowing this secret permits creating nonces. 366 """ 367 NonceStoreBase.__init__(self) 368 self.maxage = maxage 369 if secret: 370 self.server_secret = secret 371 else: 372 self.server_secret = gen_rand_str()
373
374 - def newnonce(self, ident=None):
375 """ 376 Generates a new nonce string. 377 @type ident: None or str 378 @rtype: str 379 """ 380 nonce_time = format_time(time.time()) 381 nonce_value = gen_rand_str() 382 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 383 if ident is not None: 384 token = "%s:%s" % (token, ident) 385 token = md5hex(token) 386 return "%s:%s:%s" % (nonce_time, nonce_value, token)
387
388 - def checknonce(self, nonce, count=1, ident=None):
389 """ 390 Check whether the provided string is a nonce. 391 @type nonce: str 392 @type count: int 393 @type ident: None or str 394 @rtype: bool 395 """ 396 if count != 1: 397 return False 398 try: 399 nonce_time, nonce_value, nonce_hash = nonce.split(':') 400 except ValueError: 401 return False 402 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 403 if ident is not None: 404 token = "%s:%s" % (token, ident) 405 token = md5hex(token) 406 if token != nonce_hash: 407 return False 408 409 if nonce_time < format_time(time.time() - self.maxage): 410 return False 411 return True
412 413 __all__.append("MemoryNonceStore")
414 -class MemoryNonceStore(NonceStoreBase):
415 """ 416 Simple in-memory mechanism to store nonces. 417 418 >>> s = MemoryNonceStore(maxuses=1) 419 >>> n = s.newnonce() 420 >>> s.checknonce("spam") 421 False 422 >>> s.checknonce(n) 423 True 424 >>> s.checknonce(n) 425 False 426 >>> n = s.newnonce() 427 >>> s.checknonce(n.rsplit(':', 1)[0] + "bad hash") 428 False 429 """
430 - def __init__(self, maxage=300, maxuses=5):
431 """ 432 @type maxage: int 433 @param maxage: is the number of seconds a nonce may be valid. Choosing a 434 large value may result in more memory usage whereas a smaller 435 value results in more requests. Defaults to 5 minutes. 436 @type maxuses: int 437 @param maxuses: is the number of times a nonce may be used (with 438 different nc values). A value of 1 makes nonces usable exactly 439 once resulting in more requests. Defaults to 5. 440 """ 441 NonceStoreBase.__init__(self) 442 self.maxage = maxage 443 self.maxuses = maxuses 444 self.nonces = [] # [(creation_time, nonce_value, useage_count)] 445 # as [(str (hex encoded), str, int)] 446 self.server_secret = gen_rand_str()
447
448 - def _cleanup(self):
449 """internal methods cleaning list of valid nonces""" 450 old = format_time(time.time() - self.maxage) 451 while self.nonces and self.nonces[0][0] < old: 452 self.nonces.pop(0)
453
454 - def newnonce(self, ident=None):
455 """ 456 Generates a new nonce string. 457 @type ident: None or str 458 @rtype: str 459 """ 460 self._cleanup() # avoid growing self.nonces 461 nonce_time = format_time(time.time()) 462 nonce_value = gen_rand_str() 463 self.nonces.append((nonce_time, nonce_value, 1)) 464 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 465 if ident is not None: 466 token = "%s:%s" % (token, ident) 467 token = md5hex(token) 468 return "%s:%s:%s" % (nonce_time, nonce_value, token)
469
470 - def checknonce(self, nonce, count=1, ident=None):
471 """ 472 Do a check for whether the provided string is a nonce and increase usage 473 count on returning True. 474 @type nonce: str 475 @type count: int 476 @type ident: None or str 477 @rtype: bool 478 """ 479 try: 480 nonce_time, nonce_value, nonce_hash = nonce.split(':') 481 except ValueError: 482 return False 483 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 484 if ident is not None: 485 token = "%s:%s" % (token, ident) 486 token = md5hex(token) 487 if token != nonce_hash: 488 return False 489 490 self._cleanup() # avoid stale nonces 491 492 # searching nonce_time 493 lower, upper = 0, len(self.nonces) - 1 494 while lower < upper: 495 mid = (lower + upper) // 2 496 if nonce_time <= self.nonces[mid][0]: 497 upper = mid 498 else: 499 lower = mid + 1 500 501 if len(self.nonces) <= lower: 502 return False 503 (nt, nv, uses) = self.nonces[lower] 504 if nt != nonce_time or nv != nonce_value: 505 return False 506 if count != uses: 507 del self.nonces[lower] 508 return False 509 if uses >= self.maxuses: 510 del self.nonces[lower] 511 else: 512 self.nonces[lower] = (nt, nv, uses+1) 513 return True
514 515 __all__.append("LazyDBAPI2Opener")
516 -class LazyDBAPI2Opener(object):
517 """ 518 Connects to database on first request. Otherwise it behaves like a dbapi2 519 connection. This may be usefull in combination with L{scgi.forkpool}, 520 because this way each worker child opens a new database connection when 521 the first request is to be answered. 522 """
523 - def __init__(self, function, *args, **kwargs):
524 """ 525 The database will be connected on the first method call. This is done 526 by calling the given function with the remaining parameters. 527 @param function: is the function that connects to the database 528 """ 529 self._function = function 530 self._args = args 531 self._kwargs = kwargs 532 self._dbhandle = None
533 - def _getdbhandle(self):
534 """Returns an open database connection. Open if necessary.""" 535 if self._dbhandle is None: 536 self._dbhandle = self._function(*self._args, **self._kwargs) 537 self._function = self._args = self._kwargs = None 538 return self._dbhandle
539 - def cursor(self):
540 """dbapi2""" 541 return self._getdbhandle().cursor()
542 - def commit(self):
543 """dbapi2""" 544 return self._getdbhandle().commit()
545 - def rollback(self):
546 """dbapi2""" 547 return self._getdbhandle().rollback()
548 - def close(self):
549 """dbapi2""" 550 return self._getdbhandle().close()
551 552 __all__.append("DBAPI2NonceStore")
553 -class DBAPI2NonceStore(NonceStoreBase):
554 """ 555 A dbapi2-backed nonce store implementation suitable for usage with forking 556 wsgi servers such as L{scgi.forkpool}. 557 558 >>> import sqlite3 559 >>> db = sqlite3.connect(":memory:") 560 >>> db.cursor().execute("CREATE TABLE nonces (key, value);") and None 561 >>> db.commit() and None 562 >>> s = DBAPI2NonceStore(db, maxuses=1) 563 >>> n = s.newnonce() 564 >>> s.checknonce("spam") 565 False 566 >>> s.checknonce(n) 567 True 568 >>> s.checknonce(n) 569 False 570 >>> n = s.newnonce() 571 >>> s.checknonce(n.rsplit(':', 1)[0] + "bad hash") 572 False 573 """
574 - def __init__(self, dbhandle, maxage=300, maxuses=5, table="nonces"):
575 """ 576 @param dbhandle: is a dbapi2 connection 577 @type maxage: int 578 @param maxage: is the number of seconds a nonce may be valid. Choosing a 579 large value may result in more memory usage whereas a smaller 580 value results in more requests. Defaults to 5 minutes. 581 @type maxuses: int 582 @param maxuses: is the number of times a nonce may be used (with 583 different nc values). A value of 1 makes nonces usable exactly 584 once resulting in more requests. Defaults to 5. 585 """ 586 NonceStoreBase.__init__(self) 587 self.dbhandle = dbhandle 588 self.maxage = maxage 589 self.maxuses = maxuses 590 self.table = table 591 self.server_secret = gen_rand_str()
592
593 - def _cleanup(self, cur):
594 """internal methods cleaning list of valid nonces""" 595 old = format_time(time.time() - self.maxage) 596 cur.execute("DELETE FROM %s WHERE key < '%s:';" % (self.table, old))
597
598 - def newnonce(self, ident=None):
599 """ 600 Generates a new nonce string. 601 @rtype: str 602 """ 603 nonce_time = format_time(time.time()) 604 nonce_value = gen_rand_str() 605 dbkey = "%s:%s" % (nonce_time, nonce_value) 606 cur = self.dbhandle.cursor() 607 self._cleanup(cur) # avoid growing database 608 cur.execute("INSERT INTO %s VALUES ('%s', '1');" % (self.table, dbkey)) 609 self.dbhandle.commit() 610 token = "%s:%s" % (dbkey, self.server_secret) 611 if ident is not None: 612 token = "%s:%s" % (token, ident) 613 token = md5hex(token) 614 return "%s:%s:%s" % (nonce_time, nonce_value, token)
615
616 - def checknonce(self, nonce, count=1, ident=None):
617 """ 618 Do a check for whether the provided string is a nonce and increase usage 619 count on returning True. 620 @type nonce: str 621 @type count: int 622 @type ident: str or None 623 @rtype: bool 624 """ 625 try: 626 nonce_time, nonce_value, nonce_hash = nonce.split(':') 627 except ValueError: 628 return False 629 # use bytes.isalnum to avoid locale specific interpretation 630 if not str2bytes(nonce_time).isalnum() or \ 631 not str2bytes(nonce_value.replace("+", "").replace("/", "") \ 632 .replace("=", "")).isalnum(): 633 return False 634 token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret) 635 if ident is not None: 636 token = "%s:%s" % (token, ident) 637 token = md5hex(token) 638 if token != nonce_hash: 639 return False 640 641 if nonce_time < format_time(time.time() - self.maxage): 642 return False 643 644 cur = self.dbhandle.cursor() 645 #self._cleanup(cur) # avoid growing database 646 647 dbkey = "%s:%s" % (nonce_time, nonce_value) 648 cur.execute("SELECT value FROM %s WHERE key = '%s';" % 649 (self.table, dbkey)) 650 uses = cur.fetchone() 651 if uses is None: 652 self.dbhandle.commit() 653 return False 654 uses = int(uses[0]) 655 if count != uses: 656 cur.execute("DELETE FROM %s WHERE key = '%s';" % 657 (self.table, dbkey)) 658 self.dbhandle.commit() 659 return False 660 if uses >= self.maxuses: 661 cur.execute("DELETE FROM %s WHERE key = '%s';" % 662 (self.table, dbkey)) 663 else: 664 cur.execute("UPDATE %s SET value = '%d' WHERE key = '%s';" % 665 (self.table, uses + 1, dbkey)) 666 self.dbhandle.commit() 667 return True
668
669 -def check_uri(credentials, environ):
670 """internal method for verifying the uri credential 671 @raises AuthenticationRequired: 672 """ 673 # Doing this by stripping known parts from the passed uri field 674 # until something trivial remains, as the uri cannot be 675 # reconstructed from the environment exactly. 676 try: 677 uri = credentials["uri"] 678 except KeyError: 679 raise ProtocolViolation("uri missing in client credentials") 680 if environ.get("QUERY_STRING"): 681 if not uri.endswith(environ["QUERY_STRING"]): 682 raise AuthenticationRequired("url mismatch") 683 uri = uri[:-len(environ["QUERY_STRING"])] 684 if environ.get("SCRIPT_NAME"): 685 if not uri.startswith(environ["SCRIPT_NAME"]): 686 raise AuthenticationRequired("url mismatch") 687 uri = uri[len(environ["SCRIPT_NAME"]):] 688 if environ.get("PATH_INFO"): 689 if not uri.startswith(environ["PATH_INFO"]): 690 raise AuthenticationRequired("url mismatch") 691 uri = uri[len(environ["PATH_INFO"]):] 692 if uri not in ('', '?'): 693 raise AuthenticationRequired("url mismatch")
694 695 __all__.append("AuthDigestMiddleware")
696 -class AuthDigestMiddleware(AuthenticationMiddleware):
697 """Middleware partly implementing RFC2617. (md5-sess was omited) 698 Upon successful authentication the environ dict will be extended 699 by a REMOTE_USER key before being passed to the wrapped 700 application.""" 701 authorization_method = "digest" 702 algorithms = {"md5": md5hex}
703 - def __init__(self, app, gentoken, maxage=300, maxuses=5, store=None):
704 """ 705 @param app: is the wsgi application to be served with authentication. 706 @type gentoken: str -> (str or None) 707 @param gentoken: has to have the same functionality and interface as the 708 L{AuthTokenGenerator} class. 709 @type maxage: int 710 @param maxage: deprecated, see L{MemoryNonceStore} or 711 L{StatelessNonceStore} and pass an instance to store 712 @type maxuses: int 713 @param maxuses: deprecated, see L{MemoryNonceStore} and pass an 714 instance to store 715 @type store: L{NonceStoreBase} 716 @param store: a nonce storage implementation object. Usage of this 717 parameter will override maxage and maxuses. 718 """ 719 AuthenticationMiddleware.__init__(self, app) 720 self.gentoken = gentoken 721 if store is None: 722 self.noncestore = MemoryNonceStore(maxage, maxuses) 723 else: 724 assert hasattr(store, "newnonce") 725 assert hasattr(store, "checknonce") 726 self.noncestore = store
727
728 - def authenticate(self, auth, environ):
729 assert isinstance(auth, str) 730 try: 731 credentials = parse_digest_response(auth) 732 except ValueError: 733 raise ProtocolViolation("failed to parse digest response") 734 735 ### Check algorithm field 736 credentials["algorithm"] = credentials.get("algorithm", 737 "md5").lower() 738 if not credentials["algorithm"] in self.algorithms: 739 raise ProtocolViolation("algorithm not implemented: %r" % 740 credentials["algorithm"]) 741 742 check_uri(credentials, environ) 743 744 try: 745 nonce = credentials["nonce"] 746 credresponse = credentials["response"] 747 except KeyError as err: 748 raise ProtocolViolation("%s missing in credentials" % 749 err.args[0]) 750 noncecount = 1 751 if "qop" in credentials: 752 if credentials["qop"] != "auth": 753 raise ProtocolViolation("unimplemented qop: %r" % 754 credentials["qop"]) 755 try: 756 noncecount = int(credentials["nc"], 16) 757 except KeyError: 758 raise ProtocolViolation("nc missing in qop=auth") 759 except ValueError: 760 raise ProtocolViolation("non hexdigit found in nonce count") 761 762 # raises AuthenticationRequired 763 response = self.auth_response(credentials, 764 environ["REQUEST_METHOD"]) 765 766 if not self.noncestore.checknonce(nonce, noncecount): 767 raise StaleNonce() 768 769 if response is None or response != credresponse: 770 raise AuthenticationRequired("wrong response") 771 772 digest = dict(nextnonce=(self.noncestore.newnonce(), True)) 773 if "qop" in credentials: 774 digest["qop"] = ("auth", False) 775 digest["cnonce"] = (credentials["cnonce"], True) # no KeyError 776 digest["rspauth"] = (self.auth_response(credentials, ""), True) 777 return dict(user=credentials["username"], 778 outheaders=[("Authentication-Info", format_digest(digest))])
779
780 - def auth_response(self, credentials, reqmethod):
781 """internal method generating authentication tokens 782 @raises AuthenticationRequired: 783 """ 784 try: 785 username = credentials["username"] 786 algo = credentials["algorithm"] 787 uri = credentials["uri"] 788 except KeyError as err: 789 raise ProtocolViolation("%s missing in credentials" % err.args[0]) 790 try: 791 dig = [credentials["nonce"]] 792 except KeyError: 793 raise ProtocolViolation("missing nonce in credentials") 794 qop = credentials.get("qop") 795 if qop is not None: 796 if qop != "auth": 797 raise AuthenticationRequired("unimplemented qop: %r" % qop) 798 try: 799 dig.append(credentials["nc"]) 800 dig.append(credentials["cnonce"]) 801 except KeyError as err: 802 raise ProtocolViolation( 803 "missing %s in credentials with qop=auth" % err.args[0]) 804 dig.append(qop) 805 dig.append(self.algorithms[algo]("%s:%s" % (reqmethod, uri))) 806 try: 807 a1h = self.gentoken(username, algo) 808 except TypeError: 809 a1h = self.gentoken(username) 810 if a1h is None: 811 return None # delay the error for a nonexistent user 812 dig.insert(0, a1h) 813 return self.algorithms[algo](":".join(dig))
814
815 - def www_authenticate(self, exception):
816 digest = dict(nonce=(self.noncestore.newnonce(), True), 817 realm=(self.gentoken.realm, True), 818 algorithm=("MD5", False), 819 qop=("auth", False)) 820 if isinstance(exception, StaleNonce): 821 digest["stale"] = ("TRUE", False) 822 challenge = format_digest(digest) 823 return ("WWW-Authenticate", "Digest %s" % challenge)
824