Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(recoverable): Add support for username recovery via simple login flows #1041

Merged
merged 7 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
jwag956 marked this conversation as resolved.
Show resolved Hide resolved
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:
jwag956 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading