Middleware plugins

This library provides authorization and authentication middleware plugins for aiohttp servers.

These plugins are designed to be lightweight, simple, and extensible, allowing the library to be reused regardless of the backend authentication mechanism. This provides a familiar framework across projects.

There are three middleware plugins provided by the library. The auth_middleware plugin provides a simple system for authenticating a users credentials, and ensuring that the user is who they say they are.

The autz_middleware plugin provides a generic way of authorization using different authorization policies. There is the ACL authorization policy as a part of the plugin.

The acl_middleware plugin provides a simple access control list authorization mechanism, where users are provided access to different view handlers depending on what groups the user is a member of. It is recomended to use autz_middleware with ACL policy instead of this middleware.

Authentication Middleware Usage

The auth_middleware plugin provides a simple abstraction for remembering and retrieving the authentication details for a user across http requests. Typically, an application would retrieve the login details for a user, and call the remember function to store the details. These details can then be recalled in future requests. A simplistic example of users stored in a python dict would be:

from aiohttp_auth import auth
from aiohttp import web

# Simplistic name/password map
db = {'user': 'password',
      'super_user': 'super_password'}


async def login_view(request):
    params = await request.post()
    user = params.get('username', None)
    if (user in db and
        params.get('password', None) == db[user]):

        # User is in our database, remember their login details
        await auth.remember(request, user)
        return web.Response(body='OK'.encode('utf-8'))

    raise web.HTTPUnauthorized()

User data can be verified in later requests by checking that their username is valid explicity, or by using the auth_required decorator:

async def check_explicitly_view(request):
    user = await auth.get_auth(request)
    if user is None:
        # Show login page
        return web.Response(body='Not authenticated'.encode('utf-8'))

    return web.Response(body='OK'.encode('utf-8'))

@auth.auth_required
async def check_implicitly_view(request):
    # HTTPUnauthorized is raised by the decorator if user is not valid
    return web.Response(body='OK'.encode('utf-8'))

To end the session, the user data can be forgotten by using the forget function:

@auth.auth_required
async def logout_view(request):
    await auth.forget(request)
    return web.Response(body='OK'.encode('utf-8'))

The actual mechanisms for storing the authentication credentials are passed as a policy to the session manager middleware. New policies can be implemented quite simply by overriding the AbstractAuthentication class. The aiohttp_auth package currently provides two authentication policies, a cookie based policy based loosely on mod_auth_tkt (Apache ticket module), and a second policy that uses the aiohttp_session class to store authentication tickets.

The cookie based policy (CookieTktAuthentication) is a simple mechanism for storing the username of the authenticated user in a cookie, along with a hash value known only to the server. The cookie contains the maximum age allowed before the ticket expires, and can also use the IP address (v4 or v6) of the user to link the cookie to that address. The cookies data is not encrypted, but only holds the username of the user and the cookies expiration time, along with its security hash:

def init(loop):
    app = web.Application(loop=loop)

    # Create a auth ticket mechanism that expires after 1 minute (60
    # seconds), and has a randomly generated secret. Also includes the
    # optional inclusion of the users IP address in the hash
    policy = auth.CookieTktAuthentication(urandom(32), 60,
                                          include_ip=True)

    # setup middleware in aiohttp fashion
    auth.setup(app, policy)

    app.router.add_route('POST', '/login', login_view)
    app.router.add_route('GET', '/logout', logout_view)
    app.router.add_route('GET', '/test0', check_explicitly_view)
    app.router.add_route('GET', '/test1', check_implicitly_view)

    return app

The SessionTktAuthentication policy provides many of the same features, but stores the same ticket credentials in a aiohttp_session object, allowing different storage mechanisms such as Redis storage, and EncryptedCookieStorage:

from aiohttp_session import get_session, session_middleware
from aiohttp_session.cookie_storage import EncryptedCookieStorage

def init(loop):
    app = web.Application(loop=loop)

    # setup session middleware in aiohttp fashion
    storage = EncryptedCookieStorage(urandom(32))
    aiohttp_session.setup(app, storage)

    # Create an auth ticket mechanism that expires after 1 minute (60
    # seconds), and has a randomly generated secret. Also includes the
    # optional inclusion of the users IP address in the hash
    policy = auth.SessionTktAuthentication(urandom(32), 60,
                                           include_ip=True)

    # setup aiohttp_auth.auth middleware in aiohttp fashion
    auth.setup(app, policy)

    ...

Authorization Middleware Usage

The autz middleware provides follow interface to use in applications:

  • Using autz.permit coroutine.
  • Using autz.autz_required decorator for aiohttp handlers.

The async def autz.permit(request, permission, context=None) coroutine checks if permission is allowed for a given request with a given context. The authorization checking is provided by authorization policy which is set by setup function. The nature of permission and context is also determined by a policy.

The def autz_required(permission, context=None) decorator for aiohttp’s request handlers checks if current user has requested permission with a given context. If the user does not have the correct permission it raises web.HTTPForbidden.

Note that context can be optional if authorization policy provides a way to specify global application context or if it does not require any. Also context parameter can be used to override global context if it is provided by authorization policy.

To use an authorization policy with autz middleware a class of policy should be created inherited from autz.abc.AbstractAutzPolicy. The only thing that should be implemented is permit method (see Custom authorization policy for autz middleware). The autz middleware has a built in ACL authorization policy (see ACL authorization policy for autz middleware).

The recomended way to initialize this middleware is through aiohttp_auth.autz.setup or aiohttp_auth.setup functions. As the autz middleware can be used only with authentication aiohttp_auth.auth middleware it is preferred to use aiohttp_auth.setup.

ACL authorization policy for autz middleware

The autz plugin has a built in ACL authorization policy in autz.policy.acl module. This module introduces an AbstractACLAutzPolicy - the abstract base class to create an ACL authorization policy class. The subclass should define how to retrieve user’s groups.

As the library does not know how to get groups for user and it is always up to application, it provides abstract authorization ACL policy class. Subclass should implement acl_groups method to use it with autz_middleware.

Note that an ACL context can be specified globally while initializing policy or locally through autz.permit function’s parameter. A local context will always override a global one while checking permissions. If there is no local context and global context is not set then the permit method will raise a RuntimeError.

A context is a sequence of ACL tuples which consist of an Allow/Deny action, a group, and a set of permissions for that ACL group. For example:

context = [(Permission.Allow, 'view_group', {'view', }),
           (Permission.Allow, 'edit_group', {'view', 'edit'}),]

ACL tuple sequences are checked in order, with the first tuple that matches the group the user is a member of, and includes the permission passed to the function, to be the matching ACL group. If no ACL group is found, the permit method returns False.

Groups and permissions need only be immutable objects, so can be strings, numbers, enumerations, or other immutable objects.

Note

Groups that are returned by acl_groups (if they are not None) will then be extended internally with Group.Everyone and Group.AuthenticatedUser.

Usage example:

from aiohttp import web
from aiohttp_auth import autz
from aiohttp_auth.autz import autz_required
from aiohttp_auth.autz.policy import acl
from aiohttp_auth.permissions import Permission


# create an acl authorization policy class
class ACLAutzPolicy(acl.AbstractACLAutzPolicy):
    """The concrete ACL authorization policy."""

    def __init__(self, users, context=None):
        # do not forget to call parent __init__
        super().__init__(context)

        # we will retrieve groups using some kind of users dict
        # here you can use db or cache or any other needed data
        self.users = users

    async def acl_groups(self, user_identity):
        """Return acl groups for given user identity.

        This method should return a sequence of groups for given user_identity.

        Args:
            user_identity: User identity returned by auth.get_auth.

        Returns:
            Sequence of acl groups (possibly empty) for the user identity or None.
        """
        # implement application specific logic here
        user = self.users.get(user_identity, None)
        if user is None:
            return None

        return user['groups']


def init(loop):
    app = web.Application(loop=loop)
    ...
    # here you need to initialize aiohttp_auth.auth middleware
    auth_policy = ...
    ...
    users = ...
    # Create application global context.
    # It can be overridden in autz.permit fucntion or in
    # autz_required decorator using local context explicitly.
    context = [(Permission.Allow, 'view_group', {'view', }),
               (Permission.Allow, 'edit_group', {'view', 'edit'})]
    autz_policy = ACLAutzPolicy(users, context)

    # install auth and autz middleware in aiohttp fashion
    aiohttp_auth.setup(app, auth_policy, autz_policy)


# authorization using autz decorator applying to app handler
@autz_required('view')
async def handler_view(request):
    # authorization using permit
    if await autz.permit(request, 'edit'):
        pass


local_context = [(Permission.Deny, 'view_group', {'view', })]

# authorization using autz decorator applying to app handler
# using local_context to override global one.
@autz_required('view', local_context)
async def handler_view_local(request):
    # authorization using permit and local_context to
    # override global one
    if await autz.permit(request, 'edit', local_context):
        pass

Custom authorization policy for autz middleware

Tha autz middleware makes it possible to use custom athorization policy with the same autz public interface for checking user permissions. The follow example shows how to create such simple custom policy:

from aiohttp import web
from aiohttp_auth import autz, auth
from aiohttp_auth.autz import autz_required
from aiohttp_auth.autz.abc import AbstractAutzPolicy

class CustomAutzPolicy(AbstractAutzPolicy):

    def __init__(self, admin_user_identity):
        self.admin_user_identity = admin_user_identity

    async def permit(self, user_identity, permission, context=None):
        # All we need is to implement this method

        if permission == 'admin':
            # only admin_user_identity is allowed for 'admin' permission
            if user_identity == self.admin_user_identity:
                return True

            # forbid anyone else
            return False

        # allow any other permissions for all users
        return True


def init(loop):
    app = web.Application(loop=loop)
    ...
    # here you need to initialize aiohttp_auth.auth middleware
    auth_policy = ...
    ...
    # create custom authorization policy
    autz_policy = CustomAutzPolicy(admin_user_identity='Bob')

    # install auth and autz middleware in aiohttp fashion
    aiohttp_auth.setup(app, auth_policy, autz_policy)


# authorization using autz decorator applying to app handler
@autz_required('admin')
async def handler_admin(request):
    # only Bob can run this handler

    # authorization using permit
    if await autz.permit(request, 'admin'):
        # only Bob can get here
        pass


@autz_required('guest')
async def handler_guest(request):
    # everyone can run this handler

    # authorization using permit
    if await autz.permit(request, 'guest'):
        # everyone can get here
        pass

ACL Middleware Usage

The acl_middleware` plugin (provided by the aiohttp_auth library), is layered on top of the auth_middleware plugin, and provides a access control list (ACL) system similar to that used by the Pyramid WSGI module.

Each user in the system is assigned a series of groups. Each group in the system can then be assigned permissions that they are allowed (or not allowed) to access. Groups and permissions are user defined, and need only be immutable objects, so they can be strings, numbers, enumerations, or other immutable objects.

To specify what groups a user is a member of, a function is passed to the acl_middleware factory which taks a user_id (as returned from the auth.get_auth function) as a parameter, and expects a sequence of permitted ACL groups to be returned. This can be a empty tuple to represent no explicit permissions, or None to explicitly forbid this particular user_id. Note that the user_id passed may be None if no authenticated user exists. Building apon our example, a function may be defined as:

from aiohttp import web
from aiohttp_auth import acl, auth
import aiohttp_session

group_map = {'user': (,),
             'super_user': ('edit_group',),}

async def acl_group_callback(user_id):
    # The user_id could be None if the user is not authenticated, but in
    # our example, we allow unauthenticated users access to some things, so
    # we return an empty tuple.
    return group_map.get(user_id, tuple())

def init(loop):
    ...

    app = web.Application(loop=loop)
    # setup session middleware
    storage = aiohttp_session.EncryptedCookieStorage(urandom(32))
    aiohttp_session.setup(app, storage)

    # setup aiohttp_auth.auth middleware
    policy = auth.SessionTktAuthentication(urandom(32), 60, include_ip=True)
    auth.setup(app, policy)

    # setup aiohttp_auth.acl middleware
    acl.setup(app, acl_group_callback)

    ...

Note that the ACL groups returned by the function will be modified by the acl_middleware to also include the Group.Everyone group (if the value returned is not None), and also the Group.AuthenticatedUser if the user_id is not None.

Instead of acl_group_callback as a coroutine the AbstractACLGroupsCallback class can be used (all you need is to override acl_groups method):

from aiohttp import web
from aiohttp_auth import acl, auth
from aiohttp_auth.acl.abc import AbstractACLGroupsCallback
import aiohttp_session


class ACLGroupsCallback(AbstractACLGroupsCallback):
    def __init__(self, cache):
        # Save here data you need to retrieve groups
        # for example cache or db connection
        self.cache = cache

    async def acl_groups(self, user_id):
        # override abstract method with needed logic
        user = self.cache.get(user_id, None)
        ...
        groups = user.groups() if user else tuple()
        return groups


def init(loop):
    ...

    app = web.Application(loop=loop)
    # setup session middleware
    storage = aiohttp_session.EncryptedCookieStorage(urandom(32))
    aiohttp_session.setup(app, storage)

    # setup aiohttp_auth.auth middleware
    policy = auth.SessionTktAuthentication(urandom(32), 60, include_ip=True)
    auth.setup(app, policy)

    # setup aiohttp_auth.acl middleware
    cache = ...
    acl_groups_callback = ACLGroupsCallback(cache)
    acl.setup(app, acl_group_callback)

    ...

With the groups defined, an ACL context can be specified for looking up what permissions each group is allowed to access. A context is a sequence of ACL tuples which consist of a Allow/Deny action, a group, and a sequence of permissions for that ACL group. For example:

from aiohttp_auth.permissions import Group, Permission

context = [(Permission.Allow, Group.Everyone, ('view',)),
           (Permission.Allow, Group.AuthenticatedUser, ('view', 'view_extra')),
           (Permission.Allow, 'edit_group', ('view', 'view_extra', 'edit')),]

Views can then be defined using the acl_required decorator, allowing only specific users access to a particular view. The acl_required decorator specifies a permission required to access the view, and a context to check against:

@acl_required('view', context)
async def view_view(request):
    return web.Response(body='OK'.encode('utf-8'))

@acl_required('view_extra', context)
async def view_extra_view(request):
    return web.Response(body='OK'.encode('utf-8'))

@acl_required('edit', context)
async def edit_view(request):
    return web.Response(body='OK'.encode('utf-8'))

In our example, non-logged in users will have access to the view_view, ‘user’ will have access to both the view_view and view_extra_view, and ‘super_user’ will have access to all three views. If no ACL group of the user matches the ACL permission requested by the view, the decorator raises web.HTTPForbidden.

ACL tuple sequences are checked in order, with the first tuple that matches the group the user is a member of, AND includes the permission passed to the function, declared to be the matching ACL group. This means that if the ACL context was modified to:

context = [(Permission.Allow, Group.Everyone, ('view',)),
           (Permission.Deny, 'super_user', ('view_extra')),
           (Permission.Allow, Group.AuthenticatedUser, ('view', 'view_extra')),
           (Permission.Allow, 'edit_group', ('view', 'view_extra', 'edit')),]

In this example the ‘super_user’ would be denied access to the view_extra_view even though they are an AuthenticatedUser and in the ‘edit_group’.