Skip to content

Commit

Permalink
Flesh out generic flow documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
fluffy-critter committed Jul 23, 2020
1 parent 1d42f68 commit 04863a9
Showing 1 changed file with 100 additions and 5 deletions.
105 changes: 100 additions & 5 deletions docs/flow.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,109 @@
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 :py:mod:`authl.flask` wrapper.
This is a brief, framework-agnostic overview of how to use Authl. If you want to
use Authl with `Flask`_, instead consider using the :py:mod:`authl.flask`
wrapper.

.. _Flask: https://flask.palletsprojects.com/

Typically you will simply use :py:func:`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 :py:class:`Authl` and :py:func:`Authl.add_handler`, as well as the documentation for :py:mod:`authl.handlers`.
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.

For the login flow, you need two parts: a login form, and a callback handler. The login form, at its simplest, should call :py:func:`Authl.get_handler_for_url` with the user's login URL to get the appropriate handler, and then call the handler's :py:func:`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 :py:class:`disposition.Disposition` object which should then direct the client in some way. Typically this will be either a :py:class:`disposition.Redirect` or a :py:class:`disposition.Notify`, but any of the disposition types are possible.
Typically you will simply use :py:func:`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 :py:class:`Authl` and
:py:func:`Authl.add_handler`, as well as the documentation for
:py:mod:`authl.handlers`.

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 :py:func:`handlers.Handler.check_callback` method. The resulting :py:class:`disposition.Disposition` object then indicates what comes next. Typically this will be either a :py:class:`disposition.Error` or a :py:class:`disposition.Verified`, but again, any disposition type is possible and must be handled accordingly.
For the login flow, you need two parts: a login form, and a callback handler.

Authl handlers also provide a few mechanisms that allow for an improved user experience; for example, :py:func:`authl.handlers.Handler.service_name` and :py:func:`authl.handlers.Handler.url_schemes` can be used to build out form elements that provide more information about which handlers are available, and :py:func:`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 :py:mod:`authl.flask` implementation and should absolutely be replicated in any other frontend implementation.
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
:py:func:`Authl.get_handler_for_url` with the user's login URL to get the
appropriate handler, and then call the handler's
:py:func:`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
:py:class:`disposition.Disposition` object which should then direct the client
in some way. Typically this will be either a :py:class:`disposition.Redirect` or
a :py:class:`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 :py:func:`handlers.Handler.check_callback` method. The
resulting :py:class:`disposition.Disposition` object then indicates what comes
next. Typically this will be either a :py:class:`disposition.Error` or a
:py:class:`disposition.Verified`, but again, any disposition type is possible
and must be handled accordingly.

Example (pseudo-)code follows:

.. code-block:: python
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, :py:func:`authl.handlers.Handler.service_name` and
:py:func:`authl.handlers.Handler.url_schemes` can be used to build out form
elements that provide more information about which handlers are available, and
:py:func:`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
:py:mod:`authl.flask` implementation and should absolutely be replicated in any
other frontend implementation.

See the `default Flask login template
<https://github.com/PlaidWeb/Authl/blob/main/authl/flask_templates/login.html>`_
for an example of how this might look.

0 comments on commit 04863a9

Please sign in to comment.