auth_digest.py (7628B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\carbon\common\lib\cherrypy\lib\auth_digest.py 2 """An implementation of the server-side of HTTP Digest Access 3 Authentication, which is described in :rfc:`2617`. 4 5 Example usage, using the built-in get_ha1_dict_plain function which uses a dict 6 of plaintext passwords as the credentials store:: 7 8 userpassdict = {'alice' : '4x5istwelve'} 9 get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) 10 digest_auth = {'tools.auth_digest.on': True, 11 'tools.auth_digest.realm': 'wonderland', 12 'tools.auth_digest.get_ha1': get_ha1, 13 'tools.auth_digest.key': 'a565c27146791cfb', 14 } 15 app_config = { '/' : digest_auth } 16 """ 17 __author__ = 'visteya' 18 __date__ = 'April 2009' 19 import time 20 from cherrypy._cpcompat import parse_http_list, parse_keqv_list 21 import cherrypy 22 from cherrypy._cpcompat import md5, ntob 23 md5_hex = lambda s: md5(ntob(s)).hexdigest() 24 qop_auth = 'auth' 25 qop_auth_int = 'auth-int' 26 valid_qops = (qop_auth, qop_auth_int) 27 valid_algorithms = ('MD5', 'MD5-sess') 28 29 def TRACE(msg): 30 cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') 31 32 33 def get_ha1_dict_plain(user_password_dict): 34 35 def get_ha1(realm, username): 36 password = user_password_dict.get(username) 37 if password: 38 return md5_hex('%s:%s:%s' % (username, realm, password)) 39 40 return get_ha1 41 42 43 def get_ha1_dict(user_ha1_dict): 44 45 def get_ha1(realm, username): 46 return user_ha1_dict.get(user) 47 48 return get_ha1 49 50 51 def get_ha1_file_htdigest(filename): 52 53 def get_ha1(realm, username): 54 result = None 55 f = open(filename, 'r') 56 for line in f: 57 u, r, ha1 = line.rstrip().split(':') 58 if u == username and r == realm: 59 result = ha1 60 break 61 62 f.close() 63 return result 64 65 return get_ha1 66 67 68 def synthesize_nonce(s, key, timestamp = None): 69 if timestamp is None: 70 timestamp = int(time.time()) 71 h = md5_hex('%s:%s:%s' % (timestamp, s, key)) 72 nonce = '%s:%s' % (timestamp, h) 73 return nonce 74 75 76 def H(s): 77 return md5_hex(s) 78 79 80 class HttpDigestAuthorization(object): 81 82 def errmsg(self, s): 83 return 'Digest Authorization header: %s' % s 84 85 def __init__(self, auth_header, http_method, debug = False): 86 self.http_method = http_method 87 self.debug = debug 88 scheme, params = auth_header.split(' ', 1) 89 self.scheme = scheme.lower() 90 if self.scheme != 'digest': 91 raise ValueError('Authorization scheme is not "Digest"') 92 self.auth_header = auth_header 93 items = parse_http_list(params) 94 paramsd = parse_keqv_list(items) 95 self.realm = paramsd.get('realm') 96 self.username = paramsd.get('username') 97 self.nonce = paramsd.get('nonce') 98 self.uri = paramsd.get('uri') 99 self.method = paramsd.get('method') 100 self.response = paramsd.get('response') 101 self.algorithm = paramsd.get('algorithm', 'MD5') 102 self.cnonce = paramsd.get('cnonce') 103 self.opaque = paramsd.get('opaque') 104 self.qop = paramsd.get('qop') 105 self.nc = paramsd.get('nc') 106 if self.algorithm not in valid_algorithms: 107 raise ValueError(self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm)) 108 has_reqd = self.username and self.realm and self.nonce and self.uri and self.response 109 if not has_reqd: 110 raise ValueError(self.errmsg('Not all required parameters are present.')) 111 if self.qop: 112 if self.qop not in valid_qops: 113 raise ValueError(self.errmsg("Unsupported value for qop: '%s'" % self.qop)) 114 if not (self.cnonce and self.nc): 115 raise ValueError(self.errmsg('If qop is sent then cnonce and nc MUST be present')) 116 elif self.cnonce or self.nc: 117 raise ValueError(self.errmsg('If qop is not sent, neither cnonce nor nc can be present')) 118 119 def __str__(self): 120 return 'authorization : %s' % self.auth_header 121 122 def validate_nonce(self, s, key): 123 try: 124 timestamp, hashpart = self.nonce.split(':', 1) 125 s_timestamp, s_hashpart = synthesize_nonce(s, key, timestamp).split(':', 1) 126 is_valid = s_hashpart == hashpart 127 if self.debug: 128 TRACE('validate_nonce: %s' % is_valid) 129 return is_valid 130 except ValueError: 131 pass 132 133 return False 134 135 def is_nonce_stale(self, max_age_seconds = 600): 136 try: 137 timestamp, hashpart = self.nonce.split(':', 1) 138 if int(timestamp) + max_age_seconds > int(time.time()): 139 return False 140 except ValueError: 141 pass 142 143 if self.debug: 144 TRACE('nonce is stale') 145 return True 146 147 def HA2(self, entity_body = ''): 148 if self.qop is None or self.qop == 'auth': 149 a2 = '%s:%s' % (self.http_method, self.uri) 150 elif self.qop == 'auth-int': 151 a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body)) 152 else: 153 raise ValueError(self.errmsg('Unrecognized value for qop!')) 154 return H(a2) 155 156 def request_digest(self, ha1, entity_body = ''): 157 ha2 = self.HA2(entity_body) 158 if self.qop: 159 req = '%s:%s:%s:%s:%s' % (self.nonce, 160 self.nc, 161 self.cnonce, 162 self.qop, 163 ha2) 164 else: 165 req = '%s:%s' % (self.nonce, ha2) 166 if self.algorithm == 'MD5-sess': 167 ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) 168 digest = H('%s:%s' % (ha1, req)) 169 return digest 170 171 172 def www_authenticate(realm, key, algorithm = 'MD5', nonce = None, qop = qop_auth, stale = False): 173 if qop not in valid_qops: 174 raise ValueError("Unsupported value for qop: '%s'" % qop) 175 if algorithm not in valid_algorithms: 176 raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) 177 if nonce is None: 178 nonce = synthesize_nonce(realm, key) 179 s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (realm, 180 nonce, 181 algorithm, 182 qop) 183 if stale: 184 s += ', stale="true"' 185 return s 186 187 188 def digest_auth(realm, get_ha1, key, debug = False): 189 request = cherrypy.serving.request 190 auth_header = request.headers.get('authorization') 191 nonce_is_stale = False 192 if auth_header is not None: 193 try: 194 auth = HttpDigestAuthorization(auth_header, request.method, debug=debug) 195 except ValueError: 196 raise cherrypy.HTTPError(400, 'The Authorization header could not be parsed.') 197 198 if debug: 199 TRACE(str(auth)) 200 if auth.validate_nonce(realm, key): 201 ha1 = get_ha1(realm, auth.username) 202 if ha1 is not None: 203 digest = auth.request_digest(ha1, entity_body=request.body) 204 if digest == auth.response: 205 if debug: 206 TRACE('digest matches auth.response') 207 nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600) 208 if not nonce_is_stale: 209 request.login = auth.username 210 if debug: 211 TRACE('authentication of %s successful' % auth.username) 212 return 213 header = www_authenticate(realm, key, stale=nonce_is_stale) 214 if debug: 215 TRACE(header) 216 cherrypy.serving.response.headers['WWW-Authenticate'] = header 217 raise cherrypy.HTTPError(401, 'You are not authorized to access that resource')