PageRenderTime 20ms CodeModel.GetById 2ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 1ms

/circuits/web/_httpauth.py

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