1 """
2 This module defines functions to implement HTTP Digest Authentication
3 (:rfc:`2617`).
4 This has full compliance with 'Digest' and 'Basic' authentication methods. In
5 'Digest' it supports both MD5 and MD5-sess algorithms.
6
7 Usage:
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
16 associated with the 'username' key in 'auth_map' dict. Then you use the
17 'checkResponse' function to verify if the password matches the one sent
18 by the client.
19
20 SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
21 SUPPORTED_QOP - list of supported 'Digest' 'qop'.
22 """
23 __version__ = 1, 0, 1
24 __author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>"
25 __credits__ = """
26 Peter van Kampen for its recipe which implement most of Digest
27 authentication:
28 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
29 """
30
31 __license__ = """
32 Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
33 All rights reserved.
34
35 Redistribution and use in source and binary forms, with or without
36 modification, are permitted provided that the following conditions are met:
37
38 * Redistributions of source code must retain the above copyright notice,
39 this list of conditions and the following disclaimer.
40 * Redistributions in binary form must reproduce the above copyright notice,
41 this list of conditions and the following disclaimer in the documentation
42 and/or other materials provided with the distribution.
43 * Neither the name of Sylvain Hellegouarch nor the names of his
44 contributors may be used to endorse or promote products derived from
45 this software without specific prior written permission.
46
47 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
48 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
49 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
50 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
51 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
52 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
53 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
54 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
55 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
56 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
57 """
58
59 __all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
60 "parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
61 "calculateNonce", "SUPPORTED_QOP")
62
63
64 import time
65 from cherrypy._cpcompat import base64_decode, ntob, md5
66 from cherrypy._cpcompat import parse_http_list, parse_keqv_list
67
68 MD5 = "MD5"
69 MD5_SESS = "MD5-sess"
70 AUTH = "auth"
71 AUTH_INT = "auth-int"
72
73 SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
74 SUPPORTED_QOP = (AUTH, AUTH_INT)
75
76
77
78
79 DIGEST_AUTH_ENCODERS = {
80 MD5: lambda val: md5(ntob(val)).hexdigest(),
81 MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
82
83 }
84
85
87 """This is an auxaliary function that calculates 'nonce' value. It is used
88 to handle sessions."""
89
90 global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS
91 assert algorithm in SUPPORTED_ALGORITHM
92
93 try:
94 encoder = DIGEST_AUTH_ENCODERS[algorithm]
95 except KeyError:
96 raise NotImplementedError("The chosen algorithm (%s) does not have "
97 "an implementation yet" % algorithm)
98
99 return encoder("%d:%s" % (time.time(), realm))
100
101
114
115
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
124 """'doAuth' function returns the challenge string b giving priority over
125 Digest and fallback to Basic authentication when the browser doesn't
126 support the first one.
127
128 This should be set in the HTTP header under the key 'WWW-Authenticate'."""
129
130 return digestAuth(realm) + " " + basicAuth(realm)
131
132
133
134
135
137
138 items = parse_http_list(auth_params)
139 params = parse_keqv_list(items)
140
141
142
143
144 required = ["username", "realm", "nonce", "uri", "response"]
145 for k in required:
146 if k not in params:
147 return None
148
149
150 if "qop" in params and not ("cnonce" in params
151 and "nc" in params):
152 return None
153
154
155 if ("cnonce" in params or "nc" in params) and \
156 "qop" not in params:
157 return None
158
159 return params
160
161
163 username, password = base64_decode(auth_params).split(":", 1)
164 return {"username": username, "password": password}
165
166 AUTH_SCHEMES = {
167 "basic": _parseBasicAuthorization,
168 "digest": _parseDigestAuthorization,
169 }
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
224 -def _A1(params, password):
241
242
243 -def _A2(params, method, kwargs):
244
245
246
247 qop = params.get("qop", "auth")
248 if qop == "auth":
249 return method + ":" + params["uri"]
250 elif qop == "auth-int":
251
252
253 entity_body = kwargs.get("entity_body", "")
254 H = kwargs["H"]
255
256 return "%s:%s:%s" % (
257 method,
258 params["uri"],
259 H(entity_body)
260 )
261
262 else:
263 raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
264
265
268 """
269 Generates a response respecting the algorithm defined in RFC 2617
270 """
271 params = auth_map
272
273 algorithm = params.get("algorithm", MD5)
274
275 H = DIGEST_AUTH_ENCODERS[algorithm]
276 KD = lambda secret, data: H(secret + ":" + data)
277
278 qop = params.get("qop", None)
279
280 H_A2 = H(_A2(params, method, kwargs))
281
282 if algorithm == MD5_SESS and A1 is not None:
283 H_A1 = H(A1)
284 else:
285 H_A1 = H(_A1(params, password))
286
287 if qop in ("auth", "auth-int"):
288
289
290
291
292
293
294
295 request = "%s:%s:%s:%s:%s" % (
296 params["nonce"],
297 params["nc"],
298 params["cnonce"],
299 params["qop"],
300 H_A2,
301 )
302 elif qop is None:
303
304
305
306
307 request = "%s:%s" % (params["nonce"], H_A2)
308
309 return KD(H_A1, request)
310
311
313 """This function is used to verify the response given by the client when
314 he tries to authenticate.
315 Optional arguments:
316 entity_body - when 'qop' is set to 'auth-int' you MUST provide the
317 raw data you are going to send to the client (usually the
318 HTML page.
319 request_uri - the uri from the request line compared with the 'uri'
320 directive of the authorization map. They must represent
321 the same resource (unused at this time).
322 """
323
324 if auth_map['realm'] != kwargs.get('realm', None):
325 return False
326
327 response = _computeDigestResponse(
328 auth_map, password, method, A1, **kwargs)
329
330 return response == auth_map["response"]
331
332
335
336
337 try:
338 return encrypt(auth_map["password"], auth_map["username"]) == password
339 except TypeError:
340 return encrypt(auth_map["password"]) == password
341
342 AUTH_RESPONSES = {
343 "basic": _checkBasicResponse,
344 "digest": _checkDigestResponse,
345 }
346
347
348 -def checkResponse(auth_map, password, method="GET", encrypt=None, **kwargs):
349 """'checkResponse' compares the auth_map with the password and optionally
350 other arguments that each implementation might need.
351
352 If the response is of type 'Basic' then the function has the following
353 signature::
354
355 checkBasicResponse(auth_map, password) -> bool
356
357 If the response is of type 'Digest' then the function has the following
358 signature::
359
360 checkDigestResponse(auth_map, password, method='GET', A1=None) -> bool
361
362 The 'A1' argument is only used in MD5_SESS algorithm based responses.
363 Check md5SessionKey() for more info.
364 """
365 checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
366 return checker(auth_map, password, method=method, encrypt=encrypt,
367 **kwargs)
368