From bae6499475d0f9f735caebeda56755183ced5aeb Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 9 Oct 2023 22:38:00 +0200 Subject: add some docs about the auth cookie logic --- doc/developer/authentication.rst | 115 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 doc/developer/authentication.rst (limited to 'doc/developer') diff --git a/doc/developer/authentication.rst b/doc/developer/authentication.rst new file mode 100644 index 0000000..c52abeb --- /dev/null +++ b/doc/developer/authentication.rst @@ -0,0 +1,115 @@ +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 -- cgit v1.2.3