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
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
83
84 DIGEST_AUTH_ENCODERS = {
85 MD5: lambda val: md5(val).hexdigest(),
86 MD5_SESS: lambda val: md5(val).hexdigest(),
87
88 }
89
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
117
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
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
136
161
162
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
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
195
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):
240
241
242 -def _A2(params, method, kwargs):
243
244
245
246 qop = params.get ("qop", "auth")
247 if qop == "auth":
248 return method + ":" + params["uri"]
249 elif qop == "auth-int":
250
251
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
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
286
287
288
289
290
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
301
302
303
304 request = "%s:%s" % (params["nonce"], H_A2)
305
306 return KD(H_A1, request)
307
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
328
329
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