Skip to content

Commit

Permalink
Cleanup us-signin and us-setup forms when just password is configured. (
Browse files Browse the repository at this point in the history
#287)

This allows unified signin to be a drop in replacement for standard login.

Hack - latest click doesn't work with py2.7....
  • Loading branch information
jwag956 committed Mar 14, 2020
1 parent 71a130d commit 1bd03e4
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 32 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ env:

before_install:
- "travis_retry pip install --upgrade pip setuptools"
- "travis_retry pip install twine wheel coveralls requirements-builder"
- "travis_retry pip install twine wheel coveralls click==7.0.0 requirements-builder"
- "requirements-builder -e all --level=min setup.py > .travis-lowest-requirements.txt"
- "requirements-builder -e all --level=pypi setup.py > .travis-release-requirements.txt"

Expand Down
3 changes: 0 additions & 3 deletions flask_security/templates/security/_menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ <h2>{{ _('Menu') }}</h2>
{% if security.unified_signin and not skip_login_menu %}
<li><a href="{{ url_for_security('us_signin') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _("Unified Sign In") }}</a><br/></li>
{% endif %}
{% if security.unified_signin %}
<li><a href="{{ url_for_security('us_setup') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _("Unified Setup") }}</a><br/></li>
{% endif %}
{% if security.registerable %}
<li><a href="{{ url_for_security('register') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _('Register') }}</a><br/></li>
{% endif %}
Expand Down
30 changes: 17 additions & 13 deletions flask_security/templates/security/us_setup.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,25 @@ <h1>{{ _("Setup Unified Sign In options") }}</h1>
<form action="{{ url_for_security("us_setup") }}" method="POST"
name="us_setup_form">
{{ us_setup_form.hidden_tag() }}
{{ render_field_with_errors(us_setup_form.new_totp_secret) }}
{% for subfield in us_setup_form.chosen_method %}
{% if subfield.data in methods %}
{{ render_field_with_errors(subfield) }}
{% if code_methods %}
{{ render_field_with_errors(us_setup_form.new_totp_secret) }}
{% for subfield in us_setup_form.chosen_method %}
{% if subfield.data in methods %}
{{ render_field_with_errors(subfield) }}
{% endif %}
{% endfor %}
{{ render_field_errors(us_setup_form.chosen_method) }}
{% if "sms" in methods %}
{{ render_field_with_errors(us_setup_form.phone) }}
{% endif %}
{% endfor %}
{{ render_field_errors(us_setup_form.chosen_method) }}
{% if "sms" in methods %}
{{ render_field_with_errors(us_setup_form.phone) }}
{% endif %}
{% if chosen_method == "authenticator" %}
<p>{{ _("Open your authenticator app on your device and scan the following qrcode to start receiving codes:") }}</p>
<p><img alt="{{ _("Passwordless QRCode") }}" id="qrcode" src="{{ url_for_security("us_qrcode", token=state) }}"></p>
{% if chosen_method == "authenticator" %}
<p>{{ _("Open your authenticator app on your device and scan the following qrcode to start receiving codes:") }}</p>
<p><img alt="{{ _("Passwordless QRCode") }}" id="qrcode" src="{{ url_for_security("us_qrcode", token=state) }}"></p>
{% endif %}
{{ render_field(us_setup_form.submit) }}
{% else %}
<h3>{{ _("No code methods have been enabled - nothing to setup") }}</h3>
{% endif %}
{{ render_field(us_setup_form.submit) }}
</form>
{% if state %}
<form action="{{ url_for_security("us_setup_verify", token=state) }}" method="POST"
Expand Down
4 changes: 2 additions & 2 deletions flask_security/templates/security/us_signin.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ <h1>{{ _("Sign In") }}</h1>
{{ render_field_with_errors(us_signin_form.passcode) }}
{{ render_field_with_errors(us_signin_form.remember) }}
{{ render_field(us_signin_form.submit) }}
{% if methods %}
{% if code_methods %}
<h4>{{ _("Request one-time code be sent") }}</h4>
{% for subfield in us_signin_form.chosen_method %}
{% if subfield.data in methods %}
{% if subfield.data in code_methods %}
{{ render_field_with_errors(subfield) }}
{% endif %}
{% endfor %}
Expand Down
4 changes: 2 additions & 2 deletions flask_security/templates/security/us_verify.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ <h1>{{ _("Please re-authenticate") }}</h1>
{{ us_verify_form.hidden_tag() }}
{{ render_field_with_errors(us_verify_form.passcode) }}
{{ render_field(us_verify_form.submit) }}
{% if methods %}
{% if code_methods %}
<h4>{{ _("Request one-time code be sent") }}</h4>
{% for subfield in us_verify_form.chosen_method %}
{% if subfield.data in methods %}
{% if subfield.data in code_methods %}
{{ render_field_with_errors(subfield) }}
{% endif %}
{% endfor %}
Expand Down
36 changes: 30 additions & 6 deletions flask_security/unified_signin.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,7 @@ class UnifiedSigninForm(_UnifiedPassCodeForm):

user = None

identity = StringField(
get_form_field_label("identity"),
validators=[Required()],
render_kw={"placeholder": _("email, phone, username")},
)
identity = StringField(get_form_field_label("identity"), validators=[Required()],)
remember = BooleanField(get_form_field_label("remember_me"))

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -348,6 +344,9 @@ def us_signin_send_code():
form = form_class(meta=suppress_form_csrf())
form.submit_send_code.data = True

code_methods = list(config_value("US_ENABLED_METHODS"))
if "password" in code_methods:
code_methods.remove("password")
if form.validate_on_submit():
code_sent, msg = _send_code_helper(form)
if _security._want_json(request):
Expand All @@ -360,6 +359,7 @@ def us_signin_send_code():
config_value("US_SIGNIN_TEMPLATE"),
us_signin_form=form,
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
chosen_method=form.chosen_method.data,
code_sent=code_sent,
skip_loginmenu=True,
Expand All @@ -375,6 +375,7 @@ def us_signin_send_code():
config_value("US_SIGNIN_TEMPLATE"),
us_signin_form=form,
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
skip_loginmenu=True,
**_security._run_ctx_processor("us_signin")
)
Expand All @@ -396,6 +397,9 @@ def us_verify_send_code():
form = form_class(meta=suppress_form_csrf())
form.submit_send_code.data = True

code_methods = list(config_value("US_ENABLED_METHODS"))
if "password" in code_methods:
code_methods.remove("password")
if form.validate_on_submit():
code_sent, msg = _send_code_helper(form)
if _security._want_json(request):
Expand All @@ -408,6 +412,7 @@ def us_verify_send_code():
config_value("US_VERIFY_TEMPLATE"),
us_verify_form=form,
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
chosen_method=form.chosen_method.data,
code_sent=code_sent,
skip_login_menu=True,
Expand All @@ -427,6 +432,7 @@ def us_verify_send_code():
config_value("US_VERIFY_TEMPLATE"),
us_verify_form=form,
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
skip_login_menu=True,
send_code_to=get_url(
_security.us_verify_send_code_url,
Expand Down Expand Up @@ -478,9 +484,13 @@ def us_signin():
return redirect(get_post_login_redirect())

# Here on GET or failed POST validate
code_methods = list(config_value("US_ENABLED_METHODS"))
if "password" in code_methods:
code_methods.remove("password")
if _security._want_json(request):
payload = {
"methods": config_value("US_ENABLED_METHODS"),
"code_methods": code_methods,
"identity_attributes": config_value("USER_IDENTITY_ATTRIBUTES"),
}
return base_render_json(form, include_user=False, additional=payload)
Expand All @@ -491,6 +501,7 @@ def us_signin():
config_value("US_SIGNIN_TEMPLATE"),
us_signin_form=form,
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
skip_login_menu=True,
**_security._run_ctx_processor("us_signin")
)
Expand All @@ -515,6 +526,10 @@ def us_verify():
form = form_class(meta=suppress_form_csrf())
form.submit.data = True

code_methods = list(config_value("US_ENABLED_METHODS"))
if "password" in code_methods:
code_methods.remove("password")

if form.validate_on_submit():
# verified - so set freshness time.
session["fs_paa"] = time.time()
Expand All @@ -529,6 +544,7 @@ def us_verify():
if _security._want_json(request):
payload = {
"methods": config_value("US_ENABLED_METHODS"),
"code_methods": code_methods,
}
return base_render_json(form, additional=payload)

Expand All @@ -537,7 +553,7 @@ def us_verify():
return _security.render_template(
config_value("US_VERIFY_TEMPLATE"),
us_verify_form=form,
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
skip_login_menu=True,
send_code_to=get_url(
_security.us_verify_send_code_url,
Expand Down Expand Up @@ -632,6 +648,10 @@ def us_setup():
else:
form = form_class(meta=suppress_form_csrf())

code_methods = list(config_value("US_ENABLED_METHODS"))
if "password" in code_methods:
code_methods.remove("password")

if form.validate_on_submit():
method = form.chosen_method.data
totp_secrets = current_user.us_get_totp_secrets()
Expand Down Expand Up @@ -662,6 +682,7 @@ def us_setup():
return _security.render_template(
config_value("US_SETUP_TEMPLATE"),
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
us_setup_form=form,
**_security._run_ctx_processor("us_setup")
)
Expand All @@ -674,6 +695,7 @@ def us_setup():
return _security.render_template(
config_value("US_SETUP_TEMPLATE"),
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
chosen_method=form.chosen_method.data,
us_setup_form=form,
us_setup_verify_form=_security.us_setup_verify_form(),
Expand All @@ -687,6 +709,7 @@ def us_setup():
payload = {
"identity_attributes": config_value("USER_IDENTITY_ATTRIBUTES"),
"methods": config_value("US_ENABLED_METHODS"),
"code_methods": code_methods,
"phone": current_user.us_phone_number,
}
return base_render_json(form, include_user=False, additional=payload)
Expand All @@ -696,6 +719,7 @@ def us_setup():
return _security.render_template(
config_value("US_SETUP_TEMPLATE"),
methods=config_value("US_ENABLED_METHODS"),
code_methods=code_methods,
us_setup_form=form,
**_security._run_ctx_processor("us_setup")
)
Expand Down
35 changes: 30 additions & 5 deletions tests/test_unified_signin.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,16 @@ def authned(myapp, user, **extra_args):
with capture_flashes() as flashes:

response = client_nc.get("/us-signin", headers=headers)
jresponse = response.json["response"]
assert jresponse["methods"] == app.config["SECURITY_US_ENABLED_METHODS"]
assert (
response.json["response"]["methods"]
== app.config["SECURITY_US_ENABLED_METHODS"]
)
assert (
response.json["response"]["identity_attributes"]
jresponse["identity_attributes"]
== app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"]
)
code_methods = list(app.config["SECURITY_US_ENABLED_METHODS"])
if "password" in code_methods:
code_methods.remove("password")
assert jresponse["code_methods"] == code_methods

with capture_send_code_requests() as requests:
with app.mail.record_messages() as outbox:
Expand Down Expand Up @@ -442,6 +444,7 @@ def pc(sender, user, method):
response = client_nc.get("/us-setup", headers=headers)
assert response.status_code == 200
assert response.json["response"]["methods"] == ["email", "sms"]
assert response.json["response"]["code_methods"] == ["email", "sms"]

sms_sender = SmsSenderFactory.createSender("test")
response = client_nc.post(
Expand Down Expand Up @@ -617,6 +620,16 @@ def test_verify_json(app, client, get_message):
"authenticator",
"sms",
]
assert response.json["response"]["code_methods"] == [
"email",
"authenticator",
"sms",
]

response = client.post(
"us-verify/send-code", json=dict(chosen_method="orb"), headers=headers,
)
assert response.status_code == 400

# Verify using SMS
sms_sender = SmsSenderFactory.createSender("test")
Expand Down Expand Up @@ -1089,3 +1102,15 @@ def us_send_security_token(self, method, **kwargs):
data = dict(identity="trp@lp.com", chosen_method="sms")
response = client.post("/us-signin/send-code", data=data, follow_redirects=True)
assert b"Code has been sent" in response.data


@pytest.mark.settings(us_enabled_methods=["password"])
def test_only_passwd(app, client, get_message):
authenticate(client)
response = client.get("us-setup")
assert b"No code method" in response.data

headers = {"Accept": "application/json", "Content-Type": "application/json"}
response = client.get("us-setup", headers=headers)
assert response.json["response"]["methods"] == ["password"]
assert not response.json["response"]["code_methods"]
2 changes: 2 additions & 0 deletions tests/view_scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def create_app():
app.config["LOGIN_DISABLED"] = False
app.config["WTF_CSRF_ENABLED"] = False
app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] = ["email", "us_phone_number"]
# app.config["SECURITY_US_ENABLED_METHODS"] = ["password"]

app.config["SECURITY_TOTP_SECRETS"] = {
"1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B"
}
Expand Down

0 comments on commit 1bd03e4

Please sign in to comment.