CherryPy Digest and Basic Authentication Tutorial

Many applications require the use of password based authentication. In many web applications, this is done using cookies/sessions, and the login mechanism is an HTML form that is submitted to the application. If you are trying to build a RESTful web service, or any service where this client may be automated, It becomes useful to utilize HTTP authentication.

It may not be immediately obvious from the CherryPy documentation, but CherryPy includes all the tools needed to implement HTTP authentication, both Digest and Basic. You can see what the CherryPy website has documented about authentication at the CherryPy Builtin Tools Page. The best documentation for this is available in the book: CherryPy Essentials: Rapid Python Web Application Development

Basic vs Digest

HTTP supports two types of authentication, basic and digest. To a user accessing a protected page through a web browser, both types look the same. The browser will prompt the user for a username and password. The difference is how the client (browser) sends the password to the server.

With basic authentication, the client sends the password in clear-text, so any malicious attacker who can see the request can see the password. With digest authentication, the client does not send the password, but rather a digest based on the password and other factors. The server will also compute the digest using the known correct password. If the digest sent by the client matches the one calculated by the server, authentication succeeds.

The catch with digest authentication is the users’ passwords must be stored stored in clear-text on the server** in order to calculate the digest. On the flipside, basic authentication must send the password in clear-text through the network. If you are going to use basic authentication, it is recommended you use SSL to prevent the password from being sniffed.

** This is not entirely true, The digest algorithm allows you to store a partially digested password on the server side, however CherryPy has no easy way to specify this.

Digest Authentication

The following code creates a simple application with a public page and a secure page using digest authentication. The server configuration defines that the /secure url is to be protected with digest authentication (via the tools.digest_auth Tool available in CherryPy)

import cherrypy
 
class RootServer:
    @cherrypy.expose
    def index(self):
        return """This is a public page!"""
 
class SecureServer:
    @cherrypy.expose
    def index(self):
    return "This is a secure section"
 
if __name__ == '__main__':
    users = {"admin": "secretPassword",
             "editor": "otherPassword"}
 
    conf = {'/secure': {'tools.digest_auth.on': True,
                        'tools.digest_auth.realm': 'Some site',
                        'tools.digest_auth.users': users}}
    root = RootServer()
    root.secure = SecureServer()
    cherrypy.quickstart(root, '/', config=conf)

Run this code and visit http://localhost:8080/ . Now visit http://localhost:8080/secure. When your browser prompts you to do so, enter “admin” and “secretPassword” for the username and password respectively.

The /secure configuration section sets up the digest authentication with the following options:

  • ‘tools.digest_auth.on’: This boolean enables digest authentication for the given section (in this case /secure)
  • ‘tools.digest_auth.realm’: This string defines the realm to supply to the client.
  • ‘tools.digest_auth.users’: This dictionary defines users and passwords ({user: password}). This can also take two other forms, please see below.

There is a bug in the CherryPy Digest Authentication tool that prevents authentication when the request is a post request with a body. See Cherrypy Digest Auth POST Problem (and Solution!)

Basic Authentication

We are going to use the same code to implement basic authentication, but we will change the configuration variables.

import cherrypy
 
class RootServer:
    @cherrypy.expose
    def index(self):
        return """This is a public page!"""
 
class SecureServer:
    @cherrypy.expose
    def index(self):
    return "This is a secure section"
 
if __name__ == '__main__':
    users = {"admin": "secretPassword",
             "editor": "otherPassword"}
 
    def clear_text(passwd):
        return passwd
 
    conf = {'/secure': {'tools.basic_auth.on': True,
                        'tools.basic_auth.realm': 'Some site2',
                        'tools.basic_auth.users': users,
                        'tools.basic_auth.encrypt': clear_text}}
    root = RootServer()
    root.secure = SecureServer()
    cherrypy.quickstart(root, '/', config=conf)

Run this code as before, and supply the same username and password. Can’t tell the difference? Remember, the difference is in how it is transmitted. Here are the options we supplied to enable basic authentication:

  • ‘tools.basic_auth.on’: This boolean enables digest authentication for the given section (in this case /secure)
  • ‘tools.basic_auth.realm’: This string defines the realm to supply to the client.
  • ‘tools.basic_auth.users’: This dictionary defines users and passwords ({user: password}). This can also take two other forms, please see below.
  • ‘tools.basic_auth.encrypt’: This takes a function that encrypts the user-supplied password to match a pre-encrypted password in the user dict. (In our case, the function clear_text we defined just passes the password through, allowing us to store password in clear-text for ease of demonstration). If you do not supply this option, it will be assumed the passwords are stored in MD5.

Defining Users

You can supply either a dict or function to the ‘tools.******_auth.users’ option to define your users. There are three ways to supply users: as a dict ( {user:password} ), as a function that returns a dict ( {user:password} ), or as a function that takes a username as an argument, and returns a password.

Remember that password could mean the clear-text password, or an encrypted password, depending on whether your using basic or digest, and the encryption option specified.

Using a dictionary

Simply supply a dictionary of {username: password} to the users option of the authentication configuration.

users = {'user1': 'password1',
         'user2': 'password2}
 
conf = {'/secure': {'tools.digest_auth.on': True,
                     'tools.digest_auth.realm': 'Some site',
                     'tools.digest_auth.users': users}}

Using a function that returns a dictionary

Supply a function that takes no arguments and returns a dictionary containing all usernames and passwords

def get_users():
    return {'user1': 'password1',
            'user2': 'password2}
 
conf = {'/secure': {'tools.digest_auth.on': True,
                     'tools.digest_auth.realm': 'Some site',
                     'tools.digest_auth.users': get_users}}

Using a Function Which Takes a Username (Most Useful)

Many times, applications have so many users that it does not make sense to pass the list of all users around like the options above. I have not seen this feature documented anywhere, but the code allows it. You can specify a function that takes a username string as an argument, and returns the password for that user. This function could consult a database or other mechanism, and allows your application to add and remove user more easily.

def find_password(username):
    #INSERT PASSWORD LOOKUP LOGIC HERE
    password = db.find_password_for_user(username)
    return password
 
conf = {'/secure': {'tools.digest_auth.on': True,
                     'tools.digest_auth.realm': 'Some site',
                     'tools.digest_auth.users': find_password}}

Retrieving the Username

Once authenticated your application will likely want to know which user is logged in. The username of the user who has successfully authenticated will be available at ‘cherrypy.request.login’. This variable wil be False if authentication failed.

In Closing

It may not be well documented, but the tools for basic and digest authentication are already built in to CherryPy. You just have to know how to use them, and now you do.

If there are any problems with the code or this post in general, leave me a comment and I will make it right.

Tags: , , , , , ,

7 Responses to “CherryPy Digest and Basic Authentication Tutorial”

  1. Green Data Says:

    Cool man, this is really useful as I was going to implement some API (JSON) for my web application and was willing to authenticate such requests.
    Anyway, I’ll add your blog into my Feed Reader as it seems that you are a Python/Cherrypy fan like me.

  2. Robert Brewer Says:

    Excellent! Thanks so much for this tutorial! We need more helpful people like you and articles like this one. :)

  3. rohmad Says:

    thanks alot!…its very useful

  4. morecowbell Says:

    awesome. most helpful. thank you very much!

  5. morecowbell Says:

    excellent. thanks.
    got a question: using the basic_auth module from above but changing RootServer class to (in order to force the sign in pop up with out the user having to follow a link, for example):
    class RootServer:
    s = SecureServer()
    @cherrypy.expose
    def index(self):
    return self.s.index()

    seems to circumvent the sign-in process. i was under the impression that the configuration for ‘/secure’ would still require
    login and make sure that when the methods in that class are called, authentication is required. i seem to be missing something major here. thx

  6. Jim Hoskins Says:

    @morecowbell:

    I am not exactly sure what you are going for in your example, but maybe this helps.

    If you want everything on your server to be protected, you can simply replace “/secure” with “/” in the configuration.

    CherryPy triggers authentication before the target method is called, based on the request path. If you manually call the method, CherryPy won’t be able to tell, and authentication will not be triggered.

    My suggestion would be to either:
    1) Send a HTTP Redirect from root.index, this will cause the client to request /secure, and authentication will be triggered. Or,
    2) Add or change the configuration to include authentication for “/” and authentiaction will trigger on any request.

    The use case you suggested seems possible, if the authentication code could be implemented as a decorator, but that functionality does not exist as far as I know. I may hack around with it to see if it can be done.

    I hope this helps, let me know if I didn’t fully answer your question.

  7. morecowbell Says:

    thanks for clearing that up. the example was somewhat contrived and predicated by laziness doing a “functional” redirect. obviously, that doesn’t work and you probably saved me a wild goose chase or two down the road :) thanks again.

Leave a Reply