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 authored and woodruffw committed Apr 26, 2019
1 parent 7abc62a commit 558f01d
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 2 deletions.
1 change: 1 addition & 0 deletions tests/unit/test_routes.py
Expand Up @@ -123,6 +123,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
Expand Up @@ -32,6 +32,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 @@ -219,6 +224,12 @@ def validate_password(self, field):
)


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
44 changes: 42 additions & 2 deletions warehouse/accounts/views.py
Expand Up @@ -30,6 +30,7 @@
RegistrationForm,
RequestPasswordResetForm,
ResetPasswordForm,
TwoFactorForm,
)
from warehouse.accounts.interfaces import (
IPasswordBreachedService,
Expand Down Expand Up @@ -156,6 +157,45 @@ 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)

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
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 @@ -429,8 +469,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
Expand Up @@ -98,6 +98,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
@@ -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 558f01d

Please sign in to comment.