~j5-dev/+junk/cherrypy3-3.2.0rc1

« back to all changes in this revision

Viewing changes to cherrypy/lib/httpauth.py

  • Committer: steveh at sjsoft
  • Date: 2010-07-01 13:07:15 UTC
  • Revision ID: steveh@sjsoft.com-20100701130715-w56oim8346qzqlka
New upstream release

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
try:
 
63
    # Python 2.5+
 
64
    from hashlib import md5
 
65
except ImportError:
 
66
    from md5 import new as md5
 
67
import time
 
68
import base64
 
69
from urllib2 import parse_http_list, parse_keqv_list
 
70
 
 
71
MD5 = "MD5"
 
72
MD5_SESS = "MD5-sess"
 
73
AUTH = "auth"
 
74
AUTH_INT = "auth-int"
 
75
 
 
76
SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
 
77
SUPPORTED_QOP = (AUTH, AUTH_INT)
 
78
 
 
79
################################################################################
 
80
# doAuth
 
81
#
 
82
DIGEST_AUTH_ENCODERS = {
 
83
    MD5: lambda val: md5(val).hexdigest(),
 
84
    MD5_SESS: lambda val: md5(val).hexdigest(),
 
85
#    SHA: lambda val: sha.new (val).hexdigest (),
 
86
}
 
87
 
 
88
def calculateNonce (realm, algorithm = MD5):
 
89
    """This is an auxaliary function that calculates 'nonce' value. It is used
 
90
    to handle sessions."""
 
91
 
 
92
    global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS
 
93
    assert algorithm in SUPPORTED_ALGORITHM
 
94
 
 
95
    try:
 
96
        encoder = DIGEST_AUTH_ENCODERS[algorithm]
 
97
    except KeyError:
 
98
        raise NotImplementedError ("The chosen algorithm (%s) does not have "\
 
99
                                   "an implementation yet" % algorithm)
 
100
 
 
101
    return encoder ("%d:%s" % (time.time(), realm))
 
102
 
 
103
def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH):
 
104
    """Challenges the client for a Digest authentication."""
 
105
    global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
 
106
    assert algorithm in SUPPORTED_ALGORITHM
 
107
    assert qop in SUPPORTED_QOP
 
108
 
 
109
    if nonce is None:
 
110
        nonce = calculateNonce (realm, algorithm)
 
111
 
 
112
    return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
 
113
        realm, nonce, algorithm, qop
 
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
def doAuth (realm):
 
123
    """'doAuth' function returns the challenge string b giving priority over
 
124
    Digest and fallback to Basic authentication when the browser doesn't
 
125
    support the first one.
 
126
    
 
127
    This should be set in the HTTP header under the key 'WWW-Authenticate'."""
 
128
 
 
129
    return digestAuth (realm) + " " + basicAuth (realm)
 
130
 
 
131
 
 
132
################################################################################
 
133
# Parse authorization parameters
 
134
#
 
135
def _parseDigestAuthorization (auth_params):
 
136
    # Convert the auth params to a dict
 
137
    items = parse_http_list(auth_params)
 
138
    params = parse_keqv_list(items)
 
139
 
 
140
    # Now validate the params
 
141
 
 
142
    # Check for required parameters
 
143
    required = ["username", "realm", "nonce", "uri", "response"]
 
144
    for k in required:
 
145
        if k not in params:
 
146
            return None
 
147
 
 
148
    # If qop is sent then cnonce and nc MUST be present
 
149
    if "qop" in params and not ("cnonce" in params \
 
150
                                      and "nc" in params):
 
151
        return None
 
152
 
 
153
    # If qop is not sent, neither cnonce nor nc can be present
 
154
    if ("cnonce" in params or "nc" in params) and \
 
155
       "qop" not in params:
 
156
        return None
 
157
 
 
158
    return params
 
159
 
 
160
 
 
161
def _parseBasicAuthorization (auth_params):
 
162
    username, password = base64.decodestring (auth_params).split (":", 1)
 
163
    return {"username": username, "password": password}
 
164
 
 
165
AUTH_SCHEMES = {
 
166
    "basic": _parseBasicAuthorization,
 
167
    "digest": _parseDigestAuthorization,
 
168
}
 
169
 
 
170
def parseAuthorization (credentials):
 
171
    """parseAuthorization will convert the value of the 'Authorization' key in
 
172
    the HTTP header to a map itself. If the parsing fails 'None' is returned.
 
173
    """
 
174
 
 
175
    global AUTH_SCHEMES
 
176
 
 
177
    auth_scheme, auth_params  = credentials.split(" ", 1)
 
178
    auth_scheme = auth_scheme.lower ()
 
179
 
 
180
    parser = AUTH_SCHEMES[auth_scheme]
 
181
    params = parser (auth_params)
 
182
 
 
183
    if params is None:
 
184
        return
 
185
 
 
186
    assert "auth_scheme" not in params
 
187
    params["auth_scheme"] = auth_scheme
 
188
    return params
 
189
 
 
190
 
 
191
################################################################################
 
192
# Check provided response for a valid password
 
193
#
 
194
def md5SessionKey (params, password):
 
195
    """
 
196
    If the "algorithm" directive's value is "MD5-sess", then A1 
 
197
    [the session key] is calculated only once - on the first request by the
 
198
    client following receipt of a WWW-Authenticate challenge from the server.
 
199
 
 
200
    This creates a 'session key' for the authentication of subsequent
 
201
    requests and responses which is different for each "authentication
 
202
    session", thus limiting the amount of material hashed with any one
 
203
    key.
 
204
 
 
205
    Because the server need only use the hash of the user
 
206
    credentials in order to create the A1 value, this construction could
 
207
    be used in conjunction with a third party authentication service so
 
208
    that the web server would not need the actual password value.  The
 
209
    specification of such a protocol is beyond the scope of this
 
210
    specification.
 
211
"""
 
212
 
 
213
    keys = ("username", "realm", "nonce", "cnonce")
 
214
    params_copy = {}
 
215
    for key in keys:
 
216
        params_copy[key] = params[key]
 
217
 
 
218
    params_copy["algorithm"] = MD5_SESS
 
219
    return _A1 (params_copy, password)
 
220
 
 
221
def _A1(params, password):
 
222
    algorithm = params.get ("algorithm", MD5)
 
223
    H = DIGEST_AUTH_ENCODERS[algorithm]
 
224
 
 
225
    if algorithm == MD5:
 
226
        # If the "algorithm" directive's value is "MD5" or is
 
227
        # unspecified, then A1 is:
 
228
        # A1 = unq(username-value) ":" unq(realm-value) ":" passwd
 
229
        return "%s:%s:%s" % (params["username"], params["realm"], password)
 
230
 
 
231
    elif algorithm == MD5_SESS:
 
232
 
 
233
        # This is A1 if qop is set
 
234
        # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
 
235
        #         ":" unq(nonce-value) ":" unq(cnonce-value)
 
236
        h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password))
 
237
        return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
 
238
 
 
239
 
 
240
def _A2(params, method, kwargs):
 
241
    # If the "qop" directive's value is "auth" or is unspecified, then A2 is:
 
242
    # A2 = Method ":" digest-uri-value
 
243
 
 
244
    qop = params.get ("qop", "auth")
 
245
    if qop == "auth":
 
246
        return method + ":" + params["uri"]
 
247
    elif qop == "auth-int":
 
248
        # If the "qop" value is "auth-int", then A2 is:
 
249
        # A2 = Method ":" digest-uri-value ":" H(entity-body)
 
250
        entity_body = kwargs.get ("entity_body", "")
 
251
        H = kwargs["H"]
 
252
 
 
253
        return "%s:%s:%s" % (
 
254
            method,
 
255
            params["uri"],
 
256
            H(entity_body)
 
257
        )
 
258
 
 
259
    else:
 
260
        raise NotImplementedError ("The 'qop' method is unknown: %s" % qop)
 
261
 
 
262
def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs):
 
263
    """
 
264
    Generates a response respecting the algorithm defined in RFC 2617
 
265
    """
 
266
    params = auth_map
 
267
 
 
268
    algorithm = params.get ("algorithm", MD5)
 
269
 
 
270
    H = DIGEST_AUTH_ENCODERS[algorithm]
 
271
    KD = lambda secret, data: H(secret + ":" + data)
 
272
 
 
273
    qop = params.get ("qop", None)
 
274
 
 
275
    H_A2 = H(_A2(params, method, kwargs))
 
276
 
 
277
    if algorithm == MD5_SESS and A1 is not None:
 
278
        H_A1 = H(A1)
 
279
    else:
 
280
        H_A1 = H(_A1(params, password))
 
281
 
 
282
    if qop in ("auth", "auth-int"):
 
283
        # If the "qop" value is "auth" or "auth-int":
 
284
        # request-digest  = <"> < KD ( H(A1),     unq(nonce-value)
 
285
        #                              ":" nc-value
 
286
        #                              ":" unq(cnonce-value)
 
287
        #                              ":" unq(qop-value)
 
288
        #                              ":" H(A2)
 
289
        #                      ) <">
 
290
        request = "%s:%s:%s:%s:%s" % (
 
291
            params["nonce"],
 
292
            params["nc"],
 
293
            params["cnonce"],
 
294
            params["qop"],
 
295
            H_A2,
 
296
        )
 
297
    elif qop is None:
 
298
        # If the "qop" directive is not present (this construction is
 
299
        # for compatibility with RFC 2069):
 
300
        # request-digest  =
 
301
        #         <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
 
302
        request = "%s:%s" % (params["nonce"], H_A2)
 
303
 
 
304
    return KD(H_A1, request)
 
305
 
 
306
def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs):
 
307
    """This function is used to verify the response given by the client when
 
308
    he tries to authenticate.
 
309
    Optional arguments:
 
310
     entity_body - when 'qop' is set to 'auth-int' you MUST provide the
 
311
                   raw data you are going to send to the client (usually the
 
312
                   HTML page.
 
313
     request_uri - the uri from the request line compared with the 'uri'
 
314
                   directive of the authorization map. They must represent
 
315
                   the same resource (unused at this time).
 
316
    """
 
317
 
 
318
    if auth_map['realm'] != kwargs.get('realm', None):
 
319
        return False
 
320
 
 
321
    response =  _computeDigestResponse(auth_map, password, method, A1,**kwargs)
 
322
 
 
323
    return response == auth_map["response"]
 
324
 
 
325
def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs):
 
326
    # Note that the Basic response doesn't provide the realm value so we cannot
 
327
    # test it
 
328
    try:
 
329
        return encrypt(auth_map["password"], auth_map["username"]) == password
 
330
    except TypeError:
 
331
        return encrypt(auth_map["password"]) == password
 
332
 
 
333
AUTH_RESPONSES = {
 
334
    "basic": _checkBasicResponse,
 
335
    "digest": _checkDigestResponse,
 
336
}
 
337
 
 
338
def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs):
 
339
    """'checkResponse' compares the auth_map with the password and optionally
 
340
    other arguments that each implementation might need.
 
341
    
 
342
    If the response is of type 'Basic' then the function has the following
 
343
    signature:
 
344
    
 
345
    checkBasicResponse (auth_map, password) -> bool
 
346
    
 
347
    If the response is of type 'Digest' then the function has the following
 
348
    signature:
 
349
    
 
350
    checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool
 
351
    
 
352
    The 'A1' argument is only used in MD5_SESS algorithm based responses.
 
353
    Check md5SessionKey() for more info.
 
354
    """
 
355
    global AUTH_RESPONSES
 
356
    checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
 
357
    return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
 
358
 
 
359
 
 
360
 
 
361