headerphoto

Change Your Identity in TurboGears with Entry Points

Posted by Tim Freund Thu, 14 Jun 2007 03:15:00 GMT

Paper
  PressIdentity defines who we are. Identity is made up of all the little distinguishing traits that differentiate one person from another. We've all changed our identity throughout our lives. We change from student to graduate, single to married, dogless to dogged, and more, but that's not what we're talking about today. Identity is the authentication and authorization framework for TurboGears, and it is easy to extend.

At the core of the Identity framework is an IdentityProvider. The Identity Provider interfaces with an authentication and authorization repository to determine two things: are you who you say you are, and do you belong where you are trying to go. The framework comes with two providers, one each for SQLObject and SqlAlchemy.

We will customize an IdentityProvider to authenticate against an IMAP server in the few steps that follow. This would be helpful for writing a web mail application, and the concept can be applied to other authentication mechanisms as well, including LDAP, Radius, and others.

Action Plan

  1. Quickstart a project
  2. Create an identity provider
  3. Define an entry point for the identity provider
  4. Configure the application for the new provider
  5. Finish the identity provider
  6. Test
  7. Relax

The code for this tutorial is available from subversion or as a tar.gz file. It is released under the MIT license. No TurboGears installation? Install it like so.

Step 1: Quickstart a Project

If you don't have an existing TurboGears project to experiment with, now would be a great time to start one.

tim@iris ~/src $ tg-admin quickstart -s -i iddemo
...
tim@iris ~/src/ $ cd iddemo
tim@iris ~/src/iddemo $ tg-admin sql create

Note the -s and -i flags. This is a project with support for SqlAlchemy and Identity.

Step 2: Create an Identity Provider

Any object can be an identity provider as long as it supplies the following methods: validate_identity, validate_password, load_identity, anonymous_identity, authenticated_identity, but it isn't always necessary to write one from scratch. Extending an existing provider often gets an application authenticating as required without much trouble. We will extend the SqlAlchemyIdentityProvider in this example to authenticate against an IMAP server.

iddemo/identity.py
from turbogears.identity.saprovider import SqlAlchemyIdentityProvider

class ImapSqlAlchemyIdentityProvider(SqlAlchemyIdentityProvider):
    pass

Step 3: Define an Entry Point

The Identity Framework uses an entry point named turbogears.identity.provider to decide what class to use when authenticating users. We are about to define a new option for this entry point, but further reading on the subject of entry points is recommended. Scroll to the bottom of this entry for a couple of relevant links. It's OK, we have the time.

This entry point step won't be necessary in the future, thanks to this patch, but entry points are a powerful tool and worth learning, regardless.

setup.py
setup(
    name="iddemo",
... (more setup stuff) ...
    entry_points="""
    [turbogears.identity.provider]
    imapsa = iddemo.identity:ImapSqlAlchemyIdentityProvider
    """,
... (more setup stuff) ...
)
  

To let the setuptools system know about this new identity provider, run the following:

tim@iris ~/src/iddemo $ python setup.py develop

Step 4: Configure the Application

With our imapsa option defined for the turbogears.identity.provider entry point, we can now configure the application to call the new provider. There is a value named identity.provider in app.cfg. We will replace the existing value with imapsa. While app.cfg open is, add the other three lines in the following example. They will be explained in the next step.

iddemo/config/app.cfg
...

identity.provider='imapsa'

identity.imapprovider.imap_authoritative=True
identity.imapprovider.server="localhost"
identity.imapprovider.port=143

...

  

The application is now ready to run with the new identity provider. Restart the application if it is currently running so that the configuration change will take effect.

Step 5: Finish the Identity Provider

Now let's dig in and implement the new authentication behavior.

iddemo/identity.py
...

class ImapSqlAlchemyIdentityProvider(SqlAlchemyIdentityProvider):
    def __init__(self):
        SqlAlchemyIdentityProvider.__init__(self)
        
        # These three lines get the configuration parameters we set in app.cfg
        self.imap_authoritative = get("identity.imapprovider.imap_authoritative", False)
        self.server = get("identity.imapprovider.server", "localhost")
        self.port = get("identity.imapprovider.port", 143)

        # These four lines make the user and visit classes available for
        # later use
        user_class_path = get("identity.saprovider.model.user", None)
        self.user_class = load_class(user_class_path)
        visit_class_path = get("identity.saprovider.model.visit", None)
        self.visit_class = load_class(visit_class_path)


    def validate_identity(self, user_name, password, visit_key):
        if self.validate_password(None, user_name, password):
            user = session.query(self.user_class).get_by(user_name=user_name)
            if not user:
                if self.imap_authoritative:
                    user = self.user_class()
                    user.user_name = user_name
                    user.save()
                    session.flush()
                else:
                    return None
            link = session.query(self.visit_class).get_by(visit_key=visit_key)
            if not link:
                link = self.visit_class(visit_key=visit_key, user_id=user.user_id)
                session.save(link)
            else:
                link.user_id = user.user_id
            session.flush()
            return SqlAlchemyIdentity(visit_key, user)
        return None

    def validate_password(self, user, user_name, password):
        rc = False
        try:
            imapcon = imaplib.IMAP4(self.server, self.port)
        except:
            log.error("Could not establish connection to server at %s:%d" % (self.server, self.port))
            return rc

        try:
            if imapcon.login(user_name, password)[0] == 'OK':
                rc = True
        except:
            # Probably threw an error for invalid username/password
            log.info("Passwords don't match for user: %s", user_name)
        imapcon.shutdown()
        return rc

Take a look at each method to figure out what they accomplish.

__init__ invokes the constructor of the SqlAlchemyIdentityProvider and then collects the configuration parameters required for authentication.

validate_identity first invokes validate_password. If the password validate succeeds, the user is selected from the database. If the user does not exist in the database, the provider will create the user when the imap_authoritative option is set. Should you not require this capability, you can remove this method entirely and rely upon the implementation in SqlAlchemyIdentityProvider. Finally, this method links the user with the current visit_key.

validate_password handles all IMAP access. It is the perfect method to override if all you need to do is change the identity authentication mechanism.

Step 6: Test

Open controllers.py and change the identity decorator to require an authenticated user when accessing the index page.

iddemo/controllers.py
class Root(controllers.RootController):
    @expose(template="iddemo.templates.welcome")
    @identity.require(identity.not_anonymous())
    def index(self):
        import time
        flash("Your application is now running")
        return dict(now=time.ctime())

The moment of truth has arrived. Point a browser at the application. You should see a login page, and a valid IMAP username/password should provide access to the application.

Step 7: Relax

We're done here. Go tell your friends and family how cool you are.

Any feedback is appreciated. Leave a comment here, post a response on your own blog, or send an email to tim -at- achievewith -dot- us. You can also usually find me lurking in #turbogears on irc.freenode.net as timphnode.

For more information, follow these links:

Posted in , , ,

Bookmark and Share

Comments

  1. Avatar Wesley Penner said 4 months later:
    This is very helpful. I used your instructions with those that I found for ldap authentication on the TurboGears website to make a SA ldap login. I'm using the version 1.0.4b1 and there were some problems that needed fixing, such as in "validate_identity" instead of: "user.save()" use: "session.save(user)"

Comments are disabled