aiohttp_auth 0.1.1
Authorization and authentication middleware plugin for aiohttp.
License: MIT

aiohttp_auth
============

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 two 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 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.


auth_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.HTTPForbidden()

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 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):
        # HTTPForbidden 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 encryptedd,
but only holds the username of the user and the cookies expiration time, along
with its security hash::

    def init(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))

        app = web.Application(loop=loop,
                              middlewares=[auth.auth_middleware(policy)])
        app = web.Application()
        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):

        # 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.SessionTktAuthentication(urandom(32), 60,
                                               include_ip=True))

        middlewares = [session_middleware(EncryptedCookieStorage(urandom(32))),
                       auth.auth_middleware(policy)]

        app = web.Application(loop=loop, middlewares=middlewares)

        ...


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_auth import acl

    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):
        ...

        middlewares = [session_middleware(EncryptedCookieStorage(urandom(32))),
                       auth.auth_middleware(policy),
                       acl.acl_middleware(acl_group_callback)]

        app = web.Application(loop=loop, middlewares=middlewares)
        ...


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 and user_id if the user_id
is not None.

With the groups defined, a 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 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.

License
-------

The library is licensed under a MIT license.