Commits

Sylvain Hellegouarch committed 2f8d2f3

Digest and basic auth can now take a callable which must return a dict with user credentials so that it can fetch those from a database for instance.

Basic takes also an encrypt parameter which must be a callable that will encrypt the password sent back the user-agent. So that passwords can be stored encrypted on the server.

  • Participants
  • Parent commits da38d70

Comments (0)

Files changed (3)

File cherrypy/lib/auth.py

 from httpauth import parseAuthorization, checkResponse, basicAuth, digestAuth
 
 
-def check_auth(users):
+def check_auth(users, encrypt=None):
     """If an authorization header contains credentials, return True, else False."""
     if 'authorization' in cherrypy.request.headers:
         # make sure the provided credentials are correctly set
         ah = parseAuthorization(cherrypy.request.headers['authorization'])
         if ah is None:
             raise cherrypy.HTTPError(400, 'Bad Request')
-        
+
+        if not encrypt:
+            encrypt = lambda x: x
+
+        if callable(users):
+            users = users() # expect it to return a dictionary
+
+        if not isinstance(users, dict):
+            raise ValueError, "Authentication users must be passed contained in a dictionary"
+    
         # fetch the user password
         password = users.get(ah["username"], None)
         
         # validate the authorization by re-computing it here
         # and compare it with what the user-agent provided
-        if checkResponse(ah, password, method=cherrypy.request.method):
+        if checkResponse(ah, password, method=cherrypy.request.method, encrypt=encrypt):
             return True
     
     return False
 
-def basic_auth(realm, users):
+def basic_auth(realm, users, encrypt=None):
     """If auth fails, raise 401 with a basic authentication header.
     
     realm: a string containing the authentication realm.
-    users: a dict of the form: {username: password}.
+    users: a dict of the form: {username: password} or a callable returning a dict.
+    encrypt: callable used to encrypt the password returned from the user-agent.
     """
-    if check_auth(users):
+    if check_auth(users, encrypt):
         return
     
     # inform the user-agent this path is protected
     """If auth fails, raise 401 with a digest authentication header.
     
     realm: a string containing the authentication realm.
-    users: a dict of the form: {username: password}.
+    users: a dict of the form: {username: password} or a callable returning a dict.
     """
     if check_auth(users):
         return

File cherrypy/lib/httpauth.py

 
     return KD(H_A1, request)
 
-def _checkDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs):
+def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs):
     """This function is used to verify the response given by the client when
     he tries to authenticate.
     Optional arguments:
 
     return response == auth_map["response"]
 
-def _checkBasicResponse (auth_map, password, method='GET', **kwargs):
-    return auth_map["password"] == password
+def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs):
+    return encrypt(auth_map["password"]) == password
 
 AUTH_RESPONSES = {
     "basic": _checkBasicResponse,
     "digest": _checkDigestResponse,
 }
 
-def checkResponse (auth_map, password, method = "GET", **kwargs):
+def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs):
     """'checkResponse' compares the auth_map with the password and optionally
     other arguments that each implementation might need.
     
     """
     global AUTH_RESPONSES
     checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
-    return checker (auth_map, password, method, **kwargs)
+    return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
  
 
 

File cherrypy/test/test_httpauth.py

 from cherrypy.test import test
 test.prefer_parent_path()
 
+import md5
+
 import cherrypy
 from cherrypy.lib import httpauth
 
             return "This is protected by Basic auth."
         index.exposed = True
 
+    def md5_encrypt(data):
+        return md5.new(data).hexdigest()
+
+    def fetch_users():
+        return {'test': 'test'}
+
     conf = {'/digest': {'tools.digestauth.on': True,
                         'tools.digestauth.realm': 'localhost',
-                        'tools.digestauth.users': {'test': 'test'}},
+                        'tools.digestauth.users': fetch_users},
             '/basic': {'tools.basicauth.on': True,
                        'tools.basicauth.realm': 'localhost',
-                       'tools.basicauth.users': {'test': 'test'}}}
+                       'tools.basicauth.users': {'test': md5_encrypt('test')},
+                       'tools.basicauth.encrypt': md5_encrypt}}
     root = Root()
     root.digest = DigestProtected()
     root.basic = BasicProtected()
             self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
 
             # now let's see if what 
-            base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+        base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
 
-            auth = base_auth % (nonce, '', '00000001')
+        auth = base_auth % (nonce, '', '00000001')
                 
-            params = httpauth.parseAuthorization(auth)
-            response = httpauth._computeDigestResponse(params, 'test')
+        params = httpauth.parseAuthorization(auth)
+        response = httpauth._computeDigestResponse(params, 'test')
+        
+        auth = base_auth % (nonce, response, '00000001')
+        self.getPage('/digest/', [('Authorization', auth)])
+        self.assertStatus('200 OK')
+        self.assertBody('This is protected by Digest auth.')
             
-            auth = base_auth % (nonce, response, '00000001')
-            self.getPage('/digest/', [('Authorization', auth)])
-            self.assertStatus('200 OK')
-            self.assertBody('This is protected by Digest auth.')
-
 if __name__ == "__main__":
     setup_server()
     helper.testmain()