September 06, 2010

shabti_auth_repozewho – authentication using repoze.who

This template brings repoze.who’s authentication and repoze.what’s authorisation functions to Pylons. Gustavo Narea’s quickstart plugin for repoze.who usefully integrates repoze.who authentication with the standard Shabti identity model expressed in elixir, making this template immediately usable for extension and development.

About repoze.who auth

Chris McDonough’s repoze.who runs as middleware. The overview shown below is lifted straight from the repoze.who docs .

Note

Overview

repoze.who is an identification and authentication framework for arbitrary WSGI applications. It acts as WSGI middleware.

repoze.who is inspired by Zope 2’s Pluggable Authentication Service (PAS) (but repoze.who is not dependent on Zope in any way; it is useful for any WSGI application). It provides no facility for authorization (ensuring whether a user can or cannot perform the operation implied by the request). This is considered to be the domain of the WSGI application.

It attempts to reuse implementations from paste.auth for some of its functionality.

This template follows the same approach as the basic shabti_auth – boilerplate authentication template and re-uses the majority of the code in that template. User authentication is handled by the middleware repoze.who module.

After the user performs a successful authentication, the request.environ dict gains a new key: repoze.who.identity, itself a dictionary with the user key retrieving the corresponding elixir model entity object, this binding persists until the user explicitly signs off.

The @authorize() decorator from shabti_auth – boilerplate authentication is swapped for a @require() decorator stolen^W adapted from TurboGears2 and is immediately available for use in applying authorization constraints on access to controllers and/or actions.

Persistence of user, group and permission data is handled in the application’s elixir-mediated model and is amenable to manipulation via the usual REST controller approach.

Note

shabti_auth_repozewho source code is in the bitbucket code repository

(Credit where credit’s due Dept: most of this template is based on Gustavo Narea‘s work on the repoze plugins and the successful functioning of the template application is due to key advice given by Chris McDonough.)

Using the template

After successfully installing Shabti, additional paster templates will be available. Simply create a Shabti-configured project by specifying that paster should use the shabti auth repozewho template:

$ paster create -t shabti_auth_repozewho myproj

These are the option dialogue choices appropriate for the Shabti auth template — which uses mako templates and SQLAlchemy ...

(mako/genshi/jinja/etc: Template language) ['mako']:
(True/False: Include SQLAlchemy 0.4 configuration) [False]: True
(True/False: Setup default appropriate for Google App Engine) [False]:

Once the project has been created, navigate to the project directory and initialise the store by running the project setup script:

$ paster setup-app development.ini

If successful, the setup script will stream the log of database transactions to stdout, e.g.:

CREATE TABLE user_groups__group_users (
    user_id INTEGER NOT NULL,
    group_id INTEGER NOT NULL,
    PRIMARY KEY (user_id, group_id),
     CONSTRAINT user_groups_fk FOREIGN KEY(user_id)
                    REFERENCES user (id),
     CONSTRAINT group_users_fk FOREIGN KEY(group_id)
                    REFERENCES "group" (id)
)

Once the database has been initialised, start the Pylons web app with:

$ paster serve --reload development.ini

The Shabti repoze.who auth template’s variant on the standard Pylons welcome screen is browsable at at http://localhost:5000/ ...

Welcome screen

../_images/shabti_auth_repozewho_welcome.jpg

signin page

../_images/shabti_auth_repozewho_signin.jpg

shabti model unauthenticated

../_images/shabti_auth_repozewho_model_unauth.jpg

shabti model authenticated

../_images/shabti_auth_repozewho_model_auth.jpg

Authentication and authorisation

Authentication via GETting a standard login page

In the case of a standard login page, requests directed to the URL are detected by the repoze.who middleware (when appropriately configured) which intercepts the request and handles the login process, directly accessing the Elixir identity model and validating the entered password. Upon successful authentication the user is re-routed to a (configured) post-login URL.

Successful authentication sequence for a standard login page

Lengthy but highly informative as it shows the various stages of the repoze.who/what middleware process of relating plugin resources to the characteristics and requirements of the request and the particular type of authentication in play (authenticating an XMLRPC request would require different handling to that of a browser-originated requeste)

Rendering the login form

-- repoze.who request started (/login/index) --

request classification: browser
identifier plugins registered
       [<FriendlyRedirectingFormPlugin 31512656>,
       <AuthTktCookiePlugin 31555888>]
identifier plugins matched for classification "browser":
       [<FriendlyRedirectingFormPlugin 31512656>,
       <AuthTktCookiePlugin 31555888>]
no identity returned from
       <FriendlyRedirectingFormPlugin 31512656> (None)
no identity returned from
       <AuthTktCookiePlugin 31555888> (None)
identities found: []
no identities found, not authenticating
no challenge required

-- repoze.who request ended (/login/index) --

Going to to /login/signin (as configured to do so) with username and password entered in the login form.

-- repoze.who request started (/login/signin) --

request classification: browser
identifier plugins registered
       [<FriendlyRedirectingFormPlugin 31512656>,
       <AuthTktCookiePlugin 31555888>]
identifier plugins matched for classification "browser":
       [<FriendlyRedirectingFormPlugin 31512656>,
       <AuthTktCookiePlugin 31555888>]
identity returned from
       <FriendlyRedirectingFormPlugin 31512656>:
           {'login': 'admin', 'password': 'admin'}
no identity returned from
       <AuthTktCookiePlugin 31555888> (None)
identities found:
       [(<FriendlyRedirectingFormPlugin 31512656>,
       {'login': 'admin', 'password': 'admin'})]

The authenticator plugin uses SQLachemy to access the identity model, perform password validation and retrieve the entity from the database.

authenticator plugins registered
       [<repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin
           object at 0x1e180f0>]
authenticator plugins matched for classification "browser":
       [<repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin
           object at 0x1e180f0>]
userid returned from
       <repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin
       object at 0x1e180f0>: "admin"
identities authenticated:
       [((0, 0),
       <repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin
           object at 0x1e180f0>,
       <FriendlyRedirectingFormPlugin 31512656>,
           {'login': 'admin', 'password': 'admin',
           'repoze.who.userid': 'admin'}, 'admin')]

Group and Permissions information is also retrieved from the database and the entity is fully populated with data. Three cookies are now available for use as state-preserving store.

 User belongs to the following groups: (u'Administrators',)
 User has the following permissions: ()
 static downstream application replaced with 302 Found
The resource was found at
/login/welcome?__foo=0

 no challenge required
 remembering via headers from
    <FriendlyRedirectingFormPlugin 31512656>:
    [('Set-Cookie',
     'authtkt=d075d12ec8606124b4f6adeb8838ade049923f60admin!;
      Path=/'),
     ('Set-Cookie',
     'authtkt=d075d12ec8606124b4f6adeb8838ade049923f60admin!;
     Path=/; Domain=localhost:5000'),
     ('Set-Cookie',
     'authtkt=d075d12ec8606124b4f6adeb8838ade049923f60admin!;
     Path=/; Domain=.localhost:5000')]

 -- repoze.who request ended (/login/signin) --

Login was successful, cookies baked and consumed, so the user is re-routed to the post-login URL.

-- repoze.who request started (/login/welcome) --

request classification: browser
identifier plugins registered
                       [<FriendlyRedirectingFormPlugin 31512656>,
                        <AuthTktCookiePlugin 31555888>]
identifier plugins matched for classification "browser":
                       [<FriendlyRedirectingFormPlugin 31512656>,
                        <AuthTktCookiePlugin 31555888>]
no identity returned from
           <FriendlyRedirectingFormPlugin 31512656> (None)
identity returned from
   <AuthTktCookiePlugin 31555888>:
           {'tokens': [''], 'timestamp': 1234321248,
           'repoze.who.userid': 'admin', 'userdata': ''}
identities found:
           [(<AuthTktCookiePlugin 31555888>,
               {'tokens': [''], 'timestamp': 1234321248,
               'repoze.who.userid': 'admin', 'userdata': ''})]
authenticator plugins registered
           [<repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin
               object at 0x1e180f0>]
authenticator plugins matched for classification "browser":
           [<repoze.who.plugins.sa.SQLAlchemyAuthenticatorPlugin
               object at 0x1e180f0>]
userid preauthenticated by
           <AuthTktCookiePlugin 31555888>:
           "admin" (repoze.who.userid set)
identities authenticated:
           [((0, 0), None, <AuthTktCookiePlugin 31555888>,
               {'tokens': [''], 'timestamp': 1234321248,
                'repoze.who.userid': 'admin', 'userdata': ''},
                'admin')]
User belongs to the following groups: (u'Administrators',)
User has the following permissions: ()
no challenge required

-- repoze.who request ended (/login/welcome) --

The important point to note is that the User’s group membership details have been retrieved from the database-held identity model and are ready for immediate use when checking authorisation.

The other point to note is that processing was all done upstream of the application, obviating any necessity for the developer to write authorisation-handling code in the controller.

Extending the capability and range of auth’n’auth coverage is largely effected by adding the requisite plugin (e.g. the :class:SQLAlchemyAuthenticatorPlugin referenced above) along with the appropriate configuration directives.

In-line or “URL-forwarding” Authentication

When an authenticated user agent fails to present credentials when accessing a controlled resource, in-line authentication can be more than just a user convenience, it can significantly improve usability.

When attempting to access the resource, the user’s request is detected, authentication and authorisation credentials are checked and, on failure, the user is automatically routed to a login page. Upon successful authentication, credentials are available to be presented and, if duly authenticated and authorised, the user is returned to the originally-requested resource.

Again, all this is handled upstream, the main task of the developer is to express the permissions architecture as permutations of access roles or rules and issue a 401 response when the proffered credentials are inadequate.

This can be done with a decorator such as TurboGears’ @require() decorator (TG2 uses repoze.who as its authentication component)

@require(Any(has_permission('edit-posts'), is_user('admin')))

The shabti template uses a helper to perform sub-action permissions checks. The various fragments of the links in the calling chain are shown below:

In-line authentication sequencing in the template

controllers/demo.py

def privindex(self):
    if not h.signed_in():
        abort(401, 'You are not authenticated')
    # [ ... ]
    return render('test.mak')

lib/helpers.py - signed_in

def signed_in():
    return permissions.SignedIn().check()

lib/permissions.py - SignedIn().check()

class SignedIn(object):

    def check(self):
        return (get_user() is not None)

lib/auth/__init__.py - get_user

def get_user():
    if 'repoze.who.identity' in request.environ:
        return request.environ.get('repoze.who.identity')['user']
    else:
        return None

The repoze.who middleware detects the outgoing 401 emitted by the controller action, records the origin of the 401 and creates a new URL to re-route the user to a login form. The original auth-triggering URL is urlencoded and inserted into the QUERY_STRING as the value of the attribute came_from.

http://localhost:5000/login/index\
?came_from=http%3A%2F%2Flocalhost%3A5000%2Fdemo%2Fprivindex

(formatted over two lines for presentation purposes)

Failed login attempts are optionally re-routed or just return to the login form. The repoze.who identity checker that generates the form also accepts either a form to use instead of the default login form or a callable which generates a form to be used in place of the default.

Notes on the template

development.ini

repoze.who configuration options are typically added in development.ini, albeit it could just be a reference to a separate .ini file:

who.config_file = %(here)s/{{package}}/config/who.ini
who.log_level = debug
who.log_file = stdout

In this instance however, the SQLAlchemy-enabled plugin used in the shabti auth_repozewho template abandons the .ini file in favour of a configuration implemented in Python. It’s something of a trade-off of the convenience of a SQLAlchemy-backed auth’n’auth solution for a very slight increase in configuration complexity.

Separating the configuration of the repoze.who middleware results in a cleaner middleware.py file:

config/middleware.py

repoze.who runs as middleware, so the standard Pylons middleware file needs augmenting with the app (defined in lib/auth/__init__.py), here expressed as a diff:

--- orig.middleware.py_tmpl
+++ new.middleware.py_tmpl
@@ -47,6 +47,10 @@

     # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)

+    # Import and add ready-configured repoze.what quickstart app
+    from {{package}}.lib.auth import add_auth
+    app = add_auth(app)
+
     if asbool(full_stack):
         # Handle Python exceptions
         app = ErrorHandler(app, global_conf, **config['pylons.errorware'])

lib/auth/__init__.py

The first step is to bind values to the variables that will be provided as arguments to setup_sql_auth(). This is a good example of the difference between the .ini config file approach and this one, (of effecting the configuration within the friendly confines of an executable Python source file). In a .ini approach, the model entity classes would have to be specified by using strings (e.g. “User”, “Group”), to be later turned into actual references to the object’s class.

The “translations” dictionary maps the repoze identity model User, Group, Permission and password validation function to the field names and function that are actually used in the standard Shabti elixir identity model.

With repoze.who handling access to the database, the amount of code in the standard get_user() function is usefully reduced.

user_name = 'username'
user_class = User
group_class = Group
permission_class = Permission
dbsession = Session
translations={'user_name': 'username',
              'users': 'users',
              'group_name': 'name',
              'groups': 'groups',
              'permission_name': 'name',
              'permissions': 'permissions',
              'validate_password': 'validate_password' }


def add_auth(app, skip_authentication):
    """Add authentication and authorization middleware to the ``app``."""
    return setup_sql_auth(app,
            user_class,
            group_class,
            permission_class,
            dbsession,
            # form_plugin=loginform, --not required
            form_identifies=True,
            cookie_secret='secretsquirrel',
            cookie_name='authtkt',
            login_url='/login/index',
            post_login_url='/login/welcome_back',
            post_logout_url='/login/see_you_later',
            login_counter_name='__logins',
            translations=translations,
            skip_authentication=skip_authentication)

def get_user():
    """Return the current user's database object."""
    if 'repoze.who.identity' in request.environ:
        return request.environ.get('repoze.who.identity')['user']
    else:
        return None

Note

The URLs “/login_handler” and “/login_handler” are special, they act as a kind of “pre-named route”. Because repoze.who runs before any of the Pylons app routines are called, no routes have been yet defined, which is why these two key routes are “firm-coded”, i.e. configurable at application startup but not thereafter.

lib/decorators.py

The authorize() decorator that is used other Shabti templates is replaced in the auth_repozewho template by the @require() decorator, taken directly from TurboGears trunk:

from repoze.what.authorize import check_authorization, NotAuthorizedError

def require(predicate):
    """
    Make repoze.what verify that the predicate is met.

    :param predicate: A repoze.what predicate.
    :return: The decorator that checks authorization.

    """

    @decorator
    def check_auth(func, *args, **kwargs):
        environ = request.environ
        try:
            check_authorization(predicate, environ)
        except NotAuthorizedError, reason:
            # TODO: We should warn the user
            # flash(reason, status='warning')
            raise HTTPUnauthorized()

        return func(*args, **kwargs)
    return check_auth

See also

The predicate param specified in the docstring relates to repoze.who predicates, a repoze.who plug-in provides some useful basic predicates as a starter. More information generally about creating and using repoze.who predicates can be found in the Controlling access with predicates page in the repoze.who manual and in the repoze.what manual.

controllers/demo.py

The @require() decorator allows access rules to be imposed and the repoze-added identity is immediately retrievable from the environment:

@require(Any(has_permission('edit-posts'), is_user('admin')))
def privindex(self, id):
    user = req.environ.get('repoze.who.identity')
    if not IsLicensedTo(user,id):
        abort(401, 'You are totally not authenticated')
    c.title = 'Test'
    c.identity = user
    c.dataobj = Session.query(MyEntity).filter_by(id=id).one()
    return render('template.mak')

templates/test.mak

Identity usage in the mako template, for eye-watering completeness ...

% if c.identity is not UNDEFINED:
<div>
    <span style="color:red"
        >Authenticated: ${c.identity.username}</span>
</div>
% else:
<div><span style="color:blue">Not Authenticated</span></div>
% endif
author:Graham Higgins <gjh@bel-epa.com>

September 06, 2010