I am in the process of getting Repoze.who working with Pylons, which I believe will provide me with a strong authentication framework for my application, and allow me to do users, groups, and roles. Luckily enough, I had the generous help from Author, Chris McDonough, who also helped write this tutorial with me.

This first part in my new series on working with Repoze.who will allow you to set up a very basic HTAccess based Authentication layer for your Pylons app. We’ll start off by installing Repoze.who, get moving by modifying the Middleware, and conclude by putting some logic in a controller to limit access to only authorized users. This is by no means a perfect solution to getting your Pylons app secure, but should be a good primer on how Repoze.who works, and will serve as a stepping stone to my next article on using SQLAlchemy with Repoze.who and Pylons to do groups and permissions.

Okay, now lets get on our way!

Install Repoze.who
Download and run the latest code from http://dist.repoze.org/who/latest/

wget http://dist.repoze.org/who/latest/repoze.who-1.0.1.tar.gz
tar xfz repoze.who-1.0.1.tar.gz
cd repoze.who-1.0.1
sudo python setup.py build
sudo python setup.py install

Configuring the Middleware

To get really started, you’ll need to modify middleware.py. To get an understanding of what middleware is, check out http://dev.pocoo.org/~mitsuhiko/werkzeug_en.pdf & http://dirtsimple.org/2007/02/wsgi-middleware-considered-harmful.html (thanks rcs_comp for the links).

To begin with, middleware.py probably looks a bit like this:

"""Pylons middleware initialization"""
from paste.cascade import Cascade
from paste.registry import RegistryManager
from paste.urlparser import StaticURLParser
from paste.deploy.converters import asbool

from pylons import config
from pylons.error import error_template
from pylons.middleware import error_mapper, ErrorDocuments, ErrorHandler, \
    StaticJavascripts
from pylons.wsgiapp import PylonsApp

from myapp.config.environment import load_environment

def make_app(global_conf, full_stack=True, **app_conf):
    """Create a Pylons WSGI application and return it

    ``global_conf``
        The inherited configuration for this application. Normally from
        the [DEFAULT] section of the Paste ini file.

    ``full_stack``
        Whether or not this application provides a full WSGI stack (by
        default, meaning it handles its own exceptions and errors).
        Disable full_stack when this application is "managed" by
        another WSGI middleware.

    ``app_conf``
        The application's local configuration. Normally specified in the
        [app:] section of the Paste ini file (where
        defaults to main).
    """
    # Configure the Pylons environment
    load_environment(global_conf, app_conf)

    # The Pylons WSGI app
    app = PylonsApp()

    # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)

    if asbool(full_stack):
        # Handle Python exceptions
        app = ErrorHandler(app, global_conf, error_template=error_template,
                           **config['pylons.errorware'])

        # Display error documents for 401, 403, 404 status codes (and
        # 500 when debug is disabled)
        app = ErrorDocuments(app, global_conf, mapper=error_mapper, **app_conf)

    # Establish the Registry for this application
    app = RegistryManager(app)

    # Static files
    javascripts_app = StaticJavascripts()
    static_app = StaticURLParser(config['pylons.paths']['static_files'])
    app = Cascade([static_app, javascripts_app, app])
    return app

But you want it to look more like this:

"""Pylons middleware initialization"""
from paste.cascade import Cascade
from paste.registry import RegistryManager
from paste.urlparser import StaticURLParser

from paste.deploy.converters import asbool

from pylons import config
from pylons.error import error_template
from pylons.middleware import error_mapper, ErrorDocuments, ErrorHandler, \
    StaticJavascripts
from pylons.wsgiapp import PylonsApp

from myapp.config.environment import load_environment

"""Repoze.who stuff"""
from repoze.who.interfaces import IIdentifier
from repoze.who.interfaces import IChallenger
from repoze.who.plugins.auth_tkt import AuthTktCookiePlugin
from repoze.who.plugins.cookie import InsecureCookiePlugin
from repoze.who.plugins.form import FormPlugin
from repoze.who.plugins.htpasswd import HTPasswdPlugin
from repoze.who.middleware import PluggableAuthenticationMiddleware
import StringIO
from repoze.who.classifiers import default_request_classifier
from repoze.who.classifiers import default_challenge_decider
# for repoze.who debug
import sys
import logging

def cleartext_check(password, hashed):
    return password == hashed

def make_app(global_conf, full_stack=True, **app_conf):
	"""Create a Pylons WSGI application and return it
	``global_conf``
        The inherited configuration for this application. Normally from
        the [DEFAULT] section of the Paste ini file.

    ``full_stack``
        Whether or not this application provides a full WSGI stack (by
        default, meaning it handles its own exceptions and errors).
        Disable full_stack when this application is "managed" by
        another WSGI middleware.

    ``app_conf``
        The application's local configuration. Normally specified in the
        [app:] section of the Paste ini file (where
        defaults to main).
	"""
	# Configure the Pylons environment
	load_environment(global_conf, app_conf)

	# The Pylons WSGI app
	app = PylonsApp()

	# CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)

 	if asbool(full_stack):
		# Handle Python exceptions
 		app = ErrorHandler(app, global_conf, error_template=error_template,
			**config['pylons.errorware'])

        # Display error documents for 401, 403, 404 status codes (and
        # 500 when debug is disabled)
        app = ErrorDocuments(app, global_conf, mapper=error_mapper, **app_conf)

 	# Establish the Registry for this application
	app = RegistryManager(app)

    # Static files
	javascripts_app = StaticJavascripts()
	static_app = StaticURLParser(config['pylons.paths']['static_files'])
	app = Cascade([static_app, javascripts_app, app])

	# copy and pasted from repoze.who readme
	io = StringIO.StringIO()
	salt = 'aa'
	for name, password in [ ('admin', 'nimd'), ('nym', 'myn') ]:
	    io.write('%s:%s\n' % (name, password))
	io.seek(0)

	htpasswd = HTPasswdPlugin(io, cleartext_check)
	auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt')
	form = FormPlugin('__do_login', rememberer_name='auth_tkt')

	identifiers = [('form', form),('auth_tkt',auth_tkt)]
	authenticators = [('htpasswd', htpasswd)]
	challengers = [('form',form)]
	mdproviders = []

	log_stream = None
	import os
	if os.environ.get('WHO_LOG'):
	    log_stream = sys.stdout

	middleware = PluggableAuthenticationMiddleware(
		app,
		identifiers,
		authenticators,
	    challengers,
	    mdproviders,
	    default_request_classifier,
	    default_challenge_decider,
	    log_stream = log_stream,
	    log_level = logging.DEBUG
	)

	return middleware
    #return app

So, now you have middleware working! The only thing is, nothing's different, at least nothing seems different. No pages are locked down, nobody can log in yet, but you have set up the basis for the most simple form of authentication with Repoze.who - that is HTPasswdPlugin.

Lets take a deeper look at what's going on:

	io = StringIO.StringIO()
	salt = 'aa'
	for name, password in [ ('admin', 'nimda'), ('nym', 'myn') ]:
	    io.write('%s:%s\n' % (name, password))
	io.seek(0)

	htpasswd = HTPasswdPlugin(io, cleartext_check)

This sets up the HTPasswdPlugin with what looks appears to python as a regular htpasswd file. If you wanted to replace the StringIO with a open("htaccess","r"), you could. In later tutorials I'll show how to replace this completely with a user model to avoid having to use this incredibly underpowered setup. The reason I start off with an HTPasswdPlugin is because it's the simplest thing to understand, and easiest way to get actual authentication working with Repoze.who.

After htpasswd is defined as an HTPasswdPlugin, we do a few things:

Define the AuthTktCookiePlugin and the FormPlugin:

	auth_tkt = AuthTktCookiePlugin('secret', 'auth_tkt')
	form = FormPlugin('__do_login', rememberer_name='auth_tkt')

AuthTktCookiePlugin is an IIdentifier plugin that allows for identities to be stored as client-side cookies. This is great because as it turns out, your users probably won't want to be asked to input their credentials every time they visit a page on your website. The first parameter is the secret, which is used to encrypt the cookie on the client side, and decrypt it on the server side. The second parameter is the cookie name. I personally suggest you use the cookie name "mystical_monkey_powers". Additionally, if you don't add a third parameter, the cookie can be sent over HTTP connections, as well as HTTPS connections. If the third param is True, then the REMOTE_ADDR of the WSGI environment will be put inside the cookie. I just started off with the default for now.

FormPlugin is both an IIdentifier and IChallenger plugin, which intercepts POST requests. It's also what will display a login form later when a challenge is required. The Repoze.who README says that the first parameter is "a query string name used to denote that a form POST is destined for the form plugin". For this example, we use '__do_login', which is unique. The name '__do_login' doesn't mean anything, it's just unique enough not to cause any conflicts. The second argument, remember_name='auth_tkt' is the "configuration name" of our other IIdentifier plugin that deals with the "remember and forget" responsibilities. In our case, that plugin is "auth_tkt", which is our AuthTktCookiePlugin. See how it all fits together?

After that, there are three statements that define identifiers, authenticators, and challenges.

	identifiers = [('form', form),('auth_tkt',auth_tkt)]
	authenticators = [('htpasswd', htpasswd)]
	challengers = [('form',form)]

These are passed later to the middleware constructor, and are respectively, the implementations we are going to use for each of the IIdentifier, IAuthenticator, and IChallenger types in the middleware setup.

After that, the rest just configures the middleware to do things like logging and the classifier and decider implementations. The Repoze.who README says that these are stock, so I just left them alone.

Good job so far! We're almost done with our first authentication setup...

Creating the Password Protected Controller

Next, you want to create a controller to test out your authentication setup. Run the following from your top project directory:

paster controller seekret

Of course, this creates a controller called "seekret". Very black-ops sounding, I know.

Inside of that controller, right before it returns anything, put the following conditional:

		if request.environ.get('repoze.who.identity') == None:
			abort(401)

Give it a go! See the form that wasn't there before? If you use one of the logins you previously defined in middleware.py, you'll see "Hello World", if not, you'll get the form again. Pretty cool, huh?

So how does it work?

Now when you go to your controller, if the user hasn't been previously identified, it will abort with a 401 (Unauthorized) response.

After that, the seekret controller will evaluate request.environ.get('repoze.who.identity'). Since this will be the first time we hit that page, it will spit back None. The if statement will trigger, creating an Unauthorized response. That will translate within the framework to a "request classification: browser" to repoze.who. The identifier plugins, FormPlugin and AuthTktCookiePlugin, will then go to work, and find nothing (no form response, no cookie response). Everybody says "We don't know this guy", don't let him into the secret club. Nobody knows who you are and repoze.who will say "no identities found, not authenticating". Luckily it doesn't end there.

Once everyone is certain they don't know you, they'll give you a challenge. We registered the FormPlugin as the challenger, and repoze.who will present the FormPlugin as the challenger, presented to you the user as a Username and Password form.

If you answer correctly, you'll be let in. If not, you'll continue to get the FormPlugin. Actually what happens when you answer correctly is that the FormPlugin will return an identity, the authenticator, HTPasswdPlugin will make a match to what it has on file, and instead it's more correct to say that repoze.who will redirect to the page and this time allow access, which means you'll get to see those two lovely words- "Hello World".

Next time you hit the page, no challenge will be presented to you, the user, because it happens behind the scenes. The FormPlugin set the auth_tkt (sorry, mysticalmonkeypowers) cookie, and the app will let you in since you are already got in once. It's like having a stamp on your hand, and being let back into the club. Wash that stamp off though, and the bouncer will ask who you are again. The same goes for your app, if you delete the cookie that was created, you'll be back to square one.

Want to see for yourself? Shut down your app, and type the following into your terminal:

export WHO_LOG=1

Then restart your app in the foreground. Now repoze.who will print out all it's logging statements to your terminal, and you can see for yourself what it's doing behind the scenes.

Stay tuned for Part 2, hooking up a SQLAlchemy based Model to Repoze.who!

Example Source: http://standundering.com/examples/authexample1.tgz


6 Responses to

“Authorization in Pylons with Repoze.who (Part 1 – HTAccess)”

  1. Noah Gift Says:

    Great post.

  2. David Pratt Says:

    I very much like the WSGI approach to authentication. As we know, authentication is only one half of the puzzle while authorization the other. I believe that there is generally a desire for more global and consistent solution for authorization in python. I am looking forward to your next post.

  3. Drew Whitehouse Says:

    Looking forward to the next installment (hint, hint :-)

  4. Alexander Fairley Says:

    Note to wandering coders. This is a year old, and appears, at least to my eyes, not to work with pylons 0.9.7

  5. Desdulianto Says:

    Great article.

    Now that we can authenticate a user to access our protected site, how do we implement the log out function for our user?

    Btw, is it secure to store identity on client side?

  6. fgfdgfdgfgfdg Says:

    thanks, just what i needed to read, well written and very helpful, two thumbs up :D