Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/trunk/cherrypy/lib/httpauth.py

Revision 2641 (checked in by chbrown, 6 months ago)

Sphinx docs in cherrypy.lib.*

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

Hosted by WebFaction

Log in as guest/cpguest to create tickets