| 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
|
|
|
|
|