Skip to content

Commit

Permalink
feat(recoverable): Add support for username recovery via simple login…
Browse files Browse the repository at this point in the history
… flows (#1041)

* add support for username recovery

* default username recovery to false

* add support for json and generic responses

* fix json support and api docs

* fix docs and add context processor tests
  • Loading branch information
jamesejr authored Dec 30, 2024
1 parent 50758b2 commit 9950c54
Show file tree
Hide file tree
Showing 20 changed files with 436 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Version 5.6.0
Features & Improvements
+++++++++++++++++++++++
- (:issue:`1038`) Add support for 'secret_key' rotation
- (:issue:`980`) Add support for username recovery in simple login flows

Version 5.5.2
-------------
Expand Down
20 changes: 14 additions & 6 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ Forms
.. autoclass:: flask_security.ChangeEmailForm
.. autoclass:: flask_security.ChangePasswordForm
.. autoclass:: flask_security.ConfirmRegisterForm
.. autoclass:: flask_security.Form
.. autoclass:: flask_security.FormInfo
.. autoclass:: flask_security.ForgotPasswordForm
.. autoclass:: flask_security.LoginForm
.. autoclass:: flask_security.MfRecoveryCodesForm
Expand All @@ -239,23 +241,22 @@ Forms
.. autoclass:: flask_security.RegisterForm
.. autoclass:: flask_security.ResetPasswordForm
.. autoclass:: flask_security.SendConfirmationForm
.. autoclass:: flask_security.TwoFactorVerifyCodeForm
.. autoclass:: flask_security.TwoFactorSetupForm
.. autoclass:: flask_security.TwoFactorSelectForm
.. autoclass:: flask_security.TwoFactorRescueForm
.. autoclass:: flask_security.TwoFactorSelectForm
.. autoclass:: flask_security.TwoFactorSetupForm
.. autoclass:: flask_security.TwoFactorVerifyCodeForm
.. autoclass:: flask_security.UnifiedSigninForm
.. autoclass:: flask_security.UnifiedSigninSetupForm
.. autoclass:: flask_security.UnifiedSigninSetupValidateForm
.. autoclass:: flask_security.UnifiedVerifyForm
.. autoclass:: flask_security.UsernameRecoveryForm
.. autoclass:: flask_security.VerifyForm
.. autoclass:: flask_security.WebAuthnDeleteForm
.. autoclass:: flask_security.WebAuthnRegisterForm
.. autoclass:: flask_security.WebAuthnRegisterResponseForm
.. autoclass:: flask_security.WebAuthnSigninForm
.. autoclass:: flask_security.WebAuthnSigninResponseForm
.. autoclass:: flask_security.WebAuthnDeleteForm
.. autoclass:: flask_security.WebAuthnVerifyForm
.. autoclass:: flask_security.Form
.. autoclass:: flask_security.FormInfo

.. _signals_topic:

Expand Down Expand Up @@ -385,6 +386,13 @@ sends the following signals.

.. versionadded:: 3.3.0

.. data:: username_recovery_email_sent

Sent when a username is successfully recovered and sent over email. In addition to the
app (which is the sender), it is passed the `user` argument.

.. versionadded:: 5.6.0

.. data:: us_security_token_sent

Sent when a unified sign in access code is sent. In addition to the app
Expand Down
33 changes: 33 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,35 @@ Additional relevant configuration variables:
* :py:data:`SECURITY_FRESHNESS` - Used to protect /us-setup.
* :py:data:`SECURITY_FRESHNESS_GRACE_PERIOD` - Used to protect /us-setup.

Username-Recovery
-----------------

.. versionadded:: 5.6.0

.. py:data:: SECURITY_USERNAME_RECOVERY
Specifies whether username recovery is enabled.

Default: ``False``.

.. py:data:: SECURITY_USERNAME_RECOVERY_URL
Specifies the username recovery URL.

Default: ``"/recover-username"``.

.. py:data:: SECURITY_EMAIL_SUBJECT_USERNAME_RECOVERY
Sets subject for the username recovery email.

Default: ``_("Your requested username")``.

.. py:data:: SECURITY_USERNAME_RECOVERY_TEMPLATE
Specifies the path to the template for the username recovery page.

Default: ``"security/recover_username.html"``.

Passwordless
-------------

Expand Down Expand Up @@ -1865,6 +1894,7 @@ All feature flags. By default all are 'False'/not enabled.
* :py:data:`SECURITY_CHANGEABLE`
* :py:data:`SECURITY_TWO_FACTOR`
* :py:data:`SECURITY_UNIFIED_SIGNIN`
* :py:data:`SECURITY_USERNAME_RECOVERY`
* :py:data:`SECURITY_WEBAUTHN`
* :py:data:`SECURITY_MULTI_FACTOR_RECOVERY_CODES`
* :py:data:`SECURITY_OAUTH_ENABLE`
Expand Down Expand Up @@ -1904,6 +1934,7 @@ A list of all URLs and Views:
* :py:data:`SECURITY_RESET_VIEW`
* :py:data:`SECURITY_RESET_ERROR_VIEW`
* :py:data:`SECURITY_LOGIN_ERROR_VIEW`
* :py:data:`SECURITY_USERNAME_RECOVERY_URL`
* :py:data:`SECURITY_US_SIGNIN_URL`
* :py:data:`SECURITY_US_SETUP_URL`
* :py:data:`SECURITY_US_SIGNIN_SEND_CODE_URL`
Expand Down Expand Up @@ -1935,6 +1966,7 @@ A list of all templates:
* :py:data:`SECURITY_TWO_FACTOR_VERIFY_CODE_TEMPLATE`
* :py:data:`SECURITY_TWO_FACTOR_SELECT_TEMPLATE`
* :py:data:`SECURITY_TWO_FACTOR_SETUP_TEMPLATE`
* :py:data:`SECURITY_USERNAME_RECOVERY_TEMPLATE`
* :py:data:`SECURITY_US_SIGNIN_TEMPLATE`
* :py:data:`SECURITY_US_SETUP_TEMPLATE`
* :py:data:`SECURITY_US_VERIFY_TEMPLATE`
Expand Down Expand Up @@ -2025,6 +2057,7 @@ The default messages and error levels can be found in ``core.py``.
* ``SECURITY_MSG_USERNAME_DISALLOWED_CHARACTERS``
* ``SECURITY_MSG_USERNAME_NOT_PROVIDED``
* ``SECURITY_MSG_USERNAME_ALREADY_ASSOCIATED``
* ``SECURITY_MSG_USERNAME_RECOVERY_REQUEST``
* ``SECURITY_MSG_WEBAUTHN_EXPIRED``
* ``SECURITY_MSG_WEBAUTHN_NAME_REQUIRED``
* ``SECURITY_MSG_WEBAUTHN_NAME_INUSE``
Expand Down
5 changes: 5 additions & 0 deletions docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ following is a list of view templates:
* `security/login_user.html`
* `security/mf_recovery.html`
* `security/mf_recovery_codes.html`
* `security/recover_username.html`
* `security/register_user.html`
* `security/reset_password.html`
* `security/change_password.html`
Expand Down Expand Up @@ -178,6 +179,7 @@ The following is a list of all the available form overrides:
* ``us_setup_form``: Unified sign in setup form
* ``us_setup_validate_form``: Unified sign in setup validation form
* ``us_verify_form``: Unified sign in verify form
* ``username_recovery_form``: Username recovery form
* ``wan_delete_form``: WebAuthn delete a registered key form
* ``wan_register_form``: WebAuthn initiate registration ceremony form
* ``wan_register_response_form``: WebAuthn registration ceremony form
Expand Down Expand Up @@ -366,6 +368,8 @@ The following is a list of email templates:
* `security/email/confirmation_instructions.txt`
* `security/email/login_instructions.html`
* `security/email/login_instructions.txt`
* `security/email/username_recovery.html`
* `security/email/username_recovery.txt`
* `security/email/reset_instructions.html`
* `security/email/reset_instructions.txt`
* `security/email/reset_notice.html`
Expand Down Expand Up @@ -448,6 +452,7 @@ welcome_existing SECURITY_SEND_REGISTER_EMAIL SECURITY_EM
SECURITY_RETURN_GENERIC_RESPONSES - recovery_link
welcome_existing_username SECURITY_SEND_REGISTER_EMAIL SECURITY_EMAIL_SUBJECT_REGISTER - email user_not_registered
SECURITY_RETURN_GENERIC_RESPONSES - username
username_recovery SECURITY_USERNAME_RECOVERY SECURITY_EMAIL_SUBJECT_USERNAME_RECOVERY - user username_recovery_email_sent
============================= ================================== ============================================= ====================== ===============================

When sending an email, Flask-Security goes through the following steps:
Expand Down
52 changes: 52 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,50 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/DefaultJsonErrorResponse"
/recover-username:
get:
summary: GET username recovery form
responses:
200:
description: Username recovery form
content:
text/html:
schema:
type: string
description: render_template(SECURITY_USERNAME_RECOVERY_TEMPLATE)
example: render_template(SECURITY_USERNAME_RECOVERY_TEMPLATE)
application/json:
schema:
$ref: "#/components/schemas/DefaultJsonResponseNoUser"
post:
summary: Request username recovery
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RecoverUsername"
application/x-www-form-urlencoded:
schema:
$ref: "#/components/schemas/RecoverUsername"
responses:
200:
description: Send username recovery email
content:
text/html:
schema:
type: string
description: render_template(SECURITY_USERNAME_RECOVERY_TEMPLATE)
example: render_template(SECURITY_USERNAME_RECOVERY_TEMPLATE)
application/json:
schema:
$ref: "#/components/schemas/DefaultJsonResponseNoUser"
400:
description: Error when trying to send recovery email (e.g. user doesn't exist)
content:
application/json:
schema:
$ref: "#components/schemas/DefaultJsonErrorResponse"
/confirm:
get:
summary: GET send confirmation form
Expand Down Expand Up @@ -2231,6 +2275,14 @@ components:
type: string
description: >
Email address to send link email to.
RecoverUsername:
type: object
required: [email]
properties:
email:
type: string
description: >
Email address associated with the account.
UsSignin:
type: object
required: [identity, passcode]
Expand Down
10 changes: 6 additions & 4 deletions flask_security/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,20 +45,21 @@
unauth_csrf,
)
from .forms import (
Form,
ChangePasswordForm,
ConfirmRegisterForm,
Form,
ForgotPasswordForm,
LoginForm,
PasswordlessLoginForm,
RegisterForm,
ResetPasswordForm,
PasswordlessLoginForm,
ConfirmRegisterForm,
SendConfirmationForm,
TwoFactorRescueForm,
TwoFactorSetupForm,
TwoFactorVerifyCodeForm,
VerifyForm,
unique_identity_attribute,
UsernameRecoveryForm,
VerifyForm,
)
from .mail_util import MailUtil, EmailValidateException
from .oauth_glue import OAuthGlue
Expand Down Expand Up @@ -87,6 +88,7 @@
user_confirmed,
user_registered,
user_not_registered,
username_recovery_email_sent,
us_security_token_sent,
us_profile_changed,
wan_deleted,
Expand Down
20 changes: 20 additions & 0 deletions flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
TwoFactorVerifyCodeForm,
TwoFactorSetupForm,
TwoFactorRescueForm,
UsernameRecoveryForm,
VerifyForm,
get_register_username_field,
login_username_field,
Expand Down Expand Up @@ -289,6 +290,7 @@
"EMAIL_SUBJECT_PASSWORD_NOTICE": _("Your password has been reset"),
"EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE": _("Your password has been changed"),
"EMAIL_SUBJECT_PASSWORD_RESET": _("Password reset instructions"),
"EMAIL_SUBJECT_USERNAME_RECOVERY": _("Your requested username"),
"EMAIL_PLAINTEXT": True,
"EMAIL_HTML": True,
"EMAIL_SUBJECT_TWO_FACTOR": _("Two-factor Login"),
Expand Down Expand Up @@ -325,6 +327,9 @@
"webauthn": "flask_security.webauthn.WebAuthnTfPlugin",
},
"UNIFIED_SIGNIN": False,
"USERNAME_RECOVERY": False,
"USERNAME_RECOVERY_TEMPLATE": "security/recover_username.html",
"USERNAME_RECOVERY_URL": "/recover-username",
"US_SETUP_SALT": "us-setup-salt",
"US_SIGNIN_URL": "/us-signin",
"US_SIGNIN_SEND_CODE_URL": "/us-signin/send-code",
Expand Down Expand Up @@ -642,6 +647,10 @@
),
"success",
),
"USERNAME_RECOVERY_REQUEST": (
_("If registered, your username will be sent to your email."),
"info",
),
}


Expand Down Expand Up @@ -1151,6 +1160,7 @@ class Security:
:param two_factor_select_form: set form for selecting between active 2FA methods
:param mf_recovery_codes_form: set form for retrieving and setting recovery codes
:param mf_recovery_form: set form for multi factor recovery
:param username_recovery_form: set form for the username recovery view
:param us_signin_form: set form for the unified sign in view
:param us_setup_form: set form for the unified sign in setup view
:param us_setup_validate_form: set form for the unified sign in setup validate view
Expand Down Expand Up @@ -1220,6 +1230,8 @@ class Security:
.. versionadded:: 5.5.0
``change_email_form`` in support of the
:ref:`Change-Email<configuration:change-email>` feature.
.. versionadded:: 5.6.0
``username_recovery_form``
.. deprecated:: 4.0.0
``send_mail`` and ``send_mail_task``. Replaced with ``mail_util_cls``.
Expand Down Expand Up @@ -1278,6 +1290,7 @@ def __init__(
phone_util_cls: t.Type[PhoneUtil] = PhoneUtil,
render_template: t.Callable[..., str] = default_render_template,
totp_cls: t.Type[Totp] = Totp,
username_recovery_form: t.Type[UsernameRecoveryForm] = UsernameRecoveryForm,
username_util_cls: t.Type[UsernameUtil] = UsernameUtil,
webauthn_util_cls: t.Type[WebauthnUtil] = WebauthnUtil,
mf_recovery_codes_util_cls: t.Type[MfRecoveryCodesUtil] = MfRecoveryCodesUtil,
Expand Down Expand Up @@ -1325,6 +1338,7 @@ def __init__(
"two_factor_select_form": FormInfo(cls=two_factor_select_form),
"mf_recovery_codes_form": FormInfo(cls=mf_recovery_codes_form),
"mf_recovery_form": FormInfo(cls=mf_recovery_form),
"username_recovery_form": FormInfo(cls=username_recovery_form),
"us_signin_form": FormInfo(cls=us_signin_form),
"us_setup_form": FormInfo(cls=us_setup_form),
"us_setup_validate_form": FormInfo(cls=us_setup_validate_form),
Expand Down Expand Up @@ -1466,6 +1480,7 @@ def init_app(
"two_factor_select_form",
"mf_recovery_form",
"mf_recovery_codes_form",
"username_recovery_form",
"us_signin_form",
"us_setup_form",
"us_setup_validate_form",
Expand Down Expand Up @@ -2024,6 +2039,11 @@ def change_password_context_processor(
) -> None:
self._add_ctx_processor("change_password", fn)

def recover_username_context_processor(
self, fn: t.Callable[[], dict[str, t.Any]]
) -> None:
self._add_ctx_processor("recover_username", fn)

def send_confirmation_context_processor(
self, fn: t.Callable[[], dict[str, t.Any]]
) -> None:
Expand Down
Loading

0 comments on commit 9950c54

Please sign in to comment.