Skip to content

Latest commit

 

History

History
227 lines (170 loc) · 9.31 KB

README.rst

File metadata and controls

227 lines (170 loc) · 9.31 KB

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.