Authentication Flow

This is a brief, framework-agnostic overview of how to use Authl. If you want to use Authl with Flask, instead consider using the authl.flask wrapper.

Notably, the example code below is not written against any specific framework and is just to be used as a rough example of how it might look.

Typically you will simply use authl.from_config() to build an instance with your configured handlers. However, you can also instance it and your handlers directly. See the documentation for Authl and Authl.add_handler(), as well as the documentation for authl.handlers.

For the login flow, you need two parts: a login form, and a callback handler.

The login form should, at the very least, have a text input field for users to enter their identity URL, and should track the final post-login redirection target.

When the form is submitted, it calls Authl.get_handler_for_url() with the user’s login URL to get the appropriate handler, and then call the handler’s handlers.Handler.initiate_auth() function. The callback_uri argument needs to be able to map back to the handler in some way; typically you will include the handler’s cb_id in the URL, either as a query parameter or as a path component. get_handler_for_url will then return a disposition.Disposition object which should then direct the client in some way. Typically this will be either a disposition.Redirect or a disposition.Notify, but any of the disposition types are possible.

The callback then must look up the associated handler and pass the request URL, the parsed GET arguments (if any), and the parsed POST arguments (if any) to the handler’s handlers.Handler.check_callback() method. The resulting disposition.Disposition object then indicates what comes next. Typically this will be either a disposition.Error or a disposition.Verified, but again, any disposition type is possible and must be handled accordingly.

Example (pseudo-)code follows:

def handle_disposition(disp):
    if isinstance(disp, disposition.Redirect):
        return redirect(disp.url)
    if isinstance(disp, disposition.Verified):
        set_user_session(username=disp.identity)
        return redirect(disp.redir)
    if isinstance(disp, disposition.Notify):
        return render_notification_page(message=disp.cdata)
    if isinstance(disp, disposition.Error):
        return render_login_form(error=disp.message, redir=disp.redir)
    raise RuntimeError("Unknown disposition type " + disp)

def handle_login_form(request):
    # The login form should have some means of providing the post-login
    # redirection URL
    redir_url = get_redir_url(request)

    # Get the submitted user identity; it's a good idea to support both
    # GET and POST arguments for this to let people bookmark a quick
    # login URL if they so desire
    me_url = request.args.get('me', request.post.get('me'))
    if me_url:
        handler, hid, id_url = authl_instance.get_handler_for_url(me_url)
        if handler:
            # get_callback_url is implemented by the app, and produces a URL
            # that can map to a handler by handler ID
            cb_url = get_callback_url(hid)

            # handle_disposition is implemented by the app, and handles the
            # result of an authentication step
            return handle_disposition(
                handler.initiate_auth(id_url, cb_url, redir_url))

    return render_login_form(
        error="Unknown authentication method" if me_url else None,
        redir=redir_url)

def handle_callback(request):
    hid = get_hid_from_url(request.url)
    handler = authl_instance.get_handler_by_id(hid)
    if not handler:
        return render_login_page(error="Invalid callback")
    return handle_disposition(handler.check_callback(request.url,
                                                     request.args,
                                                     request.post))

Login form UX

Authl handlers also provide a few mechanisms that allow for an improved user experience; for example, authl.handlers.Handler.service_name() and authl.handlers.Handler.url_schemes() can be used to build out form elements that provide more information about which handlers are available, and authl.Authl.get_handler_for_url() can be used to implement an interactive “URL tester” to tell users in real-time whether the URL they’re entering is a valid identity. This functionality is all expressed in the authl.flask implementation and should absolutely be replicated in any other frontend implementation.

See the default Flask login template for an example of how this might look.

Asynchronous operation

Note that many of the underlying libraries that Authl uses are blocking, so as a result, Authl as a whole will be blocking for the foreseeable future. However, if you want to use Authl asynchronously, you can wrap the functions using asyncio.loop.run_in_executor() or using a higher-level library such as a_sync to manage this for you.

The functions you’ll specifically want to wrap are:

For example, an async version of the above flow might look like:

import asyncio

async def handle_login_form(request):
    loop = asyncio.get_running_loop()

    redir_url = get_redir_url(request)
    me_url = request.args.get('me', request.post.get('me'))
    if me_url:
        handler, hid, id_url = await loop.run_in_executor(
            None,
            authl_instance.get_handler_for_url, me_url)
        if handler:
            bc_url = get_callback_url(hid)
            return handle_disposition(await loop.run_in_executor(
                None, handler.initiate_auth,
                id_url, cb_url, redir_url))

    return render_login_form(redir=redir_url)

async def handle_callback(request):
    loop = asyncio.get_running_loop()

    hid = get_hid_from_url(request.url)
    handler = authl_instance.get_handler_by_id(hid)
    if not handler:
        return render_login_page(error="Invalid callback")

    return handle_disposition(await loop.run_in_executor(
        None, handler.check_callback,
        request.url, request.args, request.post))