Skip to content

Commit 6f0dfb1

Browse files
authored
Ensure username recovery endpoint is consistent with others. (#1201)
- Test for user not active - Test for user requiring confirmation - Support the confirmation_required redirect - Create utility to unify confirm_redirect code. closes #1200
1 parent 2b9b12e commit 6f0dfb1

10 files changed

Lines changed: 108 additions & 117 deletions

File tree

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ Features & Improvements
2121
Fixes
2222
+++++
2323
- (:issue:`1179`) Fix verify_password for bcrypt 5.0 (mephi42)
24+
- (:issue:`1200`) Fix username_recovery w.r.t. inactive and non-confirmed users
2425

2526
Docs and Chores
2627
+++++++++++++++
2728
- (:pr:`1150`) Update de_DE translations (swaeberle)
2829
- (:pr:`1151`) Update ca_ES translations (arielvb)
2930
- (:pr:`1152`) Update es_ES translations (arielvb)
3031
- (:pr:`1196`) Update arabic translations (samialfattani)
32+
- (:pr:`1199`) Update it_IT translations (gissimo)
3133

3234
Version 5.7.1
3335
-------------

docs/configuration.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1952,7 +1952,7 @@ Social Login (OAuth)
19521952

19531953
.. py:data:: SECURITY_OAUTH_ENABLE
19541954
1955-
To enable using external Oauth providers - set this to ``True``.
1955+
To enable using external OAuth providers - set this to ``True``.
19561956

19571957
.. py:data:: SECURITY_OAUTH_BUILTIN_PROVIDERS
19581958
@@ -1962,13 +1962,13 @@ Social Login (OAuth)
19621962

19631963
.. py:data:: SECURITY_OAUTH_START_URL
19641964
1965-
Endpoint for starting an Oauth authentication operation.
1965+
Endpoint for starting an OAuth authentication operation.
19661966

19671967
Default: ``"/login/oauthstart"``
19681968

19691969
.. py:data:: SECURITY_OAUTH_RESPONSE_URL
19701970
1971-
Endpoint used as Oauth redirect.
1971+
Endpoint used as OAuth redirect.
19721972

19731973
Default: ``"/login/oauthresponse"``
19741974

@@ -1985,15 +1985,15 @@ Social Login (OAuth)
19851985

19861986
.. py:data:: SECURITY_OAUTH_VERIFY_START_URL
19871987
1988-
Endpoint for starting an Oauth reauthentication/verify operation.
1988+
Endpoint for starting an OAuth reauthentication/verify operation.
19891989

19901990
Default: ``"/login/oauth-verify-start"``
19911991

19921992
.. versionadded:: 5.8.0
19931993

19941994
.. py:data:: SECURITY_OAUTH_VERIFY_RESPONSE_URL
19951995
1996-
Endpoint used as the Oauth verify redirect.
1996+
Endpoint used as the OAuth verify redirect.
19971997

19981998
Default: ``"/login/oauth-verify-response"``
19991999

flask_security/core.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,7 @@ def track_failed_authn(
11761176
11771177
The default implementation sends the user_failed_authn signal.
11781178
1179+
.. versionadded:: 5.8.0
11791180
"""
11801181
user_failed_authn.send(
11811182
current_app._get_current_object(), # type: ignore[attr-defined]

flask_security/forms.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1069,7 +1069,7 @@ class TwoFactorRescueForm(Form):
10691069
submit = SubmitField(get_form_field_label("submit"), id="rescue")
10701070

10711071

1072-
class UsernameRecoveryForm(Form, UserEmailFormMixin):
1072+
class UsernameRecoveryForm(ForgotPasswordForm):
10731073
"""The username recovery form"""
10741074

10751075
submit = SubmitField(get_form_field_label("recover_username"))

flask_security/unified_signin.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
base_render_json,
7373
check_and_get_token_status,
7474
config_value as cv,
75+
confirm_redirect,
7576
do_flash,
7677
get_identity_attributes,
7778
get_post_login_redirect,
@@ -486,13 +487,8 @@ def us_signin_send_code() -> ResponseValue:
486487
}
487488
return base_render_json(form, include_user=False, additional=payload)
488489

489-
if (
490-
form.requires_confirmation
491-
and cv("REQUIRES_CONFIRMATION_ERROR_VIEW")
492-
and not cv("RETURN_GENERIC_RESPONSES")
493-
):
494-
do_flash(*get_message("CONFIRMATION_REQUIRED"))
495-
return redirect(get_url(cv("REQUIRES_CONFIRMATION_ERROR_VIEW")))
490+
if rurl := confirm_redirect(form, "email"):
491+
return rurl
496492

497493
return _security.render_template(
498494
cv("US_SIGNIN_TEMPLATE"),
@@ -618,13 +614,8 @@ def us_signin() -> ResponseValue:
618614
# On error - wipe code
619615
form.passcode.data = None
620616

621-
if (
622-
form.requires_confirmation
623-
and cv("REQUIRES_CONFIRMATION_ERROR_VIEW")
624-
and not cv("RETURN_GENERIC_RESPONSES")
625-
):
626-
do_flash(*get_message("CONFIRMATION_REQUIRED"))
627-
return redirect(get_url(cv("REQUIRES_CONFIRMATION_ERROR_VIEW")))
617+
if rurl := confirm_redirect(form, "email"):
618+
return rurl
628619

629620
return _security.render_template(
630621
cv("US_SIGNIN_TEMPLATE"),

flask_security/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,25 @@ def suppress_form_csrf():
524524
return {}
525525

526526

527+
def confirm_redirect(form, identity_attribute):
528+
"""This is a very specific utility that all open endpoints call
529+
to implement the confirm redirect feature.
530+
"""
531+
if (
532+
form.requires_confirmation
533+
and config_value("REQUIRES_CONFIRMATION_ERROR_VIEW")
534+
and not config_value("RETURN_GENERIC_RESPONSES")
535+
):
536+
do_flash(*get_message("CONFIRMATION_REQUIRED"))
537+
return redirect(
538+
get_url(
539+
config_value("REQUIRES_CONFIRMATION_ERROR_VIEW"),
540+
qparams={identity_attribute: getattr(form.user, identity_attribute)},
541+
)
542+
)
543+
return None
544+
545+
527546
def do_flash(message: str, category: str) -> None:
528547
"""Flash a message depending on if the `FLASH_MESSAGES` configuration
529548
value is set.

flask_security/views.py

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Flask-Security views module
66
77
:copyright: (c) 2012 by Matt Wright.
8-
:copyright: (c) 2019-2025 by J. Christopher Wagner (jwag).
8+
:copyright: (c) 2019-2026 by J. Christopher Wagner (jwag).
99
:license: MIT, see LICENSE for more details.
1010
1111
CSRF is tricky. By default all our forms have CSRF protection built in via
@@ -105,6 +105,7 @@
105105
check_and_update_authn_fresh,
106106
check_and_get_token_status,
107107
config_value as cv,
108+
confirm_redirect,
108109
do_flash,
109110
get_identity_attributes,
110111
get_message,
@@ -217,21 +218,9 @@ def login() -> ResponseValue:
217218
}
218219
return base_render_json(form, additional=payload)
219220

220-
if (
221-
form.requires_confirmation
222-
and cv("REQUIRES_CONFIRMATION_ERROR_VIEW")
223-
and not cv("RETURN_GENERIC_RESPONSES")
224-
):
225-
# Validation failed BECAUSE user needs to confirm
226-
assert form.user_authenticated
227-
assert form.email.data # email_required validator
228-
do_flash(*get_message("CONFIRMATION_REQUIRED"))
229-
return redirect(
230-
get_url(
231-
cv("REQUIRES_CONFIRMATION_ERROR_VIEW"),
232-
qparams={"email": form.email.data},
233-
)
234-
)
221+
if rurl := confirm_redirect(form, "email"):
222+
return rurl
223+
235224
return _security.render_template(
236225
cv("LOGIN_USER_TEMPLATE"),
237226
login_user_form=form,
@@ -552,18 +541,8 @@ def forgot_password():
552541
# Never include user info since this is an anonymous endpoint.
553542
return base_render_json(form, include_user=False)
554543

555-
if (
556-
form.requires_confirmation
557-
and cv("REQUIRES_CONFIRMATION_ERROR_VIEW")
558-
and not cv("RETURN_GENERIC_RESPONSES")
559-
):
560-
do_flash(*get_message("CONFIRMATION_REQUIRED"))
561-
return redirect(
562-
get_url(
563-
cv("REQUIRES_CONFIRMATION_ERROR_VIEW"),
564-
qparams={"email": form.email.data},
565-
)
566-
)
544+
if rurl := confirm_redirect(form, "email"):
545+
return rurl
567546

568547
if is_user_authenticated(current_user):
569548
form.email.data = current_user.email
@@ -1198,6 +1177,9 @@ def recover_username():
11981177
if _security._want_json(request):
11991178
return base_render_json(form, include_user=False)
12001179

1180+
if rurl := confirm_redirect(form, "email"):
1181+
return rurl
1182+
12011183
return _security.render_template(
12021184
cv("USERNAME_RECOVERY_TEMPLATE"),
12031185
username_recovery_form=form,

0 commit comments

Comments
 (0)