Package cherrypy :: Package lib :: Module httpauth
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.lib.httpauth

  1  """ 
  2  This module defines functions to implement HTTP Digest Authentication 
  3  (:rfc:`2617`). 
  4  This has full compliance with 'Digest' and 'Basic' authentication methods. In 
  5  'Digest' it supports both MD5 and MD5-sess algorithms. 
  6   
  7  Usage: 
  8      First use 'doAuth' to request the client authentication for a 
  9      certain resource. You should send an httplib.UNAUTHORIZED response to the 
 10      client so he knows he has to authenticate itself. 
 11   
 12      Then use 'parseAuthorization' to retrieve the 'auth_map' used in 
 13      'checkResponse'. 
 14   
 15      To use 'checkResponse' you must have already verified the password 
 16      associated with the 'username' key in 'auth_map' dict. Then you use the 
 17      'checkResponse' function to verify if the password matches the one sent 
 18      by the client. 
 19   
 20  SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms 
 21  SUPPORTED_QOP - list of supported 'Digest' 'qop'. 
 22  """ 
 23  __version__ = 1, 0, 1 
 24  __author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>" 
 25  __credits__ = """ 
 26      Peter van Kampen for its recipe which implement most of Digest 
 27      authentication: 
 28      http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378 
 29  """ 
 30   
 31  __license__ = """ 
 32  Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net> 
 33  All rights reserved. 
 34   
 35  Redistribution and use in source and binary forms, with or without 
 36  modification, are permitted provided that the following conditions are met: 
 37   
 38      * Redistributions of source code must retain the above copyright notice, 
 39        this list of conditions and the following disclaimer. 
 40      * Redistributions in binary form must reproduce the above copyright notice, 
 41        this list of conditions and the following disclaimer in the documentation 
 42        and/or other materials provided with the distribution. 
 43      * Neither the name of Sylvain Hellegouarch nor the names of his 
 44        contributors may be used to endorse or promote products derived from 
 45        this software without specific prior written permission. 
 46   
 47  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 48  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 49  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 50  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
 51  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 52  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 53  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 54  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 55  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
 56  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 57  """ 
 58   
 59  __all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse", 
 60             "parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey", 
 61             "calculateNonce", "SUPPORTED_QOP") 
 62   
 63  ########################################################################## 
 64  import time 
 65  from cherrypy._cpcompat import base64_decode, ntob, md5 
 66  from cherrypy._cpcompat import parse_http_list, parse_keqv_list 
 67   
 68  MD5 = "MD5" 
 69  MD5_SESS = "MD5-sess" 
 70  AUTH = "auth" 
 71  AUTH_INT = "auth-int" 
 72   
 73  SUPPORTED_ALGORITHM = (MD5, MD5_SESS) 
 74  SUPPORTED_QOP = (AUTH, AUTH_INT) 
 75   
 76  ########################################################################## 
 77  # doAuth 
 78  # 
 79  DIGEST_AUTH_ENCODERS = { 
 80      MD5: lambda val: md5(ntob(val)).hexdigest(), 
 81      MD5_SESS: lambda val: md5(ntob(val)).hexdigest(), 
 82      #    SHA: lambda val: sha.new(ntob(val)).hexdigest (), 
 83  } 
 84   
 85   
86 -def calculateNonce(realm, algorithm=MD5):
87 """This is an auxaliary function that calculates 'nonce' value. It is used 88 to handle sessions.""" 89 90 global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS 91 assert algorithm in SUPPORTED_ALGORITHM 92 93 try: 94 encoder = DIGEST_AUTH_ENCODERS[algorithm] 95 except KeyError: 96 raise NotImplementedError("The chosen algorithm (%s) does not have " 97 "an implementation yet" % algorithm) 98 99 return encoder("%d:%s" % (time.time(), realm))
100 101
102 -def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
103 """Challenges the client for a Digest authentication.""" 104 global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP 105 assert algorithm in SUPPORTED_ALGORITHM 106 assert qop in SUPPORTED_QOP 107 108 if nonce is None: 109 nonce = calculateNonce(realm, algorithm) 110 111 return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % ( 112 realm, nonce, algorithm, qop 113 )
114 115
116 -def basicAuth(realm):
117 """Challengenes the client for a Basic authentication.""" 118 assert '"' not in realm, "Realms cannot contain the \" (quote) character." 119 120 return 'Basic realm="%s"' % realm
121 122
123 -def doAuth(realm):
124 """'doAuth' function returns the challenge string b giving priority over 125 Digest and fallback to Basic authentication when the browser doesn't 126 support the first one. 127 128 This should be set in the HTTP header under the key 'WWW-Authenticate'.""" 129 130 return digestAuth(realm) + " " + basicAuth(realm)
131 132 133 ########################################################################## 134 # Parse authorization parameters 135 #
136 -def _parseDigestAuthorization(auth_params):
137 # Convert the auth params to a dict 138 items = parse_http_list(auth_params) 139 params = parse_keqv_list(items) 140 141 # Now validate the params 142 143 # Check for required parameters 144 required = ["username", "realm", "nonce", "uri", "response"] 145 for k in required: 146 if k not in params: 147 return None 148 149 # If qop is sent then cnonce and nc MUST be present 150 if "qop" in params and not ("cnonce" in params 151 and "nc" in params): 152 return None 153 154 # If qop is not sent, neither cnonce nor nc can be present 155 if ("cnonce" in params or "nc" in params) and \ 156 "qop" not in params: 157 return None 158 159 return params
160 161
162 -def _parseBasicAuthorization(auth_params):
163 username, password = base64_decode(auth_params).split(":", 1) 164 return {"username": username, "password": password}
165 166 AUTH_SCHEMES = { 167 "basic": _parseBasicAuthorization, 168 "digest": _parseDigestAuthorization, 169 } 170 171
172 -def parseAuthorization(credentials):
173 """parseAuthorization will convert the value of the 'Authorization' key in 174 the HTTP header to a map itself. If the parsing fails 'None' is returned. 175 """ 176 177 global AUTH_SCHEMES 178 179 auth_scheme, auth_params = credentials.split(" ", 1) 180 auth_scheme = auth_scheme.lower() 181 182 parser = AUTH_SCHEMES[auth_scheme] 183 params = parser(auth_params) 184 185 if params is None: 186 return 187 188 assert "auth_scheme" not in params 189 params["auth_scheme"] = auth_scheme 190 return params
191 192 193 ########################################################################## 194 # Check provided response for a valid password 195 #
196 -def md5SessionKey(params, password):
197 """ 198 If the "algorithm" directive's value is "MD5-sess", then A1 199 [the session key] is calculated only once - on the first request by the 200 client following receipt of a WWW-Authenticate challenge from the server. 201 202 This creates a 'session key' for the authentication of subsequent 203 requests and responses which is different for each "authentication 204 session", thus limiting the amount of material hashed with any one 205 key. 206 207 Because the server need only use the hash of the user 208 credentials in order to create the A1 value, this construction could 209 be used in conjunction with a third party authentication service so 210 that the web server would not need the actual password value. The 211 specification of such a protocol is beyond the scope of this 212 specification. 213 """ 214 215 keys = ("username", "realm", "nonce", "cnonce") 216 params_copy = {} 217 for key in keys: 218 params_copy[key] = params[key] 219 220 params_copy["algorithm"] = MD5_SESS 221 return _A1(params_copy, password)
222 223
224 -def _A1(params, password):
225 algorithm = params.get("algorithm", MD5) 226 H = DIGEST_AUTH_ENCODERS[algorithm] 227 228 if algorithm == MD5: 229 # If the "algorithm" directive's value is "MD5" or is 230 # unspecified, then A1 is: 231 # A1 = unq(username-value) ":" unq(realm-value) ":" passwd 232 return "%s:%s:%s" % (params["username"], params["realm"], password) 233 234 elif algorithm == MD5_SESS: 235 236 # This is A1 if qop is set 237 # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) 238 # ":" unq(nonce-value) ":" unq(cnonce-value) 239 h_a1 = H("%s:%s:%s" % (params["username"], params["realm"], password)) 240 return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
241 242
243 -def _A2(params, method, kwargs):
244 # If the "qop" directive's value is "auth" or is unspecified, then A2 is: 245 # A2 = Method ":" digest-uri-value 246 247 qop = params.get("qop", "auth") 248 if qop == "auth": 249 return method + ":" + params["uri"] 250 elif qop == "auth-int": 251 # If the "qop" value is "auth-int", then A2 is: 252 # A2 = Method ":" digest-uri-value ":" H(entity-body) 253 entity_body = kwargs.get("entity_body", "") 254 H = kwargs["H"] 255 256 return "%s:%s:%s" % ( 257 method, 258 params["uri"], 259 H(entity_body) 260 ) 261 262 else: 263 raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
264 265
266 -def _computeDigestResponse(auth_map, password, method="GET", A1=None, 267 **kwargs):
268 """ 269 Generates a response respecting the algorithm defined in RFC 2617 270 """ 271 params = auth_map 272 273 algorithm = params.get("algorithm", MD5) 274 275 H = DIGEST_AUTH_ENCODERS[algorithm] 276 KD = lambda secret, data: H(secret + ":" + data) 277 278 qop = params.get("qop", None) 279 280 H_A2 = H(_A2(params, method, kwargs)) 281 282 if algorithm == MD5_SESS and A1 is not None: 283 H_A1 = H(A1) 284 else: 285 H_A1 = H(_A1(params, password)) 286 287 if qop in ("auth", "auth-int"): 288 # If the "qop" value is "auth" or "auth-int": 289 # request-digest = <"> < KD ( H(A1), unq(nonce-value) 290 # ":" nc-value 291 # ":" unq(cnonce-value) 292 # ":" unq(qop-value) 293 # ":" H(A2) 294 # ) <"> 295 request = "%s:%s:%s:%s:%s" % ( 296 params["nonce"], 297 params["nc"], 298 params["cnonce"], 299 params["qop"], 300 H_A2, 301 ) 302 elif qop is None: 303 # If the "qop" directive is not present (this construction is 304 # for compatibility with RFC 2069): 305 # request-digest = 306 # <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> 307 request = "%s:%s" % (params["nonce"], H_A2) 308 309 return KD(H_A1, request)
310 311
312 -def _checkDigestResponse(auth_map, password, method="GET", A1=None, **kwargs):
313 """This function is used to verify the response given by the client when 314 he tries to authenticate. 315 Optional arguments: 316 entity_body - when 'qop' is set to 'auth-int' you MUST provide the 317 raw data you are going to send to the client (usually the 318 HTML page. 319 request_uri - the uri from the request line compared with the 'uri' 320 directive of the authorization map. They must represent 321 the same resource (unused at this time). 322 """ 323 324 if auth_map['realm'] != kwargs.get('realm', None): 325 return False 326 327 response = _computeDigestResponse( 328 auth_map, password, method, A1, **kwargs) 329 330 return response == auth_map["response"]
331 332
333 -def _checkBasicResponse(auth_map, password, method='GET', encrypt=None, 334 **kwargs):
335 # Note that the Basic response doesn't provide the realm value so we cannot 336 # test it 337 try: 338 return encrypt(auth_map["password"], auth_map["username"]) == password 339 except TypeError: 340 return encrypt(auth_map["password"]) == password
341 342 AUTH_RESPONSES = { 343 "basic": _checkBasicResponse, 344 "digest": _checkDigestResponse, 345 } 346 347
348 -def checkResponse(auth_map, password, method="GET", encrypt=None, **kwargs):
349 """'checkResponse' compares the auth_map with the password and optionally 350 other arguments that each implementation might need. 351 352 If the response is of type 'Basic' then the function has the following 353 signature:: 354 355 checkBasicResponse(auth_map, password) -> bool 356 357 If the response is of type 'Digest' then the function has the following 358 signature:: 359 360 checkDigestResponse(auth_map, password, method='GET', A1=None) -> bool 361 362 The 'A1' argument is only used in MD5_SESS algorithm based responses. 363 Check md5SessionKey() for more info. 364 """ 365 checker = AUTH_RESPONSES[auth_map["auth_scheme"]] 366 return checker(auth_map, password, method=method, encrypt=encrypt, 367 **kwargs)
368