Skip to content

Commit

Permalink
Add template of OTP verification page
Browse files Browse the repository at this point in the history
The presenter does not do anything.

Fixes: pypa#996
  • Loading branch information
Sparkycz committed Jul 29, 2018
1 parent a2b309c commit 657c865
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 4 deletions.
1 change: 1 addition & 0 deletions tests/unit/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def add_policy(name, filename):
domain=warehouse,
),
pretend.call("accounts.login", "/account/login/", domain=warehouse),
pretend.call("accounts.two-factor", "/account/two-factor/", domain=warehouse),
pretend.call("accounts.logout", "/account/logout/", domain=warehouse),
pretend.call("accounts.register", "/account/register/", domain=warehouse),
pretend.call(
Expand Down
11 changes: 11 additions & 0 deletions warehouse/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def validate_username(self, field):
raise wtforms.validators.ValidationError("No user found with that username")


class OtpCodeMixin:

otp_code = wtforms.StringField(validators=[wtforms.validators.DataRequired()])


class NewUsernameMixin:

username = wtforms.StringField(
Expand Down Expand Up @@ -166,6 +171,12 @@ def __init__(self, *args, user_service, **kwargs):
self.user_service = user_service


class TwoFactorForm(OtpCodeMixin, forms.Form):
def __init__(self, *args, user_service, **kwargs):
super().__init__(*args, **kwargs)
self.user_service = user_service


class RequestPasswordResetForm(forms.Form):
username_or_email = wtforms.StringField(
validators=[wtforms.validators.DataRequired()]
Expand Down
46 changes: 42 additions & 4 deletions warehouse/accounts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
RegistrationForm,
RequestPasswordResetForm,
ResetPasswordForm,
TwoFactorForm,
)
from warehouse.accounts.interfaces import (
IUserService,
Expand Down Expand Up @@ -163,6 +164,43 @@ def login(request, redirect_field_name=REDIRECT_FIELD_NAME, _form_class=LoginFor
}


@view_config(
route_name="accounts.two-factor",
renderer="accounts/two-factor.html",
uses_session=True,
require_csrf=True,
require_methods=False,
)
def two_factor(request, redirect_field_name=REDIRECT_FIELD_NAME, _form_class=TwoFactorForm):
if request.authenticated_userid is not None:
return HTTPSeeOther(request.route_path("manage.projects"))

user_service = request.find_service(IUserService, context=None)

redirect_to = request.POST.get(
redirect_field_name, request.GET.get(redirect_field_name)
)

form = _form_class(request.POST, user_service=user_service)
otp_code = form.otp_code.data

if request.method == "POST":
request.registry.datadog.increment(
"warehouse.authentication.two-factor.start", tags=["auth_method:two_factor_form"]
)
if form.validate():
pass # TODO: replace by call of OTP validation method (probably `user_service.validate_otp(otp_code)`)
else:
request.registry.datadog.increment(
"warehouse.authentication.two-factor.failure", tags=["auth_method:two_factor_form"]
)

return {
"form": form,
"redirect": {"field": REDIRECT_FIELD_NAME, "data": redirect_to},
}


@view_config(
route_name="accounts.logout",
renderer="accounts/logout.html",
Expand Down Expand Up @@ -387,8 +425,8 @@ def _error(message):
try:
email = (
request.db.query(Email)
.filter(Email.id == data["email.id"], Email.user == request.user)
.one()
.filter(Email.id == data["email.id"], Email.user == request.user)
.one()
)
except NoResultFound:
return _error("Email not found")
Expand Down Expand Up @@ -423,8 +461,8 @@ def _login_user(request, userid):
# that we create a new session (which will cause it to get a new
# session identifier).
if (
request.unauthenticated_userid is not None
and request.unauthenticated_userid != userid
request.unauthenticated_userid is not None
and request.unauthenticated_userid != userid
):
# There is already a userid associated with this request and it is
# a different userid than the one we're trying to remember now. In
Expand Down
1 change: 1 addition & 0 deletions warehouse/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def includeme(config):
domain=warehouse,
)
config.add_route("accounts.login", "/account/login/", domain=warehouse)
config.add_route("accounts.two-factor", "/account/two-factor/", domain=warehouse)
config.add_route("accounts.logout", "/account/logout/", domain=warehouse)
config.add_route("accounts.register", "/account/register/", domain=warehouse)
config.add_route(
Expand Down
67 changes: 67 additions & 0 deletions warehouse/templates/accounts/two-factor.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-#}
{% extends "base.html" %}

{% block title %}Log in{% endblock %}

{% block content %}
{% if testPyPI %}
{% set title = "TestPyPI" %}
{% else %}
{% set title = "PyPI" %}
{% endif %}

<section class="horizontal-section">
<div class="site-container">
<h1 class="page-title">Two-factor authentication</h1>

<form method="POST" action="{{ request.current_route_path() }}">
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">

{% if redirect.data %}
<input name="{{ redirect.field }}" type="hidden" value="{{ redirect.data }}">
{% endif %}

{% if form.errors.__all__ %}
<ul class="form-errors">
{% for error in form.errors.__all__ %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}

<div class="form-group">
<label for="otp_code" class="form-group__label">Authentication code</label>
{{ form.otp_code(placeholder="Authentication code", autocorrect="off", autocapitalize="off", spellcheck="false", required="required", class_="form-group__input", tabindex="1", autofocus=true) }}
{% if form.otp_code.errors %}
<ul class="form-errors">
{% for error in form.otp_code.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>

<div class="form-group">
<div class="split-layout split-layout--table">
<div>
<input type="submit" value="Verify" class="button button--primary" tabindex="3">
</div>
<span></span>
</div>
</div>
</form>
</div>
</section>
{% endblock %}

0 comments on commit 657c865

Please sign in to comment.