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

Source Code for Module cherrypy.lib.httpauth

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