Skip to content

Commit

Permalink
Update 'confirm access' view to not depend on password
Browse files Browse the repository at this point in the history
Also update the 2FA flows to use 'sudo' mode

Update view backup_tokens url and urlname, remove password in it

The backup token uses elevate now, and would be possible to confirm access without password
  • Loading branch information
theskumar committed Oct 31, 2023
1 parent 939afec commit e1a5b74
Show file tree
Hide file tree
Showing 14 changed files with 340 additions and 41 deletions.
4 changes: 1 addition & 3 deletions hypha/apply/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@
# page and advances user to download backup code page.
path(
"account/two_factor/setup/complete/",
RedirectView.as_view(
url=reverse_lazy("users:backup_tokens_password"), permanent=False
),
RedirectView.as_view(url=reverse_lazy("users:backup_tokens"), permanent=False),
name="two_factor:setup_complete",
),
path("", include(tf_urls, "two_factor")),
Expand Down
21 changes: 11 additions & 10 deletions hypha/apply/users/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,21 +170,22 @@ def save(self, updated_email, name, slack, commit=True):


class TWOFAPasswordForm(forms.Form):
password = forms.CharField(
label=_("Please type your password to confirm"),
strip=False,
widget=forms.PasswordInput(attrs={"autofocus": True}),
confirmation_text = forms.CharField(
label=_("Please type 'disable' below before continuing:"),
strip=True,
# add widget with autofocus to avoid password autofill
widget=forms.TextInput(attrs={"autofocus": True, "autocomplete": "off"}),
)

def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user

def clean_password(self):
password = self.cleaned_data["password"]
if not self.user.check_password(password):
def clean_confirmation_text(self):
text = self.cleaned_data["confirmation_text"]
if text != "disable":
raise forms.ValidationError(
_("Incorrect password. Please try again."),
code="password_incorrect",
_("Incorrect input."),
code="confirmation_text_incorrect",
)
return password
return text
42 changes: 42 additions & 0 deletions hypha/apply/users/migrations/0022_confirmaccesstoken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 3.2.22 on 2023-10-31 06:59

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("users", "0021_pendingsignup"),
]

operations = [
migrations.CreateModel(
name="ConfirmAccessToken",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("token", models.CharField(max_length=6)),
("created", models.DateTimeField(auto_now_add=True)),
("modified", models.DateTimeField(auto_now=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name_plural": "Confirm Access Tokens",
"ordering": ("modified",),
},
),
]
24 changes: 23 additions & 1 deletion hypha/apply/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
STAFF_GROUP_NAME,
TEAMADMIN_GROUP_NAME,
)
from .utils import get_user_by_email, is_user_already_registered, send_activation_email
from .utils import (
get_user_by_email,
is_user_already_registered,
send_activation_email,
)


class UserQuerySet(models.QuerySet):
Expand Down Expand Up @@ -393,3 +397,21 @@ def __str__(self):
class Meta:
ordering = ("created",)
verbose_name_plural = "Pending signups"


class ConfirmAccessToken(models.Model):
"""
Once the user is created, the PendingSignup instance is deleted.
"""

token = models.CharField(max_length=6)
user = models.ForeignKey(User, on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)

def __str__(self):
return f"ConfirmAccessToken: {self.user.email} ({self.created})"

class Meta:
ordering = ("modified",)
verbose_name_plural = "Confirm Access Tokens"
75 changes: 57 additions & 18 deletions hypha/apply/users/templates/elevate/elevate.html
Original file line number Diff line number Diff line change
@@ -1,34 +1,73 @@
{% extends "base-apply.html" %}
{% load i18n wagtailcore_tags %}
{% load i18n wagtailcore_tags heroicons %}

{% block title %}{% trans "Confirm access" %}{% endblock %}
{% block body_class %}bg-white{% endblock %}

{% block content %}
<div class="max-w-lg px-4 pt-4 mx-auto md:mt-5 md:py-4">
<div class="max-w-md px-4 pt-4 mx-auto md:mt-5 md:py-4">

<form class="form" method="post" action="./" class="px-4 pt-4">
{% csrf_token %}
<h2 class="text-2xl">{% trans "Confirm access" %}</h2>
<h2 class="text-2xl text-center">{% trans "Confirm access" %}</h2>

<p class="px-3 py-2 bg-orange-100 rounded mb-4">
Signed in as <strong>{{ request.user }} ({{ request.user.email }})</strong>
</p>
<p class="text-center mb-4">
Signed in as <strong>{% if request.user.full_name %} {{ request.user.full_name }} ({{ request.user.email }}) {% else %}{{ request.user.email }} {% endif %}</strong>
</p>

<section id="section-form">

{% if request.user.has_usable_password %}
<form
class="form form--error-inline mb-4 px-4 pt-4 border rounded-sm bg-gray-50"
method="post"
action="./"
data-test-id="section-password-input"
id="form-password-input"
>
{% for field in form %}
{% include "forms/includes/field.html" %}
{% endfor %}

<div class="form__group">
<button class="button button--primary" type="submit">{% trans "Confirm" %}</button>
</div>
</form>
{% else %}
<section data-test-id="section-confirm" id="confirm-code-input" class="mb-4 px-4 pt-4 text-center">

<button
class="button button--primary"
type="submit"
hx-post="{% url 'users:elevate_send_confirm_access_email' %}{% if request.GET.next %}?next={{request.GET.next}}{% endif %}"
hx-swap="outerHTML"
hx-target="#confirm-code-input"
>
{% trans "Send a confirmation code to your email" %}
</button>
</section>
{% endif %}

{% if form.non_field_errors %}
<div class="wrapper wrapper--error">{{ form.non_field_errors.as_text }}</div>
{% if request.user.has_usable_password %}
<section data-test-id="section-send-email" class="px-4 border pt-2 pb-4">
<p>{% trans "Having problems?" %}</p>
<ul class="list-disc ml-4">
<li>
<a
class="m-0"
type="submit"
hx-post="{% url 'users:elevate_send_confirm_access_email' %}{% if request.GET.next %}?next={{request.GET.next}}{% endif %}"
hx-target="#section-form"
>
{% trans "Send a confirmation code to your email" %}
</a>
</li>
</ul>
</section>
{% endif %}

{% for field in form %}
{% include "forms/includes/field.html" %}
{% endfor %}
</section>

<div class="form__group">
<button class="button button--primary" type="submit">{% trans "Confirm" %}</button>
</div>
</form>

<p class="text-xs text-center max-w-sm mt-8 text-gray-500 mx-auto">
<p class="text-xs text-center max-w-xs mt-8 text-gray-500 mx-auto leading-relaxed">
{% blocktrans %}
<strong>Tip:</strong> You are entering sudo mode. After you've performed a sudo-protected
action, you'll only be asked to re-authenticate again after a few hours of inactivity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ <h1>{% block title %}{% trans "Two-Factor Authentication(2FA)" %}{% endblock %}<
To get the backup codes you can continue to Show Codes.{% endblocktrans %}</p>

{% if not phone_methods %}
<a href="{% url 'users:backup_tokens_password' %}" class="btn btn-link">{% trans "Show Codes" %}</a>
<a href="{% url 'users:backup_tokens' %}" class="btn btn-link">{% trans "Show Codes" %}</a>
{% else %}
<p>{% blocktrans trimmed %}However, it might happen that you don't have access to
your primary token device. To enable account recovery, add a phone
number.{% endblocktrans %}</p>

<p><a href="{% url 'users:backup_tokens_password' %}" class="btn btn-block">{% trans "Show Codes" %}</a></p>
<p><a href="{% url 'users:backup_tokens' %}" class="btn btn-block">{% trans "Show Codes" %}</a></p>
<p><a href="{% url 'two_factor:phone_create' %}"
class="btn btn-success">{% trans "Add Phone Number" %}</a></p>
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ <h2>{% trans "Backup Tokens" %}</h2>
You have {{ counter }} backup tokens remaining.
{% endblocktrans %}
</p>
<p><a href="{% url 'users:backup_tokens_password' %}"
<p><a href="{% url 'users:backup_tokens' %}"
class="btn btn-info">{% trans "Show Codes" %}</a></p>

<h2>{% trans "Disable Two-Factor Authentication" %}</h2>
Expand Down
2 changes: 1 addition & 1 deletion hypha/apply/users/templates/users/account.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ <h3 class="text-base mb-0">{% trans "Password" %}</h3>

<h3 class="text-base mb-2">{% trans "Two-Factor Authentication (2FA)" %}</h3>
{% if default_device %}
<a class="button button--primary mb-2" href="{% url 'users:backup_tokens_password' %}">{% trans "Backup codes" %}</a>
<a class="button button--primary mb-2" href="{% url 'users:backup_tokens' %}">{% trans "Backup codes" %}</a>
<a class="button button--primary button--warning mb-2" href="{% url 'two_factor:disable' %}">{% trans "Disable 2FA" %}</a>
{% else %}
<a class="button button--primary" href="{% url 'two_factor:setup' %}">{% trans "Enable 2FA" %}</a>
Expand Down
19 changes: 19 additions & 0 deletions hypha/apply/users/templates/users/emails/confirm_access.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% load i18n wagtailadmin_tags %}{% base_url_setting as base_url %}
{% blocktrans %}Dear {{ user }},{% endblocktrans %}

{% blocktrans %}To confirm access at {{ org_long_name }} use the code below (valid for {{ timeout_minutes }} minutes):{% endblocktrans %}

{{ token }}

{% blocktrans %}If you did not request this email, please ignore it.{% endblocktrans %}

{% if org_email %}
{% blocktrans %}If you have any questions, please contact us at {{ org_email }}.{% endblocktrans %}
{% endif %}

{% blocktrans %}Kind Regards,
The {{ org_short_name }} Team{% endblocktrans %}

--
{{ org_long_name }}
{% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{% load i18n heroicons %}
<form
class="form form--error-inline px-4 py-4 mb-4 border rounded-sm bg-gray-50 w-full text-center"
id="elevate-check-code-form"
x-data="{ code: '' }"
>
{% csrf_token %}
{% if error %}
<p class="mb-4 font-bold text-red-700">{% trans "Invalid code, please try again!" %}</p>
{% else %}
<p class="mb-4">
{% heroicon_mini "check-circle" class="inline align-text-bottom fill-green-700" aria_hidden=true %}
<em>{% trans "An email containing a code has been sent. Please check your email for the code." %}</em>
</p>
{% endif %}

<div class="mb-4">
<label class="font-bold mr-1" for="id_code">{% trans "Enter Code" %}: </label>
<input
name='code'
id="id_code"
autofocus
required
type='text'
maxlength='6'
class="mb-2 !w-28 placeholder:text-gray-400 text-center tracking-wider"
x-model="code"
autocomplete="off"
placeholder="_ _ _ _ _ _"
data-1p-ignore
>
</div>

<div>
<button
class="button button-primary block mb-4"
type="submit"
hx-post="{% url 'users:elevate_check_code' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}"
hx-validate="true"
hx-target="#section-form"
x-bind:disabled="code ? false : true"
>
{% trans "Confirm" %}
</button>
</div>
{% if error %}
<button
class="link hover:underline"
hx-post="{% url 'users:elevate_send_confirm_access_email' %}{% if request.GET.next %}?next={{request.GET.next}}{% endif %}"
hx-target="#section-form"
>
{% trans "Re-send code?" %}
</button>
{% endif %}
</form>

{% if request.user.has_usable_password %}
<section data-test-id="section-send-email" class="px-4 border pt-2 pb-4">
<p>{% trans "Having problems?" %}</p>
<ul class="list-disc ml-4">
<li>
<a
class="m-0"
type="submit"
hx-boost="true"
href="{% url 'users:elevate' %}{% if redirect_url %}?next={{ redirect_url }}{% endif %}"
>
{% trans "Use your password" %}
</a>
</li>
</ul>
</section>
{% endif %}

16 changes: 14 additions & 2 deletions hypha/apply/users/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
account_email_change,
become,
create_password,
elevate_check_code_view,
oauth,
send_confirm_access_email_view,
set_password_view,
)

Expand Down Expand Up @@ -122,9 +124,9 @@
# 2FA
path("two_factor/setup/", TWOFASetupView.as_view(), name="setup"),
path(
"two_factor/backup_tokens/password/",
"two_factor/backup_tokens/",
BackupTokensView.as_view(),
name="backup_tokens_password",
name="backup_tokens",
),
path("two_factor/disable/", TWOFADisableView.as_view(), name="disable"),
path(
Expand Down Expand Up @@ -153,6 +155,16 @@
{"template_name": "elevate/elevate.html"},
name="elevate",
),
path(
"sessions/send-confirm-access-email/",
send_confirm_access_email_view,
name="elevate_send_confirm_access_email",
),
path(
"sessions/verify-confirmation-code/",
elevate_check_code_view,
name="elevate_check_code",
),
]

urlpatterns = [path("account/", include(account_urls))]
Expand Down

0 comments on commit e1a5b74

Please sign in to comment.