From ff98e12e241db873f38acb8872ba2193aed2e8c3 Mon Sep 17 00:00:00 2001 From: Chris Wagner Date: Sat, 18 May 2024 14:25:44 -0700 Subject: [PATCH] Improve docs Made a sweep through all the docs and fixed/improved things: - added missing recoverY_codes API - added missing change_email signals - many openapi requests had a missing 'type: string' so didn't render - added sphinx extension autosectionlabel to make it easier to link - moved overview and search to top of doc pages (for some long pages like API - it was hard to find them at the bottom). --- CHANGES.rst | 2 +- docs/api.rst | 14 ++++++ docs/conf.py | 5 ++- docs/configuration.rst | 24 +++++------ docs/features.rst | 35 ++++++--------- docs/openapi.yaml | 74 +++++++++++++++++++++++--------- docs/quickstart.rst | 2 +- flask_security/core.py | 11 +++-- flask_security/models/fsqla.py | 2 +- flask_security/recovery_codes.py | 40 +++++++++++------ 10 files changed, 135 insertions(+), 74 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1ab9e304..61d4f862 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -968,7 +968,7 @@ Released March 31, 2020 Features ++++++++ -- (:pr:`257`) Support a unified sign in feature. Please see :ref:`unified-sign-in`. +- (:pr:`257`) Support a unified sign in feature. Please see :ref:`configuration:unified signin`. - (:pr:`265`) Add phone number validation class. This is used in both unified sign in as well as two-factor when using ``sms``. - (:pr:`274`) Add support for 'freshness' of caller's authentication. This permits endpoints diff --git a/docs/api.rst b/docs/api.rst index 787585ec..c0c1196c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -337,6 +337,20 @@ sends the following signals. Sent when a user requests a password reset. In addition to the app (which is the sender), it is passed `user`, `token` (deprecated), and `reset_token` arguments. +.. data:: change_email_instructions_sent + + Sent when a user requests to change their registered email address. In addition to the + app (which is the sender) it is passed `user`, `token`, and `new_email`. + + .. versionadded:: 5.5.0 + +.. data:: change_email_confirmed + + Sent when a user has confirmed their new email address. In addition to the + app (which is the sender) it is passed `user`, `old_email`. + + .. versionadded:: 5.5.0 + .. data:: tf_code_confirmed Sent when a user performs two-factor authentication login on the site. In diff --git a/docs/conf.py b/docs/conf.py index 25c9371c..4c20969a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -33,6 +33,7 @@ "pallets_sphinx_themes", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", + "sphinx.ext.autosectionlabel", "sphinx_issues", ] @@ -114,6 +115,8 @@ autodoc_type_aliases = { "CbType": "oauth_provider.CbType", } +autosectionlabel_prefix_document = True +autosectionlabel_maxdepth = 2 intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), @@ -175,7 +178,7 @@ # Custom sidebar templates, maps document names to template names. html_sidebars = { "index": ["project.html", "localtoc.html", "searchbox.html"], - "**": ["localtoc.html", "relations.html", "searchbox.html"], + "**": ["relations.html", "searchbox.html", "localtoc.html"], } singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]} diff --git a/docs/configuration.rst b/docs/configuration.rst index b7bb8f9e..ee7c20fd 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -85,7 +85,7 @@ These configuration keys are used globally across all features. .. py:data:: SECURITY_PASSWORD_SCHEMES - List of support password hash algorithms. ``SECURITY_PASSWORD_HASH`` + List of supported password hash algorithms. ``SECURITY_PASSWORD_HASH`` must be from this list. Passwords encrypted with any of these schemes will be honored. .. py:data:: SECURITY_DEPRECATED_PASSWORD_SCHEMES @@ -108,7 +108,7 @@ These configuration keys are used globally across all features. .. py:data:: SECURITY_PASSWORD_SINGLE_HASH A list of schemes that should not be hashed twice. By default, passwords are - hashed twice, first with ``SECURITY_PASSWORD_SALT``, and then with a random salt. + hashed twice, first with :py:data:`SECURITY_PASSWORD_SALT`, and then with a random salt. Default: a list of known schemes not working with double hashing (`django_{digest}`, `plaintext`). @@ -347,7 +347,7 @@ These configuration keys are used globally across all features. .. versionchanged:: 4.1.0 The 'key' attribute was deprecated in favor of a separate configuration - variable ``SECURITY_CSRF_COOKIE_NAME``. + variable :data:`SECURITY_CSRF_COOKIE_NAME`. .. py:data:: SECURITY_CSRF_HEADER @@ -805,7 +805,7 @@ Registerable Specifies the view to redirect to after a user successfully registers. This value can be set to a URL or an endpoint name. If this value is - ``None``, the user is redirected to the value of ``SECURITY_POST_LOGIN_VIEW``. + ``None``, the user is redirected to the value of :data:`SECURITY_POST_LOGIN_VIEW`. Note that if the request URL or form has a ``next`` parameter, that will take precedence. Default: ``None``. @@ -912,7 +912,7 @@ Confirmable Specifies the view to redirect to after a user successfully confirms their email. This value can be set to a URL or an endpoint name. If this value is ``None``, the user is redirected to the - value of ``SECURITY_POST_LOGIN_VIEW``. + value of :data:`SECURITY_POST_LOGIN_VIEW`. Default: ``None``. .. py:data:: SECURITY_AUTO_LOGIN_AFTER_CONFIRM @@ -927,7 +927,7 @@ Confirmable .. py:data:: SECURITY_LOGIN_WITHOUT_CONFIRMATION Specifies if a user may login before confirming their email when - the value of ``SECURITY_CONFIRMABLE`` is set to ``True``. + the value of :data:`SECURITY_CONFIRMABLE` is set to ``True``. Default: ``False``. .. py:data:: SECURITY_REQUIRES_CONFIRMATION_ERROR_VIEW @@ -957,7 +957,7 @@ Configuration variables for the ``SECURITY_CHANGEABLE`` feature: Specifies the view to redirect to after a user successfully changes their password. This value can be set to a URL or an endpoint name. If this value is ``None``, the user is redirected to the - value of ``SECURITY_POST_LOGIN_VIEW``. + value of :data:`SECURITY_POST_LOGIN_VIEW`. Default: ``None``. .. py:data:: SECURITY_CHANGE_PASSWORD_TEMPLATE @@ -1078,7 +1078,7 @@ Recoverable Default: ``_("Your password has been reset")``. -Change_Email +Change-Email ------------ .. versionadded:: 5.5.0 @@ -1154,7 +1154,7 @@ Configuration related to the two-factor authentication feature. Specifies if Flask-Security should enable the two-factor login feature. If set to ``True``, in addition to their passwords, users will be required to enter a code that is sent to them. Note that unless - ``SECURITY_TWO_FACTOR_REQUIRED`` is set - this is opt-in. + :data:`SECURITY_TWO_FACTOR_REQUIRED` is set - this is opt-in. Default: ``False``. .. py:data:: SECURITY_TWO_FACTOR_REQUIRED @@ -1404,8 +1404,8 @@ Unified Signin .. py:data:: SECURITY_US_ENABLED_METHODS Specifies the default enabled methods for unified signin authentication. - Be aware that ``password`` only affects this ``SECURITY_US_SIGNIN_URL`` endpoint. - Removing it from here won't stop users from using the ``SECURITY_LOGIN_URL`` endpoint + Be aware that ``password`` only affects this :data:`SECURITY_US_SIGNIN_URL` endpoint. + Removing it from here won't stop users from using the :data:`SECURITY_LOGIN_URL` endpoint (unless you replace the login endpoint using :py:data:`SECURITY_US_SIGNIN_REPLACES_LOGIN`). This config variable defines which methods can be used to provide authentication data. @@ -1654,7 +1654,7 @@ WebAuthn - ``"secondary"`` - just keys registered as "secondary" are allowed If list is empty or ``None`` WebAuthn keys aren't allowed. This also means that the - :py:data:``SECURITY_WAN_VERIFY`` endpoint won't be registered. + :py:data:`SECURITY_WAN_VERIFY_URL` endpoint won't be registered. Default: ``["first", "secondary"]`` diff --git a/docs/features.rst b/docs/features.rst index 3b07eea5..aaf478f6 100644 --- a/docs/features.rst +++ b/docs/features.rst @@ -99,12 +99,10 @@ expiry value (settable via the :data:`SECURITY_TOKEN_EXPIRE_TIMESTAMP` callable) there are some endpoints that require session information (e.g. a session cookie). Please read :ref:`freshness_topic` and :ref:`csrf_topic` -.. _two-factor: - Two-factor Authentication ---------------------------------------- - -Two-factor authentication is enabled by generating time-based one time passwords +If :ref:`configured`, +the two-factor authentication feature generates time-based one time passwords (Tokens). The tokens are generated using the users `totp secret`_, which is unique per user, and is generated both on first login, and when changing the two-factor method (doing this causes the previous totp secret to become invalid). The token @@ -121,13 +119,12 @@ they lose track of their secondary factor device. Rescue options include sending a one time code via email, send an email to the application admin, and using a previously generated and downloaded one-time code (see :py:data:`SECURITY_MULTI_FACTOR_RECOVERY_CODES`). -.. _unified-sign-in: - Unified Sign In --------------- **This feature is in Beta - mostly due to it being brand new and little to no production soak time** -Unified sign in provides a generalized login endpoint that takes an `identity` +If :ref:`configured`, +a generalized login endpoint is provided that takes an `identity` and a `passcode`; where (based on configuration): * `identity` is any of :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES` (e.g. email, username, phone) @@ -172,20 +169,18 @@ If you disable the freshness check then sessions aren't required. * Registration and Confirmation only work with email - so while you can enable multiple authentication methods, you still have to register with email. -.. _webauthn: - WebAuthn ---------------- +-------- **This feature is in Beta - mostly due to it being brand new and little to no production soak time** WebAuthn is a standardized protocol that connects authenticators (such as YubiKey and mobile biometrics) -with websites. Flask-Security supports using WebAuthn keys as either 'first' or 'secondary' +with websites. If :ref:`configured`, Flask-Security supports using WebAuthn keys as either 'first' or 'secondary' authenticators. Please read :ref:`webauthn_topic` for more details. Email Confirmation ------------------ - -If desired you can require that new users confirm their email address. +If :ref:`configured`, your application +can require that new users confirm their email address prior to allowing them to authenticate. Flask-Security will send an email message to any new users with a confirmation link. Upon navigating to the confirmation link, the user's account will be set to 'confirmed'. The user can then sign in usually the normal mechanisms. @@ -194,11 +189,10 @@ if the user happens to try to use an expired token or has lost the previous email. Confirmation links can be configured to expire after a specified amount of time (default 5 days). - Password Reset/Recovery ----------------------- - -Password reset and recovery is available for when a user forgets their +If :ref:`configured`, +password reset and recovery is available for when a user forgets their password. Flask-Security sends an email to the user with a link to a view which allows them to reset their password. Once the password is reset they are redirected to the login page where they need to authenticate using the new password. @@ -210,8 +204,7 @@ which will invalidate all existing sessions AND (by default) all authentication User Registration ----------------- - -Flask-Security comes packaged with a basic user registration view. This view is +If :ref:`configured`, Flask-Security provides a basic user registration view. This view is very simple and new users need only supply an email address and their password. This view can be overridden if your registration process requires more fields. User email is validated and normalized using the @@ -224,7 +217,7 @@ able to authenticate with EITHER email or username - however that can be changed Password Change --------------- -Flask-Security comes packaged with a basic change user password view. Unlike password +If :ref:`configured` users can change their password. Unlike password recovery, this endpoint is used when the user is already authenticated. The result of a successful password change is not only a new password, but a new value for ``fs_uniquifier``. This has the effect is immediately invalidating all existing sessions. The change request @@ -238,13 +231,13 @@ when generating authentication tokens and so won't be affected by password chang Email Change ------------ -If configured, users can change the email they registered with. This will send a new confirmation email to the new email address. +If :ref:`configured`, users can change the email they registered with. This will send a new confirmation email to the new email address. Login Tracking -------------- -Flask-Security can, if configured, keep track of basic login events and +Flask-Security can, if :ref:`configured`, keep track of basic login events and statistics. They include: * Last login date diff --git a/docs/openapi.yaml b/docs/openapi.yaml index abcf8023..7b524496 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -29,12 +29,16 @@ paths: 200: description: > Login form or user information. The JSON response will always - carry the csrf_token information. If the caller is logged in, then + carry the csrf_token information. + If SECURITY_CSRF_COOKIE_NAME is set then a cookie with the csrf token will be set. + If the caller is logged in, then additional information is returned. This can be very useful for single-page applications where during a force refresh, all state is lost. By performing this GET, the session cookie will authenticate the user and the response will contain user information. content: text/html: schema: + type: string + description: render_template(SECURITY_LOGIN_USER_TEMPLATE) example: render_template(SECURITY_LOGIN_USER_TEMPLATE) application/json: schema: @@ -222,7 +226,7 @@ paths: description: Http status code /verify: get: - summary: GET Basic re-authentication form + summary: GET verify/re-authentication form description: > If an endpoint is protected with @auth_required() with a freshness declaration this endpoint will be called to request an already signed in user to re-authenticate. @@ -232,6 +236,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_VERIFY_TEMPLATE) example: render_template(SECURITY_VERIFY_TEMPLATE) application/json: schema: @@ -243,7 +249,7 @@ paths: True if caller has a registered WebAuthn Key which has a `usage` that is allowed by the SECURITY_WAN_ALLOW_AS_VERIFY configuration setting. post: - summary: Basic re-authentication + summary: Verify/re-authentication parameters: - $ref: "#/components/parameters/include_auth_token" requestBody: @@ -293,6 +299,7 @@ paths: text/html: schema: type: string + description: render_template(SECURITY_REGISTER_USER_TEMPLATE) example: render_template(SECURITY_REGISTER_USER_TEMPLATE) 302: description: Response when already logged in @@ -355,6 +362,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_CHANGE_PASSWORD_TEMPLATE) example: render_template(SECURITY_CHANGE_PASSWORD_TEMPLATE) application/json: schema: @@ -520,6 +529,7 @@ paths: text/html: schema: type: string + description: render_template(SECURITY_FORGOT_PASSWORD_TEMPLATE) example: render_template(SECURITY_FORGOT_PASSWORD_TEMPLATE) 302: description: Response when already logged in @@ -644,6 +654,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_SEND_CONFIRMATION_TEMPLATE) example: render_template(SECURITY_SEND_CONFIRMATION_TEMPLATE) post: summary: Send confirmation email @@ -726,6 +738,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_US_SIGNIN_TEMPLATE) example: render_template(SECURITY_US_SIGNIN_TEMPLATE) application/json: schema: @@ -767,8 +781,8 @@ paths: - $ref: "#/components/schemas/LoginJsonResponse" text/html: schema: - description: Unsuccessful sign in type: string + description: Unsuccessful sign in - render_template(SECURITY_US_SIGNIN_TEMPLATE) with error values example: render_template(SECURITY_US_SIGNIN_TEMPLATE) with error values 302: description: > @@ -807,7 +821,7 @@ paths: content: application/json: schema: - description: Code successfully sent + $ref: "#/components/schemas/DefaultJsonResponse" text/html: schema: description: Validation error, code send error, or code successfully sent @@ -828,7 +842,7 @@ paths: /us-verify: get: - summary: GET Unified sign in re-authentication form/information + summary: GET Unified sign in verify/re-authentication form/information description: > If an endpoint is protected with @auth_required() with a freshness declaration this endpoint will be called to request an already signed in user to re-authenticate. @@ -838,6 +852,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_US_VERIFY_TEMPLATE) example: render_template(SECURITY_US_VERIFY_TEMPLATE) application/json: schema: @@ -861,7 +877,7 @@ paths: True if caller has a registered WebAuthn Key which has a `usage` that is allowed by the SECURITY_WAN_ALLOW_AS_VERIFY configuration setting. post: - summary: Unified sign in re-authentication + summary: Unified sign in verify/re-authentication parameters: - $ref: "#/components/parameters/include_auth_token" requestBody: @@ -885,8 +901,8 @@ paths: - $ref: "#/components/schemas/JsonResponseWithToken" text/html: schema: - description: Unsuccessful re-authentication. type: string + description: Unsuccessful re-authentication - render_template(SECURITY_US_VERIFY_TEMPLATE) with error values example: render_template(SECURITY_US_VERIFY_TEMPLATE) with error values 302: description: User successfully re-authenticated when using form based request. @@ -919,7 +935,7 @@ paths: content: application/json: schema: - description: Code successfully sent + $ref: "#/components/schemas/DefaultJsonResponse" text/html: schema: description: Validation error, code send error, or code successfully sent @@ -946,6 +962,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_US_SETUP_TEMPLATE) example: render_template(SECURITY_US_SETUP_TEMPLATE) application/json: schema: @@ -1032,6 +1050,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_US_SETUP_TEMPLATE) example: render_template(SECURITY_US_SETUP_TEMPLATE) post: @@ -1120,6 +1140,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_TWO_FACTOR_SETUP_TEMPLATE) example: render_template(SECURITY_TWO_FACTOR_SETUP_TEMPLATE) application/json: schema: @@ -1340,7 +1362,7 @@ paths: content: text/html: schema: - description: Select Form (SECURITY_TWO_FACTOR_SELECT_TEMPLATE) + description: render_template(SECURITY_TWO_FACTOR_SELECT_TEMPLATE) type: string example: render_template(SECURITY_TWO_FACTOR_SELECT_TEMPLATE) application/json: @@ -1405,18 +1427,27 @@ paths: schema: $ref: "#/components/schemas/DefaultJsonErrorResponse" /mf-recovery-codes: + summary: Generate and retrieve one-time use recovery codes. get: - summary: Generate and retrieve one-time use recovery codes. + summary: Retrieve one-time use recovery codes. description: > If a user has two-factor authentication enabled, they can generate and use a recovery code if they lose or otherwise can't use their second factor device. + parameters: + - name: show_codes + in: path + required: false + schema: + type: string responses: 200: description: Multi-factor recovery code form content: text/html: schema: + type: string + description: render_template(SECURITY_MULTI_FACTOR_RECOVERY_CODES_TEMPLATE) example: render_template(SECURITY_MULTI_FACTOR_RECOVERY_CODES_TEMPLATE) application/json: schema: @@ -1435,13 +1466,6 @@ paths: post: summary: Generate a new set of one-time recovery codes - requestBody: - content: - application/x-www-form-urlencoded: - schema: - description: Generate Codes Form - type: string - example: render_template(SECURITY_MULTI_FACTOR_RECOVERY_CODES_TEMPLATE) responses: 200: description: New one-time codes generated. @@ -1462,7 +1486,7 @@ paths: type: string text/html: schema: - description: New codes generated. + description: New codes generated - render_template(SECURITY_MULTI_FACTOR_RECOVERY_CODES_TEMPLATE) type: string example: render_template(SECURITY_MULTI_FACTOR_RECOVERY_CODES_TEMPLATE) 400: @@ -1485,6 +1509,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_MULTI_FACTOR_RECOVERY_TEMPLATE) example: render_template(SECURITY_MULTI_FACTOR_RECOVERY_TEMPLATE) application/json: schema: @@ -1537,6 +1563,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_WAN_REGISTER_TEMPLATE) example: render_template(SECURITY_WAN_REGISTER_TEMPLATE) application/json: schema: @@ -1584,7 +1612,7 @@ paths: $ref: "#/components/schemas/WanRegisterJsonResponse" text/html: schema: - description: Validation failed + description: Validation failed - render_template(SECURITY_WAN_REGISTER_TEMPLATE) type: string example: render_template(SECURITY_WAN_REGISTER_TEMPLATE) with error values 400: @@ -1650,6 +1678,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_WAN_SIGNIN_TEMPLATE) example: render_template(SECURITY_WAN_SIGNIN_TEMPLATE) application/json: schema: @@ -1753,7 +1783,7 @@ paths: $ref: "#/components/schemas/DefaultJsonErrorResponse" /wan-delete: get: - summary: GET Delete WebAuthn key form + summary: GET Delete WebAuthn key - this method isn't very useful. responses: 200: description: Delete an existing WebAuthn Key @@ -1814,6 +1844,8 @@ paths: content: text/html: schema: + type: string + description: render_template(SECURITY_WAN_VERIFY_TEMPLATE) example: render_template(SECURITY_WAN_VERIFY_TEMPLATE) application/json: schema: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index f1278ccb..706f5c83 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -13,7 +13,7 @@ There are some complete (but simple) examples available in the *examples* direct for some missing packages. .. note:: - The default ``SECURITY_PASSWORD_HASH`` is "bcrypt" - so be sure to install bcrypt. + The default :data:`SECURITY_PASSWORD_HASH` is "bcrypt" - so be sure to install bcrypt. If you opt for a different hash e.g. "argon2" you will need to install the appropriate package e.g. `argon_cffi`_. .. danger:: The examples below place secrets in source files. Never do this for your application diff --git a/flask_security/core.py b/flask_security/core.py index 7329659a..15567aea 100644 --- a/flask_security/core.py +++ b/flask_security/core.py @@ -1059,10 +1059,11 @@ class Security: :param register_blueprint: to register the Security blueprint or not. :param login_form: set form for the login view :param verify_form: set form for re-authentication due to freshness check + :param change_email_form: set form for changing email address :param register_form: set form for the register view when - *SECURITY_CONFIRMABLE* is false + :data:`SECURITY_CONFIRMABLE` is false :param confirm_register_form: set form for the register view when - *SECURITY_CONFIRMABLE* is true + :data:`SECURITY_CONFIRMABLE` is true :param forgot_password_form: set form for the forgot password view :param reset_password_form: set form for the reset password view :param change_password_form: set form for the change password view @@ -1111,7 +1112,8 @@ class Security: .. versionadded:: 3.4.0 ``us_signin_form``, ``us_setup_form``, ``us_setup_validate_form``, and - ``us_verify_form`` added as part of the :ref:`unified-sign-in` feature. + ``us_verify_form`` added as part of the :ref:`configuration:unified signin` + feature. .. versionadded:: 3.4.0 ``totp_cls`` added to enable applications to implement replay protection - see @@ -1139,6 +1141,9 @@ class Security: ``mf_recovery_form``. .. versionadded:: 5.1.0 ``mf_recovery_codes_util_cls``, ``oauth`` + .. versionadded:: 5.5.0 + ``change_email_form`` in support of the + :ref:`Change-Email` feature. .. deprecated:: 4.0.0 ``send_mail`` and ``send_mail_task``. Replaced with ``mail_util_cls``. diff --git a/flask_security/models/fsqla.py b/flask_security/models/fsqla.py index 0949b859..5a6e4839 100644 --- a/flask_security/models/fsqla.py +++ b/flask_security/models/fsqla.py @@ -56,7 +56,7 @@ def set_db_info( .. note:: This should only be used if you are utilizing the fsqla data models. With your own models you would need similar but slightly - difficult code. + different code. """ cls.db = appdb cls.user_table_name = user_table_name diff --git a/flask_security/recovery_codes.py b/flask_security/recovery_codes.py index 2ac689a5..4193b529 100644 --- a/flask_security/recovery_codes.py +++ b/flask_security/recovery_codes.py @@ -48,9 +48,15 @@ class MfRecoveryCodesUtil: """Handle creation, checking, encrypting and decrypting recovery codes. Since these are rarely used - keep them encrypted until needed - yes if someone gets access to memory they can find the key... + + .. versionadded:: 5.0.0 """ def __init__(self, app: flask.Flask): + """Initialize MfRecoveryCodesUtil. + If :data:`SECURITY_MULTI_FACTOR_RECOVERY_CODES_KEYS` is set then + initialize ``cryptor``. + """ self.cryptor: MultiFernet | None = None keys = cv("MULTI_FACTOR_RECOVERY_CODES_KEYS", app) # N.B. order is important - first key is 'primary'. @@ -66,36 +72,41 @@ def setup_cryptor(self, keys: list[bytes]) -> None: self.cryptor = MultiFernet(cryptors) def create_recovery_codes(self, user: User) -> list[str]: - # Create new recovery codes and store in user record. - # If configured codes are stored encrypted - but plainttext - # versions are returned. + """Create :data:`SECURITY_MULTI_FACTOR_RECOVERY_CODES_N` new recovery codes and + store in user record. + If configured (:data:`SECURITY_MULTI_FACTOR_RECOVERY_CODES_KEYS`), + codes are stored encrypted - but plainttext versions are returned. + """ new_codes = _security._totp_factory.generate_recovery_codes( cv("MULTI_FACTOR_RECOVERY_CODES_N") ) - _datastore.mf_set_recovery_codes(user, self.encrypt_codes(new_codes)) + _datastore.mf_set_recovery_codes(user, self._encrypt_codes(new_codes)) return new_codes def get_recovery_codes(self, user: User) -> list[str]: + """Return list of (unencrypted) recovery codes""" ecodes = _datastore.mf_get_recovery_codes(user) - return self.decrypt_codes(ecodes) + return self._decrypt_codes(ecodes) def check_recovery_code(self, user: User, code: str) -> bool: - # Verify code is valid + """Verify code is valid""" codes = _datastore.mf_get_recovery_codes(user) - dcodes = self.decrypt_codes(codes) + dcodes = self._decrypt_codes(codes) return code in dcodes def delete_recovery_code(self, user: User, code: str) -> bool: - # codes are single use - so delete after use. - # encrypting code gives different answer due to time stamp. - # we don't want to re-encrypt other codes. + """codes are single use - so delete after use. + encrypting code gives different answer due to time stamp. + we don't want to re-encrypt other codes. + """ codes = _datastore.mf_get_recovery_codes(user) if self.cryptor: - codes = self.decrypt_codes(codes) + codes = self._decrypt_codes(codes) idx = codes.index(code) return _datastore.mf_delete_recovery_code(user, idx) - def encrypt_codes(self, codes: list[str]) -> list[str]: + def _encrypt_codes(self, codes: list[str]) -> list[str]: + """Return list of encrypted codes""" if not self.cryptor: return codes ecodes = [] @@ -103,7 +114,10 @@ def encrypt_codes(self, codes: list[str]) -> list[str]: ecodes.append(self.cryptor.encrypt(code.encode()).decode()) return ecodes - def decrypt_codes(self, codes: list[str]) -> list[str]: + def _decrypt_codes(self, codes: list[str]) -> list[str]: + """Return list of decrypted codes. + Don't include in list if no longer valid. + """ from cryptography.fernet import InvalidToken if not self.cryptor: