Authentication & Sessions
=========================

This document gives a quick overview over the way that Fietsboek uses sessions
and authenticates users, going a bit into the technical details and reasoning
behind them. Fietsboek mostly uses `Pyramid's authentication facilities`_,
using hashed passwords in the database, and combining both the
``SessionAuthenticationHelper`` and the ``AuthTktCookieHelper``.

.. _Pyramid's authentication facilities: https://docs.pylonsproject.org/projects/pyramid/en/latest/api/authentication.html

Password Storage
----------------

Fietsboek stores passwords as a salted `scrypt`_ hash, using the implementation
in the `cryptography`_ library. The parameters Fietsboek uses are taken from
the ``cryptography`` documentation, which in turn takes them from `RFC 7914`_:

.. code:: python

    SCRYPT_PARAMETERS = {
        "length": 32,
        "n": 2**14,
        "r": 8,
        "p": 1,
    }

We note that the recommendation is from 2016, so a slightly increased value of
``n`` could be chosen to stay around the 100 ms target. The `Go documentation`_
mentions ``n = 2**15``. The reason why this is not updated yet is that it would
require logic to update password hashes when users log in, which has not been
implemented.

The randomness for the salt is taken from Python's built-in `secrets`_ module,
using high-quality random sources.

.. _scrypt: https://en.wikipedia.org/wiki/Scrypt
.. _cryptography: https://cryptography.io/en/latest/
.. _RFC 7914: https://datatracker.ietf.org/doc/html/rfc7914.html
.. _Go documentation: https://pkg.go.dev/golang.org/x/crypto/scrypt#Key
.. _secrets: https://docs.python.org/3/library/secrets.html

Cookies
-------

The authentication logic uses two cookies: one session cookie for normal
log-ins, and one long-lived cookie for the "Remember me" functionality. The
reason for the cookie split is that session cookies expire after a few minutes,
and by keeping a separate long-lived cookie, we can realize a "Remember me"
functionality without destroying the timeout for the remainder of the session
data.

The flow in the login handler therefore does two steps:

#. If the session contains a valid user data, the user is considered
   authenticated.
#. If not, the "Remember me"-cookie is checked. If it contains a valid user
   data, the session fingerprint is set to match, and the user is
   considered authenticated.

The user data in the cookie consists of two parts: the user ID in plain, which
is used to quickly retrieve the right user from the database, and the user
fingerprint, which is used to verify that the user is the correct one (see
below).

It is important to note that the session and the cookie are signed with the
application secret. Therefore, it is not possible to easily forge a valid
authentication cookie.

User Fingerprint
----------------

The cookie signature makes it hard to forge valid authentication tokens in the
first place. However, even with the signature mechanism in place, there are (at
least) two possible issues if we leave away the extra fingerprint:

Firstly, while no "new" authentication cookies can be forged, a legitimate
cookie that is saved might lead to a wrong user being retrieved. For example,
if someone saves their authentication cookie, then deletes their user account,
and the database re-uses the ID for the next new user, then the old
authentication cookie could be used to gain access to the new account.

Secondly, as sessions and cookies are client-side, we cannot influence them
from the server. Therefore, there would be no way to invalidate a specific
cookie, or all authentication cookies for a specific user (short of changing
the application secret and invalidating *all* cookies). To provide a "Log out
all sessions" functionality, we need a way to invalidate sessions remotely.

To solve those problems, we add a cryptographic hash (more precisely, a 10 byte
SHAKE_ hash) to the authentication cookie, which is formed over the following
data points:

* The user's email address
* The hash of the user's password
* A random session secret, specific to each user

This means that if any of those data points changes, the fingerprint will
change, and old authentication sessions and cookies will no longer be valid:

* If the user changes their email address, we consider this a relevant change,
  and require the user to login again.
* If a user changes their password, all sessions are also invalidated, and the
  user needs to login again.
* If we want to invalidate the sessions without setting a new password (e.g.
  because the user requests it), we simply set a new random session secret for
  the user.

We note that the fingerprint only offers 80 bits of security (which is rather
low), but the fingerprint is not the main line of defense against forged
logins: the main source of security for forged logins is the cookie signature.
The fingerprint only ensures that no "user mixups" can occur. So not only would
you need to brute-force the correct 80 bits of fingerprint, but for each
fingerprint you also need to generate a valid signature.

.. _SHAKE: https://en.wikipedia.org/wiki/SHA-3