Advanced Authentication in TurboGears 2 – Part 1

TurboGears is one of the best python web frameworks you can find this days. I could start listing its features but this post is already long enough and you can read about them in the official TurboGears website. Also, if you are interested in what the title of this post says it is about, you may already know one or two things about TurboGears. So let’s get to the point: Authentication.

Authentication is the act of verifying that somebody is really who he/she claims to be, is about finding who you are. Authorization, on the other hand, is the act of granting access to given resources depending on who would use them. For example, allowing registered members to leave comments on a blog, or allowing your friends to see your pictures while others cannot. In other words, finding what you may do (Authentication and Authorization in TurboGears 2).

TurboGears 2 uses two frameworks to deal with authentication and authorization. Together, these frameworks, are part of a robust, extendable and pluggable system that works in almost any situation but can be extended to suit your needs if it doesn’t. The two frameworks are repoze.who and repoze.what:

  • repoze.who, a framework for authentication in WSGI applications. You normally don’t have to care about it because by default TG2 applications ship all the code to set it up (as long as you had selected such an option when you created the project), but if you need something more advanced you are at the right place.
  • repoze.what, the successor of tg.ext.repoze.who and tgext.authorization (used in unstable TG2 releases), is a framework for authorization that is mostly compatible with the TurboGears 1.x Identity authentication, identification and authorization system.

Normal authentication, using username and password, can be easily enabled in existing TurboGears applications and is even easier to get if you’re creating a new project. However, if you need support for other authentication methods like Facebook Connect, Sign in with Twitter or any other OAuth based authentication method, you’ll be expending a few hours of your time playing with the authentication and authorization system.

This post is about how to create a TurboGears 2 project with support for standard username and password login, Facebook Connect and Sign in with Twitter, all at the same time. There will be a lot of code to show and thus the post will be long so I have split it in three parts:

  • Part 1: Using a .INI file to configure authentication and authorization middleware in TurboGears. In Part 1 we’re going to start with a new TurboGears 2 project without authentication and authorization support and then we’re going to configure repoze.who and repoze.what by hand. Adding a middleware and using a .INI file to define its behavior, among other things. At the end of the post your application will support normal username/password authentication.
  • Part 2: Adding support for Facebook Connect. Part 2 is about adding a second authentication method, Facebook Connect. We’ll create a repoze.who and repoze.what plugins, use Facebook Python SDK to gather user information from facebook servers and use Facebook JavaScript SDK in the client side to handle cookie creation for the authentication method to work.
  • Part 3: Adding support for Sign in with Twitter. Part 3 is a lot like Part 2, but instead of Facebook Connect we’re going to add Sign in with Twitter as our third authentication method. Again, we’ll create a repoze.who plugin and we’re going to use Tweepy library to get information from user’s twitter account.

Before we start, there are a few things you may need to know:

  • I assume you already have TurboGears 2.1 installed.
  • You need access to a Facebook Application. In Part 2 you’ll need the Application ID, API Key and Application Secret.
  • You need access to a Twitter Application. In Part 3 you’ll need the Consumer Key and Consumer Secret. Also you probably will need to change the Callback URL.
  • You need Apache installed on your machine.

Update (09/28/2010)

The following 8 steps lead to a project with several errors regarding broken links or missing controller actions. To fix those problems the code in Step 8. was revised and updated, Step 9. was introduced and the following must be done:

  1. Open project/templates/master.html and replace logout_handler with logout.
  2. Open project/templates/login.html and replace login_handler with authenticate.

Part 1. Using a .INI file to configure authentication and authorization middleware in TurboGears

Step 1. Create a TurboGears project

TurboGears provides a suite of tools for working with projects by adding several commands to the Python command line tool paster. In this tutorial we are going to use quickstart, setup and serve. The first tool you’ll need is quickstart, which initializes a TurboGears project. Go to a command line and run the following command:

[codesyntax lang=”bash”]

paster quickstart tg-advanced-authentication

[/codesyntax]

For simplicity, the name of the package will be just project. I’ll use Mako templates, but there is no problem if you want to use Genshi; we won’t be working much on the templates. And, finally, answer no to the question Do you need authentication and authorization in this project?, that’s just what we are going to set up manually in what is left of this post.

To test your new project, cd into your tg-advanced-authentication directory, go to a command line and run the following command:

[codesyntax lang=”bash”]

paster serve --reload development.ini

[/codesyntax]

Open your favorite browser and go to http://localhost:8080/. You should see a page with a big title “Welcome to TurboGears 2”.

Step 2. Create a VirtualHost to use as a proxy for the application

We need a VirtualHost because Facebook needs the application to be running on a well formed domain and localhost:8080 won’t work. Facebook Connect will only work if you access the application on the domain specified in the Facebook Application settings.

Use the following VirtualHost template to create your Apache configuration file:

[codesyntax lang=”apache”]

<VirtualHost *:80>
        ServerName example.com
        DocumentRoot /path/to/document/root

        Errorlog  /path/to/document/root/logs/error_log
        Customlog /path/to/document/root/logs/access_log "%h %l %u %t \"%r\" %>s %b"

        AddDefaultCharset utf-8

        ProxyPreserveHost On
        ProxyPass /public !
        ProxyPass /error !

        <Location />
                Order allow,deny
                allow from all
                ProxyPass http://127.0.0.1:8080/
                ProxyPassReverse http://127.0.0.1:8080/
        </Location>

        <Directory /path/to/document/root>
                Options FollowSymLinks
                AllowOverride All
        </Directory>
</VirtualHost>

[/codesyntax]

The document root should point to directory containing a directory called logs and a symlink to the public directory within your project (project/project/public). Make sure Apache can read those locations. Also, I assume you’re testing locally so you may want to add an alias in /etc/hosts to example.com (or the domain you used in the VirtulHost configuration file).

Start or reload Apache and serve (paster serve --reload development.ini) the application. Go to your favorite browser and open http://example.com/, you should see the page with the big title again.

Step 3. Add custom middleware for authentication and authorization support

When TurboGears receives a request, the request is passed through a series of middleware that take care of the process of creating a response and finally a page or a resource that you can access in the web browser. Two of those middleware are for authentication and authorization and we are about to configure them:

Let’s start by opening project/config/middleware.py and change its content to look exactly like the code below:

[codesyntax lang=”python”]

# -*- coding: utf-8 -*-
"""WSGI middleware initialization for the project application."""

from repoze.who.config import make_middleware_with_config

from project.config.app_cfg import base_config
from project.config.environment import load_environment

__all__ = ['make_app']

# Use base_config to setup the necessary PasteDeploy application factory.
# make_base_app will wrap the TG2 app with all the middleware it needs.
make_base_app = base_config.setup_tg_wsgi_app(load_environment)

def make_app(global_conf, full_stack=True, **app_conf):
    """
    Set project up with the settings found in the PasteDeploy configuration
    file used.

    :param global_conf: The global settings for project (those
        defined under the ``[DEFAULT]`` section).
    :type global_conf: dict
    :param full_stack: Should the whole TG2 stack be set up?
    :type full_stack: str or bool
    :return: The project application with all the relevant middleware
        loaded.

    This is the PasteDeploy factory for the project application.

    ``app_conf`` contains all the application-specific settings (those defined
    under ``[app:main]``.

    """
    app = make_base_app(global_conf, full_stack=True, **app_conf)

    # Wrap your base TurboGears 2 application with custom middleware here

    app = make_middleware_with_config(
        app,
        global_conf,
        app_conf.get('who.config_file','who.ini'),
        app_conf.get('who.log_file','stdout'),
        app_conf.get('who.log_level','debug')
    )

    return app

[/codesyntax]

What we just did was to add a custom middleware for authentication and authorization that we can configure using a .INI file.

To make things easier during development we need to define a logger for repoze.who and repoze.what. That way we know what’s going on while the request is being processed. That said, add the following code to your development.ini , just put it near the other logger definitions:

[codesyntax lang=”ini”]

# A logger for authentication, identification and authorization -- this is
# repoze.who and repoze.what:
[logger_auth]
level = WARN
handlers =
qualname = auth

[/codesyntax]

Step 4. Create SQLAlchemy models for storing users, groups and permissions.

If we need authentication and authorization, that means we have users and there are different kind of users and every kind of users will be allowed or forbidden to do something in the application. We need to store all that information somehow, here we’re going to use a database and three models: User, Group (kind of user) and Permission (what an user of a certain kind is allowed to do).

TurboGears already created an auth module for you, but it’s empty. To fix that, replace the content of the module project.model.auth with following code:

[codesyntax lang=”python”]

# -*- coding: utf-8 -*-

"""
Auth* related model.

This is where the models used by :mod:`repoze.who` and :mod:`repoze.what` are
defined.

It's perfectly fine to re-use this definition in the frik application,
though.

"""

import os
import sys

try:
    from hashlib import sha1
except ImportError:
    sys.exit('ImportError: No module named hashlib\n'
             'If you are on python2.4 this library is not part of python. '
             'Please install it. Example: easy_install hashlib')

from datetime import datetime

from sqlalchemy import Table, ForeignKey, Column
from sqlalchemy.types import Unicode, Integer, DateTime
from sqlalchemy.orm import relation, synonym

from project.model import DeclarativeBase, metadata, DBSession

__all__ = ['User', 'Group', 'Permission']

#
# Association tables
#

# This is the association table for the many-to-many relationship between
# groups and permissions. This is required by repoze.what.
group_permission_table = Table('GroupPermissions', metadata,
    Column('group_id', Integer, ForeignKey('Groups.id',
        onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
    Column('permission_id', Integer, ForeignKey('Permissions.id',
        onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
)

# This is the association table for the many-to-many relationship between
# groups and members - this is, the memberships. It's required by repoze.what.
user_group_table = Table('UserGroups', metadata,
    Column('user_id', Integer, ForeignKey('Users.id',
        onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
    Column('group_id', Integer, ForeignKey('Groups.id',
        onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
)

#
# *The auth* model itself
#

class Group(DeclarativeBase):
    """
    Group definition for :mod:`repoze.what`.

    Only the ``group_name`` column is required by :mod:`repoze.what`.

    """

    __tablename__ = 'Groups'

    # columns

    id = Column(Integer, autoincrement=True, primary_key=True)
    slug = Column(Unicode(16), unique=True, nullable=False)
    name = Column(Unicode(255))
    created = Column(DateTime, default=datetime.now)

    # relations

    users = relation('User', secondary=user_group_table, backref='groups')

    # special methods

    def __repr__(self):
        return '<Group: name=%r>' % self.group_name

    def __unicode__(self):
        return self.group_name

# The 'info' argument we're passing to the email_address and password columns
# contain metadata that Rum (http://python-rum.org/) can use generate an
# admin interface for your models.
class User(DeclarativeBase):
    """
    User definition.

    This is the user definition used by :mod:`repoze.who`, which requires at
    least the ``user_name`` column.

    """
    __tablename__ = 'Users'

    # columns

    id = Column(Integer, autoincrement=True, primary_key=True)
    name = Column(Unicode(255))
    _email = Column(Unicode(255), unique=True, info={'rum': {'field':'Email'}})
    _password = Column('password', Unicode(80), info={'rum': {'field':'Password'}})
    picture = Column(Unicode(255))

    fbid = Column(Integer, unique=True)
    twid = Column(Integer, unique=True)

    twitter_key = Column(Unicode(100), unique=True)
    twitter_secret = Column(Unicode(100), unique=True)

    created = Column(DateTime, default=datetime.now)

    # email and user_name properties
    def _get_email(self):
        return self._email

    def _set_email(self, email):
        self._email = email.lower()

    email = synonym('_email', descriptor=property(_get_email, _set_email))

    # password property
    def _set_password(self, password):
        """Hash ``password`` on the fly and store its hashed version."""
        # Make sure password is a str because we cannot hash unicode objects
        if isinstance(password, unicode):
            password = password.encode('utf-8')
        salt = sha1()
        salt.update(os.urandom(60))
        hash = sha1()
        hash.update(password + salt.hexdigest())
        password = salt.hexdigest() + hash.hexdigest()
        # Make sure the hashed password is a unicode object at the end of the
        # process because SQLAlchemy _wants_ unicode objects for Unicode cols
        if not isinstance(password, unicode):
            password = password.decode('utf-8')
        self._password = password

    def _get_password(self):
        """Return the hashed version of the password."""
        return self._password

    password = synonym('_password', descriptor=property(_get_password, _set_password))

    # class methods

    @classmethod
    def by_email_address(cls, email):
        """Return the user object whose email address is ``email``."""
        return DBSession.query(cls).filter(cls.email == email).first()

#    @classmethod
#    def by_user_name(cls, username):
#        """Return the user object whose user name is ``username``."""
#        return DBSession.query(cls).filter(cls.user_name == username).first()

    # non-column properties

    @property
    def permissions(self):
        """Return a set with all permissions granted to the user."""
        perms = set()
        for g in self.groups:
            perms = perms | set(g.permissions)
        return perms

    # other methods

    def validate_password(self, password):
        """
        Check the password against existing credentials.

        :param password: the password that was provided by the user to
            try and authenticate. This is the clear text version that we will
            need to match against the hashed one in the database.
        :type password: unicode object.
        :return: Whether the password is valid.
        :rtype: bool

        """
        hash = sha1()
        if isinstance(password, unicode):
            password = password.encode('utf-8')
        hash.update(password + str(self.password[:40]))
        return self.password[40:] == hash.hexdigest()

    # special methods

    def __repr__(self):
        return '<User: name=%r, email=%r>' % (self.name, self.email)

    def __unicode__(self):
        return self.name

class Permission(DeclarativeBase):
    """
    Permission definition for :mod:`repoze.what`.

    Only the ``permission_name`` column is required by :mod:`repoze.what`.

    """

    __tablename__ = 'Permissions'

    # columns

    id = Column(Integer, autoincrement=True, primary_key=True)
    name = Column(Unicode(63), unique=True, nullable=False)
    description = Column(Unicode(255))

    # relations

    groups = relation(Group, secondary=group_permission_table, backref='permissions')

    # special methods

    def __repr__(self):
        return '<Permission: name=%r>' % self.permission_name

    def __unicode__(self):
        return self.permission_name

[/codesyntax]

Those models are a modified version of models you would get using paster quickstart -a project. Some of the changes I’ve made are listed below:

  • I changed the name of some columns: group_name was renamed to slug; permission_name and display_name were renamed to name, group_id, permission_id and user_id were renamed to id and email_address was reneamed to email.
  • The name of the tables were changed to Groups, Users, Permissions, UserGroups and GroupPermissions.
  • The User model doesn’t have an user_name column. I decided to remove the user_name column and use the email as username for normal login. If the user is using Facebook Connect or Sign in wth Twitter then it will be authenticated using the respective OAuth tokens.
  • The method by_user_name was removed from User class and __repr__ and __unicode__ methods were updated according to the other changes.

Don’t forget to import User, Group and Permission in your project/model/__init__.py file:

[codesyntax lang=”python”]

from project.model.auth import User, Group, Permission

[/codesyntax]

Step 5. Create the .INI file

Back in Step 3, when we added a custom middleware for authentication and authorization we set the configuration file as who.ini. In that file we’re going to describe the plugins we’ll use for identification and authorization, to remember credentials and provide additional user information (the metadata). For Part 1 of this post the .INI file we need is shown below:

[codesyntax lang=”ini”]

# Sample of a who.ini file from which to begin configuring
# this looks a lot like the "quickstart" application\'s setup,
# minus the translation capability...

[plugin:form]
# Redirecting form which does login via a "post"
# from a regular /login form
use = repoze.who.plugins.friendlyform:FriendlyFormPlugin
login_form_url = /login
login_handler_path = /authenticate
logout_handler_path = /logout
rememberer_name = ticket
post_login_url = /post_login
post_logout_url = /post_logout

[plugin:ticket]
# Cookie-based session identification storage
use = repoze.who.plugins.auth_tkt:make_plugin
secret = '975caff3876fdd38214a9cff43484cf8bc712915'
cookie_name = 'AuthenticationTicket'

[plugin:sqlauth]
# An SQLAlchemy authorization plugin
use = project.lib.auth:authenticator

#
# Now the configuration starts wiring together the pieces
#

[general]
request_classifier = repoze.who.classifiers:default_request_classifier
challenge_decider = repoze.who.classifiers:default_challenge_decider

[identifiers]
# We can decide who the user is trying to identify as using either
# a fresh form-post, or the session identifier cookie
plugins =
    form;browser
    ticket

[authenticators]
plugins =
    sqlauth

[challengers]
plugins =
    form;browser

[mdproviders]
# Metadata providers are the things that actually look up a user's credentials
# here we have a plugin that provides "user" information (user) and another,
# which acts as an adapter to the first, to provide group/permission information.
plugins =
    project.lib.auth:user
    project.lib.auth:group

[/codesyntax]

Let’s talk about what this file does. First, it defines three plugins:

  • form, an instance of repoze.who.plugins.friendlyform.FriendlyFormPlugin responsable for showing the login form and collecting user credentials (email and password) to be used later by other plugins to complete the authentication process.
  • ticket, an instance of repoze.who.plugins.auth_tkt.AuthTktCookiePlugin. It’s the plugin that creates the cookies to keep the user logged in and delete them after he/she logs out.
  • sqlauth, an instance of repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin. It’s responsable for taking the credentials collected by form and make sure an user with that email address and that password exists in the database. The plugin is defined in .INI file but configured in project.lib.auth, a module we’ll create in the next step.

The last part of the file makes sure every plugin is assigned to right part of the process of authentication. form and ticket are Identifiers, sqlauth is an Authenticator, form is also a Challenger and, user and group, two plugins we’ll define later, are the Metadata Providers. Read more more about type of plugins used by repoze.who.

Step 6. Create project.lib.auth module

In previous step we referenced a project.lib.auth module that still doesn’t exist. That module is a complement to the authentication and authorization middleware we’re configuring. There, we’ll create instances for the Authenticator plugin form and the Metadata Providers user and group. Also, in Part 2 and Part 3 of this post, we’ll create new Identifier plugins to support Facebook Connect and Sign in with Twitter.

At this point, the module should look something like the code below:

[codesyntax lang=”python”]

# coding: utf-8

"""Intended to work like a quick-started SQLAlchemy plugin"""

from repoze.what.middleware import AuthorizationMetadata
from repoze.what.plugins.pylonshq import booleanize_predicates
from repoze.what.plugins.sql import configure_sql_adapters
from repoze.who.plugins.sa import SQLAlchemyAuthenticatorPlugin
from repoze.who.plugins.sa import SQLAlchemyUserMDPlugin

from project.model import DBSession, User, Group, Permission

#
# authenticator plugin
#

authenticator = SQLAlchemyAuthenticatorPlugin(User, DBSession)
# users whoe log in using a regular form use their email address as username
authenticator.translations['user_name'] = 'email'

#
# metadata provider plugins
#

#
# From the documentation in repoze.what.plugins.sql.adapters package
#
# For developers to be able to use the names they want in their model, both the
# groups and permissions source adapters use a "translation table" for the
# field and table names involved:
#  * Group source adapter:
#    * "section_name" (default: "group_name"): The name of the table field that
#      contains the primary key in the groups table.
#    * "sections" (default: "groups"): The groups to which a given user belongs.
#    * "item_name" (default: "user_name"): The name of the table field that
#      contains the primary key in the users table.
#    * "items" (default: "users"): The users that belong to a given group.
#  * Permission source adapter:
#    * "section_name" (default: "permission_name"): The name of the table field
#      that contains the primary key in the permissions table.
#    * "sections" (default: "permissions"): The permissions granted to a given
#      group.
#    * "item_name" (default: "group_name"): The name of the table field that
#      contains the primary key in the groups table.
#    * "items" (default: "groups"): The groups that are granted a given
#      permission.
adapters = configure_sql_adapters(User, Group, Permission, DBSession,
                                  group_translations={'section_name': 'slug',
                                                      'item_name': 'email'},
                                  permission_translations={'section_name': 'name',
                                                           'item_name': 'slug'})

user = SQLAlchemyUserMDPlugin(User, DBSession)
# we get metadata based on user id, the only attribute an user is guranteed to
# have regardles the authentication method he/she uses (Form, Facebook, Twitter)
user.translations['user_name'] = 'email'

group = AuthorizationMetadata({'sqlauth': adapters['group']}, {'sqlauth': adapters['permission']})

# THIS IS CRITICALLY IMPORTANT!  Without this your site will
# consider every repoze.what predicate True!
booleanize_predicates()

[/codesyntax]

Wondering what those *.translations['foo'] = 'bar' means? Remember those column names I changed when we where defining the models? Turns out the default names are the names all these plugins expect to find and since we changed them we need to provide translations so the plugins can still work.

At this point, we’ve already finished defining the authentication and authorization middleware. But we still need to do a few things before we can test our changes.

Step 7. Create default Users, Groups and Permissions

We already defined models for storing users, groups and permissions, but if we want to test authentication, we need to add some data to those models. TurboGears allows you to create instances of your models and insert them right when the database is being created. All you need to do is create those instances in bootstrap function of project.websetup.bootstrap module.

Add the following code to body of the bootstrap function:

[codesyntax lang=”python”]

    from sqlalchemy.exc import IntegrityError

    try:
        u = model.User()
        u.name = u'Example manager'
        u.email = u'[email protected]ain.com'
        u.password = u'managepass'

        model.DBSession.add(u)

        g = model.Group()
        g.slug = u'managers'
        g.name = u'Managers Group'

        g.users.append(u)

        model.DBSession.add(g)

        p = model.Permission()
        p.name = u'manage'
        p.description = u'This permission give an administrative right to the bearer'
        p.groups.append(g)

        model.DBSession.add(p)

        editor = model.User()
        editor.name = u'Example editor'
        editor.email = u'[email protected]'
        editor.password = u'editpass'

        model.DBSession.add(editor)
        model.DBSession.flush()
        transaction.commit()
    except IntegrityError:
        print 'Warning, there was a problem adding your auth data, it may have already been added:'
        import traceback
        print traceback.format_exc()
        transaction.abort()
        print 'Continuing with bootstrapping...'

[/codesyntax]

Then stop your application, run paster setup-app development.ini and start you application again. Now there is two users, one group and one permission. We’re almost ready to test.

Step 8. Create necessary controller methods

Some of the plugins defined in the .INI file need additional help to fullfil their purpose, form is one of them. It needs some actions to be defined, the actions we specified in login_form_url, post_login_url and post_logout_url.

We also may want to define other actions so we can fully test the authorization capabilities of TurboGears. /auth simply let you see a page explaining authentication and authorization, /mange_permission_only can only be seen by users with the manage permission and editor_user_only can only be seen by the user with email address [email protected]. All that is possible using something called predicates.

Add the following imports at the top of project.controllers.root module:

[codesyntax lang=”python”]

from tgext.admin.tgadminconfig import TGAdminConfig
from tgext.admin.controller import AdminController
from repoze.what import predicates

from project import model
from project.controllers.secure import SecureController

[/codesyntax]

Then, add the following code to the body of the RootController of your application:

[codesyntax lang=”python”]

   
    secc = SecureController()

    admin = AdminController(model, DBSession, config_type=TGAdminConfig)

    @expose('project.templates.authentication')
    def auth(self):
        """Display some information about auth* on this application."""
        return dict(page='auth')

    @expose('project.templates.index')
    @require(predicates.has_permission('manage', msg=l_('Only for managers')))
    def manage_permission_only(self, **kw):
        """Illustrate how a page for managers only works."""
        return dict(page='managers stuff')

    @expose('project.templates.index')
    @require(predicates.is_user('[email protected]', msg=l_('Only for the editor')))
    def editor_user_only(self, **kw):
        """Illustrate how a page exclusive for the editor works."""
        return dict(page='editor stuff')

    @expose('project.templates.login')
    def login(self, came_from=url('/')):
        """Start the user login."""
        # logged-in users aren't allowed to see this page
#        if request.identity and 'repoze.who.userid' in request.identity:
#            return redirect(came_from)
        logins = request.environ['repoze.who.logins']
        if logins > 0:
            flash(_('Wrong credentials'), 'warning')
        return dict(page='login', login_counter=str(logins), came_from=came_from)

    @expose()
    def post_login(self, came_from='/'):
        """
        Redirect the user to the initially requested page on successful
        authentication or redirect her back to the login page if login failed.

        """
        if not request.identity:
            login_counter = request.environ['repoze.who.logins'] + 1
            redirect('/login', came_from=came_from, __logins=login_counter)
        flash(_('Welcome back, %s!') % request.identity['user'].name)
        redirect(came_from)

    @expose()
    def post_logout(self, came_from=url('/')):
        """
        Redirect the user to the initially requested page on logout and say
        goodbye as well.

        """
        flash(_('We hope to see you soon!'))
        redirect(came_from)

[/codesyntax]

Finally, add the following code to your project.lib.base module, just before the return call in the __call__ method of BaseController. This is necessary so the logged in user’s information, if any, is available to the templates and the application:

[codesyntax lang=”python”]

        request.identity = request.environ.get('repoze.who.identity')
        tmpl_context.identity = request.identity

[/codesyntax]

Then you’ll need to insert from tg import request at the top of that module.

Step 9. Create SecureController

Open project.controllers.secure module and change its content to the following code:

[codesyntax lang=”python”]

# -*- coding: utf-8 -*-

"""Sample controller with all its actions protected."""
from tg import expose, flash
from pylons.i18n import ugettext as _, lazy_ugettext as l_
from repoze.what.predicates import has_permission

from project.lib.base import BaseController

__all__ = ['SecureController']

class SecureController(BaseController):
    """Sample controller-wide authorization"""

    # The predicate that must be met for all the actions in this controller:
    allow_only = has_permission('manage',
                                msg=l_('Only for people with the "manage" permission'))

    @expose('project.templates.index')
    def index(self):
        """Let the user know that's visiting a protected controller."""
        flash(_("Secure Controller here"))
        return dict(page='index')

    @expose('project.templates.index')
    def some_where(self):
        """Let the user know that this action is protected too."""
        return dict(page='some_where')

[/codesyntax]

And that’s the last file we’ll need to edit for now. Start or restart the application if you haven’t done that already and open http://example.com/ in your browser. If everything is fine you should see the “Welcome to TurboGears 2” page, but this time, a “Login” link should appear in the navigation section of that page.

Now, start playing around, try to login using [email protected] as username and managepass as password. Then go to http://example.com/auth and try to follow the instructions, but remember, we’re using email addresses as username.

Conclusion

We have done, with a lot of effort, what we could have just achieved running paster quickstart -a project. However, what we are going to do in Part 2 and Part 3 of this post wouldn’t be possible without going through the process we just completed.

Come back next week to see how to add support for Facebook Connect to this TurboGears application. Thanks for reading.

Update: Nov 19/2011

There is a problem when trying to access the admin/permissions/ and admin/groups/ pages. To fix it you need to add more translations when defining the AdminController (http://goo.gl/bh4lL). Thank you to psilar for letting me know about this bug.