Skip to content

Commit

Permalink
Small improvements. (pallets-eco#731)
Browse files Browse the repository at this point in the history
Recovery code form now shows 'no recovery codes setup' if user asks to see them and there aren't any.

two-factor-setup template shows existing, if any, already setup method.

In docs - remove 2FA is beta - its been around a while.

Yet a few more non-translated headers in templates.
  • Loading branch information
jwag956 authored Jan 21, 2023
1 parent 954a700 commit a216fb7
Show file tree
Hide file tree
Showing 12 changed files with 33 additions and 14 deletions.
1 change: 1 addition & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1777,6 +1777,7 @@ The default messages and error levels can be found in ``core.py``.
* ``SECURITY_MSG_LOGIN``
* ``SECURITY_MSG_LOGIN_EMAIL_SENT``
* ``SECURITY_MSG_LOGIN_EXPIRED``
* ``SECURITY_NO_RECOVERY_CODES_SETUP``
* ``SECURITY_MSG_OAUTH_HANDSHAKE_ERROR``
* ``SECURITY_MSG_PASSWORDLESS_LOGIN_SUCCESSFUL``
* ``SECURITY_MSG_PASSWORD_BREACHED``
Expand Down
7 changes: 4 additions & 3 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ at first use if null.

Two-factor Authentication
----------------------------------------
**This feature is in Beta - mostly due to it being brand new and little to no production soak time**

Two-factor authentication is enabled by generating time-based one time passwords
(Tokens). The tokens are generated using the users `totp secret`_, which is unique
Expand Down Expand Up @@ -259,6 +258,7 @@ JSON is supported for the following operations:
* Two-factor login requests
* Change two-factor method requests
* WebAuthn registration and signin requests
* Two-Factor recovery code requests

In addition, Single-Page-Applications (like those built with Vue, Angular, and
React) are supported via customizable redirect links.
Expand All @@ -273,9 +273,9 @@ registered. They can be completely disabled or their names can be changed.
Run ``flask --help`` and look for users and roles.


Social/Oauth authentication
Social/Oauth Authentication
----------------------------
Flask-Security provides a thin layer which integrates authlib with Flask-Security
Flask-Security provides a thin layer which integrates `authlib`_ with Flask-Security
views and features (such as two-factor authentication). Flask-Security is shipped
with support for github and google - others can be added by the application.

Expand All @@ -302,3 +302,4 @@ specified as environment variables.
.. _PyQRCode: https://pypi.python.org/pypi/PyQRCode/
.. _Wikipedia: https://en.wikipedia.org/wiki/Multi-factor_authentication
.. _Microsoft's: https://docs.microsoft.com/en-us/azure/active-directory/user-help/user-help-auth-app-overview
.. _authlib: https://authlib.org/
3 changes: 2 additions & 1 deletion flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@
"RETYPE_PASSWORD_MISMATCH": (_("Passwords do not match"), "error"),
"INVALID_REDIRECT": (_("Redirections outside the domain are forbidden"), "error"),
"INVALID_RECOVERY_CODE": (_("Recovery code invalid"), "error"),
"NO_RECOVERY_CODES_SETUP": (_("No recovery codes generated yet"), "info"),
"PASSWORD_RESET_REQUEST": (
_("Instructions to reset your password have been sent to %(email)s."),
"info",
Expand Down Expand Up @@ -625,7 +626,7 @@ class FormInfo:
See :py:meth:`flask_security.Security.set_form_info`
.. versionadded:: 5.x.x
.. versionadded:: 5.1.0
"""

instantiator: t.Callable[..., Form] = _default_form_instantiator
Expand Down
13 changes: 10 additions & 3 deletions flask_security/recovery_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Flask-Security Recovery Codes Module
:copyright: (c) 2022-2022 by J. Christopher Wagner (jwag).
:copyright: (c) 2022-2023 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
"""
import typing as t
Expand Down Expand Up @@ -121,6 +121,7 @@ def decrypt_codes(self, codes: t.List[str]) -> t.List[str]:
class MfRecoveryCodesForm(Form):
"""Generate and fetch recovery codes"""

# show_codes is a GET option., generate_new_codes is a POST option
show_codes = SubmitField(get_form_field_xlate(_("Show Recovery Codes")))
generate_new_codes = SubmitField(
get_form_field_xlate(_("Generate New Recovery Codes"))
Expand Down Expand Up @@ -172,7 +173,9 @@ def mf_recovery_codes() -> "ResponseValue":
For forms, we want the user to explicitly request to see the codes - so
the form has a show_codes submit button.
"""
form = build_form_from_request("mf_recovery_codes_form")
form = t.cast(
MfRecoveryCodesForm, build_form_from_request("mf_recovery_codes_form")
)

if form.validate_on_submit():
# generate new codes
Expand All @@ -193,10 +196,14 @@ def mf_recovery_codes() -> "ResponseValue":
return base_render_json(
form, include_user=False, additional=dict(recovery_codes=codes)
)
show_codes = request.args.get("show_codes", False)
if show_codes and not codes:
form.show_codes.errors = []
form.show_codes.errors.append(get_message("NO_RECOVERY_CODES_SETUP")[0])
return _security.render_template(
cv("MULTI_FACTOR_RECOVERY_CODES_TEMPLATE"),
mf_recovery_codes_form=form,
recovery_codes=codes if request.args.get("show_codes", None) else [],
recovery_codes=codes if show_codes else [],
**_security._run_ctx_processor("mf_recovery_codes"),
)

Expand Down
2 changes: 1 addition & 1 deletion flask_security/templates/security/change_password.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ <h1>{{ _fsdomain('Change password') }}</h1>
{% if active_password %}
{{ render_field_with_errors(change_password_form.password) }}
{% else %}
<h3>{{ _fsdomain('You do not currently have a password - this will add one.') }}</h3>
<h3>{{ _fsdomain('You do not currently have a password - this will add one.') }}</h3>
{% endif %}
{{ render_field_with_errors(change_password_form.new_password) }}
{{ render_field_with_errors(change_password_form.new_password_confirm) }}
Expand Down
4 changes: 2 additions & 2 deletions flask_security/templates/security/mf_recovery_codes.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ <h1>{{ _fsdomain("Recovery Codes") }}</h1>
<form action="{{ url_for_security('mf_recovery_codes') }}" method="GET"
name="mf_recovery_codes_form">

{{ render_field(mf_recovery_codes_form.show_codes) }}
{{ render_field_with_errors(mf_recovery_codes_form.show_codes) }}
</form>
{% endif %}
<hr class="fs-gap">
<h2>Generate new Recovery Codes</h2>
<h2>{{ _fsdomain("Generate new Recovery Codes") }}</h2>
<form action="{{ url_for_security('mf_recovery_codes') }}" method="POST"
name="mf_recovery_codes_form">
{{ mf_recovery_codes_form.hidden_tag() }}
Expand Down
3 changes: 3 additions & 0 deletions flask_security/templates/security/two_factor_setup.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
On GET:
choices: Value of SECURITY_TWO_FACTOR_ENABLED_METHODS (with possible addition of 'delete'
two_factor_required: Value of SECURITY_TWO_FACTOR_REQUIRED
primary_method: if a two-factor method has already been set up.
On successful POST:
chosen_method: which 2FA method was chosen (e.g. sms, authenticator)
choices: Value of SECURITY_TWO_FACTOR_ENABLED_METHODS
Expand All @@ -24,6 +25,8 @@ <h1>{{ _fsdomain("Two-factor authentication adds an extra layer of security to y
<h3>{{ _fsdomain("In addition to your username and password, you'll need to use a code.") }}</h3>
<form action="{{ url_for_security('two_factor_setup') }}" method="POST" name="two_factor_setup_form">
{{ two_factor_setup_form.hidden_tag() }}
<div class="fs-div">{{ _fsdomain("Currently setup two-factor method: %(method)s", method=primary_method) }}</div>
<div class="fs-gap"></div>
{% for subfield in two_factor_setup_form.setup %}
{% if subfield.data in choices %}
{{ render_field_with_errors(subfield) }}
Expand Down
2 changes: 1 addition & 1 deletion flask_security/templates/security/us_setup.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ <h1>{{ _fsdomain("Setup Unified Sign In") }}</h1>
{{ us_setup_form.hidden_tag() }}
{{ render_form_errors(us_setup_form) }}
{% if setup_methods %}
<div class="fs-div">Currently active sign in options:<em>
<div class="fs-div">{{ _fsdomain("Currently active sign in options:") }}<em>
{% if active_methods %}
{{ ", ".join(active_methods) }}
{% else %}
Expand Down
2 changes: 1 addition & 1 deletion flask_security/templates/security/verify.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ <h1>{{ _fsdomain("Please Reauthenticate") }}</h1>
</form>
{% if has_webauthn_verify_credential %}
<hr class="fs-gap">
<h2>Use a WebAuthn Security Key to Reauthenticate</h2>
<h2>{{ _fsdomain("Use a WebAuthn Security Key to Reauthenticate") }}</h2>
<form action="{{ url_for_security('wan_verify') }}{{ prop_next() }}" method="POST"
name="wan_verify_form">
{{ wan_verify_form.hidden_tag() }}
Expand Down
1 change: 1 addition & 0 deletions flask_security/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,7 @@ def two_factor_setup():
two_factor_verify_code_form=code_form,
choices=choices,
chosen_method=form.setup.data,
primary_method=getattr(user, "tf_primary_method", "None"),
two_factor_required=cv("TWO_FACTOR_REQUIRED"),
**_ctx("tf_setup"),
)
Expand Down
6 changes: 5 additions & 1 deletion tests/test_recovery_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
recovery code tests
:copyright: (c) 2022-2022 by J. Christopher Wagner (jwag).
:copyright: (c) 2022-2023 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
"""

Expand Down Expand Up @@ -82,6 +82,10 @@ def test_rc(app, client, get_message):
# gal has two-factor already setup for 'sms'
tf_authenticate(app, client)

response = client.get("/mf-recovery-codes?show_codes=hi")
assert response.status_code == 200
assert get_message("NO_RECOVERY_CODES_SETUP") in response.data

response = client.post("/mf-recovery-codes")
rd = response.data.decode("utf-8")
codes = re.findall(r"[a-f,\d]{4}-[a-f,\d]{4}-[a-f,\d]{4}", rd)
Expand Down
3 changes: 2 additions & 1 deletion tests/test_two_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
two_factor tests
:copyright: (c) 2019-2022 by J. Christopher Wagner (jwag).
:copyright: (c) 2019-2023 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
"""

Expand Down Expand Up @@ -722,6 +722,7 @@ def test_no_opt_out(app, client, get_message):

response = client.get("/tf-setup", follow_redirects=True)
assert b"Disable two factor" not in response.data
assert b"Currently setup two-factor method: sms" in response.data

# Try to opt-out
data = dict(setup="disable")
Expand Down

0 comments on commit a216fb7

Please sign in to comment.