repoze.who is great for using authentication within your site, but the documentation is woefully incomplete as to how to actually implement the plugin. And although the Pylons book and wiki contain sparse examples of how to set up your project, they’re still lacking in several key features. Since I’ve spent quite some time working on company-related projects that use repoze.who, I want to share my knowledge.
There are four “roles” that plug-ins can fulfill, which will be configured in our who.ini:
- IIdentifier: After a user submits their credentials, this plug-in is responsible for stripping the information and retrieving the info–such as a username/password from a log in page, or retrieving a single sign-on (SSO) key. This is then used by the…
- IAuthenticator: After a user’s information has been retrieved, the authenticator will determine whether or not the presented information is correct. For example, if it was a SSO key, this plug-in would be used to verify that it is still valid. If it were a username and password, this would verify that the information is correct.
- IChallenger: When a user is either not logged in–or has failed to login–the challenger will present the user with a page allowing them ao authenticate.
- IMetadataProvider: When logged in, this plug-in retrieves metadata based on the IIdentifier. (optional)
For this first part, I’ll focus primarily on getting two of the built-in plug-ins configured to fulfill these. First off, lets create a blank project to get things started.
Creating a Pylons plugin
If you’ve never touched Pylons, make sure you follow the guide on the official Pylons Getting Started page.
Lets create a basic server and controller using paster:
$ paster create -t pylons authapp template_engine=mako sqlalchemy=False Selected and implied templates: Pylons#pylons Pylons application template [...] $ cd authapp $ paster controller secret Creating [...]
At this point, we want to add repoze.who to our project. Open authapp/config/middleware.py and add the following. Take special note that the middleware setup is put AFTER the if asbool(full_stack): line. This is because the StatusCodeRedirect middleware will take the 401 error that we throw, and display an error page.
# Add to imports from repoze.who.config import make_middleware_with_config as make_who_with_config [...] def make_app(global_conf, full_stack=True, static_files=True, **app_conf): [...] # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) # BEGIN REPOZE.WHO CODE app = make_who_with_config(app, global_conf, app_conf['repoze.who.ini'], app_conf['repoze.who.log.file'], app_conf['repoze.who.log.level']) # END REPOZE.WHO CODE
Then, add the following lines in development.ini under the [app:main] heading:
[app:main] repoze.who.ini = %(here)s/who.ini repoze.who.log.file = stdout repoze.who.log.level = debug
Now, we’ll add a sample who.ini, which will fill in as this article progresses. This should be in the same directory as your development.ini config.
[general] request_classifier = repoze.who.classifiers:default_request_classifier challenge_decider = repoze.who.classifiers:default_challenge_decider [identifiers] plugins = [authenticators] plugins = [challengers] plugins =
At this point, we should have a functional Pylons stack setup. But just to be sure, lets test it by running paster.
$ paster serve development.ini Starting server in PID 10186. serving on http://127.0.0.1:5000
At this point, you should be able to open a browser and navigate to http://127.0.0.1:5000 and/or http://127.0.0.1:5000/secret/index, and see either a “Pylons Page” or a “Hello World” page, respectively. If you don’t see this, retrace your steps and try to debug the issue.
BasicAuth
The first plug-in we’ll look at is BasicAuth, which fulfills the IIdentifier and IChallenger roles. Basic authentication is commonly used to protect pages, and although it may not be the most user-friendly, its a great way to get started. Because its included with repoze.who, it requires zero additional setup, except for the following additions to your who.ini:
[identifiers] plugins = basicauth [challengers] plugins = basicauth [plugin:basicauth] use = repoze.who.plugins.basicauth:make_plugin realm = some_unique_security_realm
realm should be some quasi-descriptive name of your site or project.
Of course, we could never really login, since we have nothing to check the usernames/passwords against.
htpasswd
htpasswd is responsible for taking the information that the IIdentifier plugin (BasicAuth) has parsed out, and determine whether or not the user is actually allowed to access the page. Because htpasswd needs to do some authentication, lets set up some users first. An easy way to create hashes is using the variety of sites that provide interactive ways to do so, such as this one. Lets generate a few sample ones, and put them into a file called passwd in the same directory as our who.ini:
andrew:2kRr2LXOu9cp6 robert:2ML8KpK./SeQI
Now that we have users andrew and robert setup (their passwords are both the text secret), we can setup the htpasswd plug-in in who.ini:
[authenticators] plugins = htpasswd [plugin:htpasswd] use = repoze.who.plugins.htpasswd:make_plugin filename = %(here)s/passwd
At this point, we now have everything we need to authenticate. But what happens when you start your server? Its like we haven’t done anything, right? That’s because we need some way to tell the repoze.who middleware when we want someone to have to authenticate to access that area of the site.
Protecting Actions
For this, we’re going to create a new plug-in. In all reality, this plug-in can exist anywhere you want. For this tutorial, it will exist in authapp.lib.authenticated. Open up authapp/lib/authenticated.py and insert the following:
from decorator import decorator from pylons import request from pylons.controllers.util import abort def user_is_authenticated(): """ Returns True if a user is authenticated, False otherwise """ identity = request.environ.get('repoze.who.identity') return identity is not None @decorator def authenticated(func, *args, **kwargs): """ Decorator that will use Pylons' abort method to trigger the repoze.who middleware that this request needs to be authenticated. """ if not user_is_authenticated(): abort(401) return func(*args, **kwargs)
In a nut shell, this creates a decorator that will inform repoze.who if it needs to be authenticated.
At this point, you should now be able to start paster and access the secret page at http://127.0.0.1:5000/secret/index, and wallah! You should now be prompted with a BasicAuth challenge!
Conclusion
In my next post, I hope to provide a little more flexible login situation by walking through writing a custom plug-in, and all that it entails.
Great article! I definitely agree that the current repoze.who and repoze.what documentation is way too sparse for Pylons. I appreciate the write-up!
However, it is my understanding that the middleware setup for repoze.who needs to be done BEFORE the
if asbool(full_stack):. This is because repoze.who will throw an exception along with an error code, for instance, when a user fails to authenticate. The error is then passed to the StatusCodeRedirect function which then gets passed on to your applications custom error handling controller in controllers/error.py. This is what allows the developer to customize different error actions for the application.This is actually not entirely True, because the middleware itself uses a
401error code to determine whether or not it needs to issue a request. For example, you’ll notice that theauthenticateddecorator aborts with a 401–this NEEDS to be caught by the repoze.who middleware, otherwise the error page will always be displayed if you haven’t logged in.However, one alternative is to force the error handling middleware to ignore the 401 error–by removing it from the list of status codes the error handler will catch. If the repoze.who middleware tries to authenticate but cannot, then the 403 (or other) should trickle up to the error handling middleware.
I’ll have to do some testing myself to verify that this is 100% correct, but that’s what my understanding is.
Hello Andrew,
Thanks for the post. I have pylons / repoze working with htpasswd but need to have it working with SSO. Would you be able to provide any insight on how to achieve this? I need to integrate repoze with Apache’s webauth module.
Any help would be much appreciated.
Regards,
Anusha
The answer as to whether or not you can is probably `yes`. But as for how, I’m not really sure–mostly because I have no idea how the Apache webauth module works.
However, I am currently drafting my next article, which is how to extend the functionality by writing your own plug-in. Since you seem to be familiar with the webauth module, I feel fairly certain you could write your own module to add this functionality.
Does that sound good? I might be able to provide more immediate support, but I think waiting on the next article is definitely your best bet. :)
Andrew