Index: third_party/cherrypy/lib/httpauth.py |
=================================================================== |
--- third_party/cherrypy/lib/httpauth.py (revision 0) |
+++ third_party/cherrypy/lib/httpauth.py (revision 0) |
@@ -0,0 +1,354 @@ |
+""" |
+This module defines functions to implement HTTP Digest Authentication (:rfc:`2617`). |
+This has full compliance with 'Digest' and 'Basic' authentication methods. In |
+'Digest' it supports both MD5 and MD5-sess algorithms. |
+ |
+Usage: |
+ First use 'doAuth' to request the client authentication for a |
+ certain resource. You should send an httplib.UNAUTHORIZED response to the |
+ client so he knows he has to authenticate itself. |
+ |
+ Then use 'parseAuthorization' to retrieve the 'auth_map' used in |
+ 'checkResponse'. |
+ |
+ To use 'checkResponse' you must have already verified the password associated |
+ with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse' |
+ function to verify if the password matches the one sent by the client. |
+ |
+SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms |
+SUPPORTED_QOP - list of supported 'Digest' 'qop'. |
+""" |
+__version__ = 1, 0, 1 |
+__author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>" |
+__credits__ = """ |
+ Peter van Kampen for its recipe which implement most of Digest authentication: |
+ http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378 |
+""" |
+ |
+__license__ = """ |
+Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net> |
+All rights reserved. |
+ |
+Redistribution and use in source and binary forms, with or without modification, |
+are permitted provided that the following conditions are met: |
+ |
+ * Redistributions of source code must retain the above copyright notice, |
+ this list of conditions and the following disclaimer. |
+ * Redistributions in binary form must reproduce the above copyright notice, |
+ this list of conditions and the following disclaimer in the documentation |
+ and/or other materials provided with the distribution. |
+ * Neither the name of Sylvain Hellegouarch nor the names of his contributors |
+ may be used to endorse or promote products derived from this software |
+ without specific prior written permission. |
+ |
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE |
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
+""" |
+ |
+__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse", |
+ "parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey", |
+ "calculateNonce", "SUPPORTED_QOP") |
+ |
+################################################################################ |
+import time |
+from cherrypy._cpcompat import base64_decode, ntob, md5 |
+from cherrypy._cpcompat import parse_http_list, parse_keqv_list |
+ |
+MD5 = "MD5" |
+MD5_SESS = "MD5-sess" |
+AUTH = "auth" |
+AUTH_INT = "auth-int" |
+ |
+SUPPORTED_ALGORITHM = (MD5, MD5_SESS) |
+SUPPORTED_QOP = (AUTH, AUTH_INT) |
+ |
+################################################################################ |
+# doAuth |
+# |
+DIGEST_AUTH_ENCODERS = { |
+ MD5: lambda val: md5(ntob(val)).hexdigest(), |
+ MD5_SESS: lambda val: md5(ntob(val)).hexdigest(), |
+# SHA: lambda val: sha.new(ntob(val)).hexdigest (), |
+} |
+ |
+def calculateNonce (realm, algorithm = MD5): |
+ """This is an auxaliary function that calculates 'nonce' value. It is used |
+ to handle sessions.""" |
+ |
+ global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS |
+ assert algorithm in SUPPORTED_ALGORITHM |
+ |
+ try: |
+ encoder = DIGEST_AUTH_ENCODERS[algorithm] |
+ except KeyError: |
+ raise NotImplementedError ("The chosen algorithm (%s) does not have "\ |
+ "an implementation yet" % algorithm) |
+ |
+ return encoder ("%d:%s" % (time.time(), realm)) |
+ |
+def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH): |
+ """Challenges the client for a Digest authentication.""" |
+ global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP |
+ assert algorithm in SUPPORTED_ALGORITHM |
+ assert qop in SUPPORTED_QOP |
+ |
+ if nonce is None: |
+ nonce = calculateNonce (realm, algorithm) |
+ |
+ return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % ( |
+ realm, nonce, algorithm, qop |
+ ) |
+ |
+def basicAuth (realm): |
+ """Challengenes the client for a Basic authentication.""" |
+ assert '"' not in realm, "Realms cannot contain the \" (quote) character." |
+ |
+ return 'Basic realm="%s"' % realm |
+ |
+def doAuth (realm): |
+ """'doAuth' function returns the challenge string b giving priority over |
+ Digest and fallback to Basic authentication when the browser doesn't |
+ support the first one. |
+ |
+ This should be set in the HTTP header under the key 'WWW-Authenticate'.""" |
+ |
+ return digestAuth (realm) + " " + basicAuth (realm) |
+ |
+ |
+################################################################################ |
+# Parse authorization parameters |
+# |
+def _parseDigestAuthorization (auth_params): |
+ # Convert the auth params to a dict |
+ items = parse_http_list(auth_params) |
+ params = parse_keqv_list(items) |
+ |
+ # Now validate the params |
+ |
+ # Check for required parameters |
+ required = ["username", "realm", "nonce", "uri", "response"] |
+ for k in required: |
+ if k not in params: |
+ return None |
+ |
+ # If qop is sent then cnonce and nc MUST be present |
+ if "qop" in params and not ("cnonce" in params \ |
+ and "nc" in params): |
+ return None |
+ |
+ # If qop is not sent, neither cnonce nor nc can be present |
+ if ("cnonce" in params or "nc" in params) and \ |
+ "qop" not in params: |
+ return None |
+ |
+ return params |
+ |
+ |
+def _parseBasicAuthorization (auth_params): |
+ username, password = base64_decode(auth_params).split(":", 1) |
+ return {"username": username, "password": password} |
+ |
+AUTH_SCHEMES = { |
+ "basic": _parseBasicAuthorization, |
+ "digest": _parseDigestAuthorization, |
+} |
+ |
+def parseAuthorization (credentials): |
+ """parseAuthorization will convert the value of the 'Authorization' key in |
+ the HTTP header to a map itself. If the parsing fails 'None' is returned. |
+ """ |
+ |
+ global AUTH_SCHEMES |
+ |
+ auth_scheme, auth_params = credentials.split(" ", 1) |
+ auth_scheme = auth_scheme.lower () |
+ |
+ parser = AUTH_SCHEMES[auth_scheme] |
+ params = parser (auth_params) |
+ |
+ if params is None: |
+ return |
+ |
+ assert "auth_scheme" not in params |
+ params["auth_scheme"] = auth_scheme |
+ return params |
+ |
+ |
+################################################################################ |
+# Check provided response for a valid password |
+# |
+def md5SessionKey (params, password): |
+ """ |
+ If the "algorithm" directive's value is "MD5-sess", then A1 |
+ [the session key] is calculated only once - on the first request by the |
+ client following receipt of a WWW-Authenticate challenge from the server. |
+ |
+ This creates a 'session key' for the authentication of subsequent |
+ requests and responses which is different for each "authentication |
+ session", thus limiting the amount of material hashed with any one |
+ key. |
+ |
+ Because the server need only use the hash of the user |
+ credentials in order to create the A1 value, this construction could |
+ be used in conjunction with a third party authentication service so |
+ that the web server would not need the actual password value. The |
+ specification of such a protocol is beyond the scope of this |
+ specification. |
+""" |
+ |
+ keys = ("username", "realm", "nonce", "cnonce") |
+ params_copy = {} |
+ for key in keys: |
+ params_copy[key] = params[key] |
+ |
+ params_copy["algorithm"] = MD5_SESS |
+ return _A1 (params_copy, password) |
+ |
+def _A1(params, password): |
+ algorithm = params.get ("algorithm", MD5) |
+ H = DIGEST_AUTH_ENCODERS[algorithm] |
+ |
+ if algorithm == MD5: |
+ # If the "algorithm" directive's value is "MD5" or is |
+ # unspecified, then A1 is: |
+ # A1 = unq(username-value) ":" unq(realm-value) ":" passwd |
+ return "%s:%s:%s" % (params["username"], params["realm"], password) |
+ |
+ elif algorithm == MD5_SESS: |
+ |
+ # This is A1 if qop is set |
+ # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) |
+ # ":" unq(nonce-value) ":" unq(cnonce-value) |
+ h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password)) |
+ return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"]) |
+ |
+ |
+def _A2(params, method, kwargs): |
+ # If the "qop" directive's value is "auth" or is unspecified, then A2 is: |
+ # A2 = Method ":" digest-uri-value |
+ |
+ qop = params.get ("qop", "auth") |
+ if qop == "auth": |
+ return method + ":" + params["uri"] |
+ elif qop == "auth-int": |
+ # If the "qop" value is "auth-int", then A2 is: |
+ # A2 = Method ":" digest-uri-value ":" H(entity-body) |
+ entity_body = kwargs.get ("entity_body", "") |
+ H = kwargs["H"] |
+ |
+ return "%s:%s:%s" % ( |
+ method, |
+ params["uri"], |
+ H(entity_body) |
+ ) |
+ |
+ else: |
+ raise NotImplementedError ("The 'qop' method is unknown: %s" % qop) |
+ |
+def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs): |
+ """ |
+ Generates a response respecting the algorithm defined in RFC 2617 |
+ """ |
+ params = auth_map |
+ |
+ algorithm = params.get ("algorithm", MD5) |
+ |
+ H = DIGEST_AUTH_ENCODERS[algorithm] |
+ KD = lambda secret, data: H(secret + ":" + data) |
+ |
+ qop = params.get ("qop", None) |
+ |
+ H_A2 = H(_A2(params, method, kwargs)) |
+ |
+ if algorithm == MD5_SESS and A1 is not None: |
+ H_A1 = H(A1) |
+ else: |
+ H_A1 = H(_A1(params, password)) |
+ |
+ if qop in ("auth", "auth-int"): |
+ # If the "qop" value is "auth" or "auth-int": |
+ # request-digest = <"> < KD ( H(A1), unq(nonce-value) |
+ # ":" nc-value |
+ # ":" unq(cnonce-value) |
+ # ":" unq(qop-value) |
+ # ":" H(A2) |
+ # ) <"> |
+ request = "%s:%s:%s:%s:%s" % ( |
+ params["nonce"], |
+ params["nc"], |
+ params["cnonce"], |
+ params["qop"], |
+ H_A2, |
+ ) |
+ elif qop is None: |
+ # If the "qop" directive is not present (this construction is |
+ # for compatibility with RFC 2069): |
+ # request-digest = |
+ # <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> |
+ request = "%s:%s" % (params["nonce"], H_A2) |
+ |
+ return KD(H_A1, request) |
+ |
+def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs): |
+ """This function is used to verify the response given by the client when |
+ he tries to authenticate. |
+ Optional arguments: |
+ entity_body - when 'qop' is set to 'auth-int' you MUST provide the |
+ raw data you are going to send to the client (usually the |
+ HTML page. |
+ request_uri - the uri from the request line compared with the 'uri' |
+ directive of the authorization map. They must represent |
+ the same resource (unused at this time). |
+ """ |
+ |
+ if auth_map['realm'] != kwargs.get('realm', None): |
+ return False |
+ |
+ response = _computeDigestResponse(auth_map, password, method, A1,**kwargs) |
+ |
+ return response == auth_map["response"] |
+ |
+def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs): |
+ # Note that the Basic response doesn't provide the realm value so we cannot |
+ # test it |
+ try: |
+ return encrypt(auth_map["password"], auth_map["username"]) == password |
+ except TypeError: |
+ return encrypt(auth_map["password"]) == password |
+ |
+AUTH_RESPONSES = { |
+ "basic": _checkBasicResponse, |
+ "digest": _checkDigestResponse, |
+} |
+ |
+def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs): |
+ """'checkResponse' compares the auth_map with the password and optionally |
+ other arguments that each implementation might need. |
+ |
+ If the response is of type 'Basic' then the function has the following |
+ signature:: |
+ |
+ checkBasicResponse (auth_map, password) -> bool |
+ |
+ If the response is of type 'Digest' then the function has the following |
+ signature:: |
+ |
+ checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool |
+ |
+ The 'A1' argument is only used in MD5_SESS algorithm based responses. |
+ Check md5SessionKey() for more info. |
+ """ |
+ checker = AUTH_RESPONSES[auth_map["auth_scheme"]] |
+ return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs) |
+ |
+ |
+ |
+ |
Property changes on: third_party/cherrypy/lib/httpauth.py |
___________________________________________________________________ |
Added: svn:eol-style |
+ LF |