diff --git a/docs/setup/administrators/configuration.md b/docs/setup/administrators/configuration.md index 2fb6f9f6a7..402df12422 100644 --- a/docs/setup/administrators/configuration.md +++ b/docs/setup/administrators/configuration.md @@ -48,12 +48,18 @@ The corresponding locale dir is named: en, en_GB, en_US ---- -Number of seconds that password reset and account activation links are valid (default 259200, 3 days). +Number of seconds that password reset links are valid (default 259200, 3 days). PASSWORD_RESET_TIMEOUT = env.int('PASSWORD_RESET_TIMEOUT', 259200) ---- +Number of seconds that account activation links are valid (default 900, 15 minutes). + + PASSWORD_ACTIVATION_TIMEOUT = env.int("PASSWORD_ACTIVATION_TIMEOUT", 900) + +---- + Seconds to enter password on password page while email change/2FA change (default 120). PASSWORD_PAGE_TIMEOUT = env.int('PASSWORD_PAGE_TIMEOUT', 120) diff --git a/hypha/apply/users/templates/two_factor/_base.html b/hypha/apply/users/templates/two_factor/_base.html index 76790f57a1..753e687428 100644 --- a/hypha/apply/users/templates/two_factor/_base.html +++ b/hypha/apply/users/templates/two_factor/_base.html @@ -28,6 +28,9 @@ {{ block.super }} {# Focus the 2FA field. #} {% endblock %} diff --git a/hypha/apply/users/templates/two_factor/core/backup_tokens.html b/hypha/apply/users/templates/two_factor/core/backup_tokens.html index ee2a181076..db847cd533 100644 --- a/hypha/apply/users/templates/two_factor/core/backup_tokens.html +++ b/hypha/apply/users/templates/two_factor/core/backup_tokens.html @@ -2,9 +2,9 @@ {% load static i18n users_tags heroicons %} {% block content_inner %} -

+

{% block title %}{% trans "Backup Codes" %}{% endblock %} -

+

{% blocktrans %}You should now print these codes or copy them to your diff --git a/hypha/apply/users/templates/two_factor/core/backup_tokens_password.html b/hypha/apply/users/templates/two_factor/core/backup_tokens_password.html deleted file mode 100644 index 10f1ef5265..0000000000 --- a/hypha/apply/users/templates/two_factor/core/backup_tokens_password.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "two_factor/_base.html" %} -{% load i18n %} - -{% block content %} -

{% block title %}{% trans "Backup Codes" %}{% endblock %}

-

{% blocktrans trimmed %}If you loose your smartphone, or your Authenticator app is not available, - you can use a backup code along with your username and password to login until you recover your smartphone. - Each backup code can be used only once. -
-
- These codes should be kept in a secure, private place (print them or store them in your password manager) - for when you need them. When they are used up, you can generate a new set of backup codes.{% endblocktrans %}

-
-
- {% if form.non_field_errors %} - - {% endif %} - - {% if form.errors %} - - {% endif %} - - {% csrf_token %} - - {% for field in form %} - {% include "forms/includes/field.html" %} - {% endfor %} - - -
-
-{% endblock %} diff --git a/hypha/apply/users/templates/two_factor/core/setup_complete.html b/hypha/apply/users/templates/two_factor/core/setup_complete.html index d0f29a33bc..916db99cd3 100644 --- a/hypha/apply/users/templates/two_factor/core/setup_complete.html +++ b/hypha/apply/users/templates/two_factor/core/setup_complete.html @@ -2,7 +2,6 @@ {% load i18n %} {% block content_inner %} -

{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor authentication.{% endblocktrans %}

@@ -31,5 +30,4 @@ {% endif %}
- {% endblock %} diff --git a/hypha/apply/users/templates/two_factor/core/two_factor_required.html b/hypha/apply/users/templates/two_factor/core/two_factor_required.html index a28e2d5c80..aad667e924 100644 --- a/hypha/apply/users/templates/two_factor/core/two_factor_required.html +++ b/hypha/apply/users/templates/two_factor/core/two_factor_required.html @@ -2,8 +2,8 @@ {% extends "two_factor/_base.html" %} {% load i18n %} -{% block content %} -

{% block title %}{% trans "Permission Denied" %}: {{ reason }}{% endblock %}

+{% block content_inner %} +

{% block title %}{% trans "Permission Denied" %}: {{ reason }}{% endblock %}

diff --git a/hypha/apply/users/templates/two_factor/profile/disable.html b/hypha/apply/users/templates/two_factor/profile/disable.html index c0086024b2..1053369a91 100644 --- a/hypha/apply/users/templates/two_factor/profile/disable.html +++ b/hypha/apply/users/templates/two_factor/profile/disable.html @@ -3,8 +3,8 @@ {% block content_inner %} -

{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}

-

{% blocktrans trimmed %}Disabling Two-factor authentication weakens your account security. +

{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}

+

{% blocktrans trimmed %}Disabling Two-factor authentication weakens your account security. We recommend reenabling it when you can.{% endblocktrans %}

diff --git a/hypha/apply/users/templates/users/activation/confirm.html b/hypha/apply/users/templates/users/activation/confirm.html new file mode 100644 index 0000000000..4e6a68e706 --- /dev/null +++ b/hypha/apply/users/templates/users/activation/confirm.html @@ -0,0 +1,50 @@ +{% extends "base-apply.html" %} +{% load i18n heroicons %} + +{% block title %}{% if is_signup or is_activation %}{% trans "Confirm account creation" %}{% else %}{% trans "Confirm login" %}{% endif %}{% endblock %} +{% block body_class %}bg-base-200{% endblock %} + +{% block content %} +
+
+
+ + {% heroicon_outline "key" aria_hidden="true" size=64 %} + + +

+ {% if is_signup or is_activation %} + {% trans "Complete your registration" %} + {% else %} + {% trans "Complete your login" %} + {% endif %} +

+ +
+ {% if is_signup or is_activation %} +

{% trans "Your activation link is valid. Click the button below to create your account." %}

+ {% else %} +

{% trans "Your login link is valid. Click the button below to complete your login." %}

+ {% endif %} +
+ + + {% csrf_token %} + {% if remember_me %} + + {% endif %} + {% if next %} + + {% endif %} + + +
+
+
+{% endblock %} diff --git a/hypha/apply/users/templates/users/activation/email.txt b/hypha/apply/users/templates/users/activation/email.txt index 180d5601f7..003cbd6e3a 100644 --- a/hypha/apply/users/templates/users/activation/email.txt +++ b/hypha/apply/users/templates/users/activation/email.txt @@ -5,14 +5,14 @@ {% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{{ activation_path }} -{% blocktrans %}This link can be used only once and will lead you to a page where you can set your password. It will remain active for {{ timeout_days }} days, so please set your password as soon as possible.{% endblocktrans %} +{% blocktrans %}This link can be used only once and will lead you to a page where you can set your password. It will remain active for {{ timeout_minutes }} minutes, so please set your password as soon as possible.{% endblocktrans %} {% trans "After setting your password, you will be able to log in at" %}: {% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %} {% trans "in the future using" %}: {% trans "Username" %}: {{ username }} {% trans "Password" %}: {% trans "Your chosen password" %} -{% blocktrans %}If you do not complete the activation process within {{ timeout_days }} days you can use the password reset form at{% endblocktrans %}: {% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{% url 'users:password_reset' %} +{% blocktrans %}If you do not complete the activation process within {{ timeout_minutes }} minutes you can use the password reset form at{% endblocktrans %}: {% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{% url 'users:password_reset' %} {% blocktrans %}Kind Regards, The {{ ORG_SHORT_NAME }} Team{% endblocktrans %} diff --git a/hypha/apply/users/templates/users/activation/invalid.html b/hypha/apply/users/templates/users/activation/invalid.html index 32333da7b2..c4e358acc0 100644 --- a/hypha/apply/users/templates/users/activation/invalid.html +++ b/hypha/apply/users/templates/users/activation/invalid.html @@ -1,7 +1,7 @@ {% extends "base-apply.html" %} {% load i18n heroicons %} -{% block title %}{% trans "Activation link issue" %}{% endblock %} +{% block title %}{% trans "One time link issue" %}{% endblock %} {% block body_class %}bg-base-200{% endblock %} {% block content %} @@ -12,12 +12,12 @@ {% heroicon_outline "exclamation-triangle" class="w-14 h-14 text-warning" %} -

{% trans "We couldn't activate your account" %}

+

{% trans "The one time link has expired" %}

{% url 'users:password_reset' as password_reset %} -

{% trans "This usually happens because your activation link has expired, or your account is already activated." %}

+

{% trans "This usually happens because your one time link has expired, or your account is already activated." %}

{% blocktrans %}Try resetting your password first. If you're still having trouble, contact us at {{ ORG_SHORT_NAME }}:{% endblocktrans %} diff --git a/hypha/apply/users/templates/users/email_change/confirm_email.txt b/hypha/apply/users/templates/users/email_change/confirm_email.txt index 3e5082ba59..e6ddd72a8a 100644 --- a/hypha/apply/users/templates/users/email_change/confirm_email.txt +++ b/hypha/apply/users/templates/users/email_change/confirm_email.txt @@ -5,7 +5,7 @@ {% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{{ activation_path }} -{% blocktrans %}This link will only remain active for {{ timeout_days }} days and will lead you to profile page after verification.{% endblocktrans %} +{% blocktrans %}This link will only remain active for {{ timeout_minutes }} minutes and will lead you to profile page after verification.{% endblocktrans %} {% blocktrans %}Kind Regards, The {{ ORG_SHORT_NAME }} Team{% endblocktrans %} diff --git a/hypha/apply/users/templates/users/emails/passwordless_login_email.md b/hypha/apply/users/templates/users/emails/passwordless_login_email.md index 3eb07cf3e2..37eae599a4 100644 --- a/hypha/apply/users/templates/users/emails/passwordless_login_email.md +++ b/hypha/apply/users/templates/users/emails/passwordless_login_email.md @@ -6,7 +6,7 @@ {% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{{ login_path }} -{% blocktrans %}This link will valid for {{ timeout_minutes }} minutes and can be used only once.{% endblocktrans %} +{% blocktrans %}This link will be valid for {{ timeout_minutes }} minutes and can be used only once.{% endblocktrans %} {% else %} {% blocktrans %}Your account on the {{ ORG_LONG_NAME }} web site is deactivated. Please contact site administrators.{% endblocktrans %} diff --git a/hypha/apply/users/templates/users/emails/passwordless_new_account_login.md b/hypha/apply/users/templates/users/emails/passwordless_new_account_login.md index 58fa694348..9058cac734 100644 --- a/hypha/apply/users/templates/users/emails/passwordless_new_account_login.md +++ b/hypha/apply/users/templates/users/emails/passwordless_new_account_login.md @@ -5,7 +5,7 @@ {% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{{ signup_path }} -{% blocktrans %}This link will valid for {{ timeout_minutes }} minutes and can be used only once.{% endblocktrans %} +{% blocktrans %}This link will be valid for {{ timeout_minutes }} minutes and can be used only once.{% endblocktrans %} {% blocktrans %}If you did not request this email, please ignore it.{% endblocktrans %} diff --git a/hypha/apply/users/templates/users/emails/set_password.txt b/hypha/apply/users/templates/users/emails/set_password.txt index e7c16410b1..db827b9d62 100644 --- a/hypha/apply/users/templates/users/emails/set_password.txt +++ b/hypha/apply/users/templates/users/emails/set_password.txt @@ -5,7 +5,7 @@ {% if site %}{{ site.root_url }}{% else %}{{ base_url }}{% endif %}{{ activation_path }} -{% blocktrans %}This link can be used only once and will lead you to a page where you can set your password. It will remain active for {{ timeout_days }} days, so please set your password as soon as possible.{% endblocktrans %} +{% blocktrans %}This link can be used only once and will lead you to a page where you can set your password. It will remain active for {{ timeout_minutes }} minutes, so please set your password as soon as possible.{% endblocktrans %} {% blocktrans %}Kind Regards, The {{ ORG_SHORT_NAME }} Team{% endblocktrans %} diff --git a/hypha/apply/users/templates/users/login.html b/hypha/apply/users/templates/users/login.html index 7f69b8ceae..e216303222 100644 --- a/hypha/apply/users/templates/users/login.html +++ b/hypha/apply/users/templates/users/login.html @@ -41,7 +41,7 @@

{% trans "Two factor verification" %}

} -

{% blocktrans %}Log in to {{ ORG_SHORT_NAME }}{% endblocktrans %}

+

{% blocktrans %}Log in to {{ ORG_SHORT_NAME }}{% endblocktrans %}

{% for field in form %}
{% include "forms/includes/field.html" %} diff --git a/hypha/apply/users/templates/users/partials/passwordless_login_signup_sent.html b/hypha/apply/users/templates/users/partials/passwordless_login_signup_sent.html deleted file mode 100644 index bd4f75c4c7..0000000000 --- a/hypha/apply/users/templates/users/partials/passwordless_login_signup_sent.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends base_template %} -{% load i18n heroicons %} - -{% block content %} -
-
- {% heroicon_outline "document-check" aria_hidden="true" size=64 %} -
-

- {% trans "Check your inbox to proceed!" %} -

- -

- {% if ENABLE_PUBLIC_SIGNUP %} - {% trans "We have sent you an email containing a link for logging in or signing up. Please check your email and use the link provided to either login or create your account." %}

- {% else %} - {% trans "We've sent you an email with a login link. Kindly check your email and follow the link to access your account." %}

- {% endif %} -

- -

- {% blocktrans %}Check your "Spam" folder, if you don't find the email in your inbox.{% endblocktrans %} -

- -

- {% trans "Try again" %} -

-
-{% endblock content %} diff --git a/hypha/apply/users/templates/users/password_reset/confirm.html b/hypha/apply/users/templates/users/password_reset/confirm.html index 61b0653931..bede1ebeef 100644 --- a/hypha/apply/users/templates/users/password_reset/confirm.html +++ b/hypha/apply/users/templates/users/password_reset/confirm.html @@ -4,45 +4,49 @@ {% block body_class %}bg-base-100{% endblock %} {% block content %} - -
- {% if validlink %} - -

{% trans "Reset password" %}

-

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

- -
- {% csrf_token %} - {% if redirect_url %} - - {% endif %} - - {% if form.non_field_errors %} -
    - {% for error in form.non_field_errors %} -
  • {{ error }}
  • +
    +
    +
    +

    {% trans "Reset password" %}

    + {% if validlink %} +

    + {% trans "Please enter your new password twice so we can verify you typed it in correctly." %} +

    + + + {% csrf_token %} + {% if redirect_url %} + + {% endif %} + + {% if form.non_field_errors %} +
      + {% for error in form.non_field_errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% endif %} + + {% if form.errors %} +
      + {% blocktrans trimmed count counter=form.errors.items|length %} +
    • Please correct the error below.
    • + {% plural %} +
    • Please correct the errors below.
    • + {% endblocktrans %} +
    + {% endif %} + + {% for field in form %} + {% include "forms/includes/field.html" %} {% endfor %} -
- {% endif %} - {% if form.errors %} -
    - {% blocktrans trimmed count counter=form.errors.items|length %} -
  • Please correct the error below.
  • - {% plural %} -
  • Please correct the errors below.
  • - {% endblocktrans %} -
+ +
+ {% else %} +

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

{% endif %} - - {% for field in form %} - {% include "forms/includes/field.html" %} - {% endfor %} - - - - {% else %} -

{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}

- {% endif %} +
+
{% endblock %} diff --git a/hypha/apply/users/templates/users/password_reset/done.html b/hypha/apply/users/templates/users/password_reset/done.html index f2993904f6..a153ebe212 100644 --- a/hypha/apply/users/templates/users/password_reset/done.html +++ b/hypha/apply/users/templates/users/password_reset/done.html @@ -1,21 +1,28 @@ {% extends "base-apply.html" %} -{% load i18n %} +{% load i18n heroicons %} {% block title %}{% trans "Password Reset" %}{% endblock %} {% block body_class %}bg-base-100{% endblock %} {% block content %} +
+
+
+ + {% heroicon_outline "envelope-open" aria_hidden="true" size=64 %} + -
+

{% trans "Check your inbox to proceed!" %}

-

- {% trans "Check your inbox for a password reset email!" %} -

-

- {% blocktrans %}We have sent an email to you with a password recovery link, open the link in the email to change your password.{% endblocktrans %} -

-

- {% blocktrans %}Check your "Spam" folder, if you don't find the email in your inbox.{% endblocktrans %} -

+
+

+ {% blocktrans %}We have sent an email to you with a password recovery link, open the link in the email to change your password.{% endblocktrans %} +

+

+ {% blocktrans %}Check your "Spam" folder, if you don't find the email in your inbox.{% endblocktrans %} +

+
+
+
{% endblock %} diff --git a/hypha/apply/users/templates/users/password_reset/email.txt b/hypha/apply/users/templates/users/password_reset/email.txt index 9d86d5df39..c26906d6e1 100644 --- a/hypha/apply/users/templates/users/password_reset/email.txt +++ b/hypha/apply/users/templates/users/password_reset/email.txt @@ -7,6 +7,8 @@ {{ protocol }}://{{ domain }}{% url 'users:password_reset_confirm' uidb64=uid token=token %}{% if redirect_url %}?next={{ redirect_url }}{% endif%} +{% blocktrans %}This link will lead you to a page where you can set your password. It will remain active for {{ timeout_minutes }} minutes, so please set your password as soon as possible.{% endblocktrans %} + {% blocktrans %}Kind Regards, The {{ ORG_SHORT_NAME }} Team{% endblocktrans %} diff --git a/hypha/apply/users/templates/users/password_reset/form.html b/hypha/apply/users/templates/users/password_reset/form.html index 665083ec42..0daa8ab712 100644 --- a/hypha/apply/users/templates/users/password_reset/form.html +++ b/hypha/apply/users/templates/users/password_reset/form.html @@ -1,30 +1,34 @@ {% extends "base-apply.html" %} -{% load i18n %} +{% load i18n heroicons %} {% block title %}{% trans "Reset password" %}{% endblock %} {% block content %} +
+
+
+

{% trans "Forgot your password?" %}

-
-
+

+ {% trans "Please enter your user account's email address and we will send you a password reset link." %} +

- {% csrf_token %} -

{% trans "Forgot password?" %}

-

- {% trans "Please enter your user account's email address and we will send you a password reset link." %} -

+ + {% csrf_token %} - {% if redirect_url %} - - {% endif %} + {% if redirect_url %} + + {% endif %} - {% for field in form %} - {% include "forms/includes/field.html" %} - {% endfor %} + {% for field in form %} + {% include "forms/includes/field.html" %} + {% endfor %} - -
+ + +
+
{% endblock %} diff --git a/hypha/apply/users/templates/users/passwordless_login_signup.html b/hypha/apply/users/templates/users/passwordless_login_signup.html index 27fe7752f4..20c88f8433 100644 --- a/hypha/apply/users/templates/users/passwordless_login_signup.html +++ b/hypha/apply/users/templates/users/passwordless_login_signup.html @@ -16,13 +16,13 @@ {% endif %} -

+

{% if ENABLE_PUBLIC_SIGNUP %} {% blocktrans %}Log in or signup to {{ ORG_SHORT_NAME }}{% endblocktrans %} {% else %} {% blocktrans %}Log in to {{ ORG_SHORT_NAME }}{% endblocktrans %} {% endif %} -

+
{% for hidden in form.hidden_fields %} diff --git a/hypha/apply/users/templates/users/passwordless_login_signup_sent.html b/hypha/apply/users/templates/users/passwordless_login_signup_sent.html new file mode 100644 index 0000000000..c0d3b43daa --- /dev/null +++ b/hypha/apply/users/templates/users/passwordless_login_signup_sent.html @@ -0,0 +1,34 @@ +{% extends base_template %} +{% load i18n heroicons %} + +{% block content %} +
+
+
+ + {% heroicon_outline "envelope-open" aria_hidden="true" size=64 %} + + +

{% trans "Check your inbox to proceed!" %}

+ +
+

+ {% if ENABLE_PUBLIC_SIGNUP %} + {% trans "We have sent you an email containing a link for logging in or signing up. Please check your email and use the link provided to either login or create your account." %}

+ {% else %} + {% trans "We have sent an email to you with a login link, open the link in the email to login to your account." %}

+ {% endif %} +

+ +

+ {% blocktrans %}Check your "Spam" folder, if you don't find the email in your inbox.{% endblocktrans %} +

+ +

+ {% trans "Try again" %} +

+
+
+
+
+{% endblock content %} diff --git a/hypha/apply/users/utils.py b/hypha/apply/users/utils.py index da2709b7a4..ce3382d840 100644 --- a/hypha/apply/users/utils.py +++ b/hypha/apply/users/utils.py @@ -77,14 +77,14 @@ def send_activation_email( if redirect_url: activation_path = f"{activation_path}?next={redirect_url}" - timeout_days = settings.PASSWORD_RESET_TIMEOUT // (24 * 3600) + timeout_minutes = settings.PASSWORD_ACTIVATION_TIMEOUT // 60 context = { "user": user, "name": user.get_full_name(), "username": user.get_username(), "activation_path": activation_path, - "timeout_days": timeout_days, + "timeout_minutes": timeout_minutes, "ORG_LONG_NAME": settings.ORG_LONG_NAME, "ORG_SHORT_NAME": settings.ORG_SHORT_NAME, } @@ -111,7 +111,7 @@ def send_confirmation_email(user, token, updated_email=None, site=None): "users:confirm_email", kwargs={"uidb64": uid, "token": token} ) - timeout_days = settings.PASSWORD_RESET_TIMEOUT // (24 * 3600) + timeout_minutes = settings.PASSWORD_RESET_TIMEOUT // 60 context = { "user": user, @@ -119,7 +119,7 @@ def send_confirmation_email(user, token, updated_email=None, site=None): "username": user.get_username(), "unverified_email": updated_email, "activation_path": activation_path, - "timeout_days": timeout_days, + "timeout_minutes": timeout_minutes, "ORG_LONG_NAME": settings.ORG_LONG_NAME, "ORG_SHORT_NAME": settings.ORG_SHORT_NAME, } diff --git a/hypha/apply/users/views.py b/hypha/apply/users/views.py index 87d5c39ffc..c5a821859e 100644 --- a/hypha/apply/users/views.py +++ b/hypha/apply/users/views.py @@ -304,6 +304,23 @@ class ActivationView(TemplateView): def get(self, request, *args, **kwargs): user = self.get_user(kwargs.get("uidb64")) + if self.valid(user, kwargs.get("token")): + # Token valid — show confirmation page before consuming it. + # Prevents link-preview bots from activating the account on GET. + return render( + request, + "users/activation/confirm.html", + { + "is_activation": True, + "next": request.GET.get(self.redirect_field_name, ""), + }, + ) + + return render(request, "users/activation/invalid.html") + + def post(self, request, *args, **kwargs): + user = self.get_user(kwargs.get("uidb64")) + if self.valid(user, kwargs.get("token")): user.backend = settings.CUSTOM_AUTH_BACKEND login(request, user) @@ -391,6 +408,7 @@ def get_context_data(self, **kwargs): def get_extra_email_context(self): return { + "timeout_minutes": settings.PASSWORD_RESET_TIMEOUT // 60, "redirect_url": get_redirect_url(self.request, self.redirect_field_name), "site": Site.find_for_request(self.request), "ORG_SHORT_NAME": settings.ORG_SHORT_NAME, @@ -615,7 +633,7 @@ def post(self, request): return TemplateResponse( self.request, - "users/partials/passwordless_login_signup_sent.html", + "users/passwordless_login_signup_sent.html", self.get_context_data(), ) else: @@ -625,8 +643,9 @@ def post(self, request): class PasswordlessLoginView(LoginView): """This view is used to capture the passwordless login token and log the user in. - If the token is valid, the user is logged in and redirected to the dashboard. - If the token is invalid, the user is shown invalid token page. + If the token is valid, the user is shown a confirmation page requiring a click + before login is completed. This prevents link-preview bots (e.g. MS Outlook's + MicrosoftPreview) from consuming the one-time token on a GET request. This view inherits from LoginView to reuse the 2FA views, if a mfa device is added to the user. @@ -638,21 +657,47 @@ def get(self, request, uidb64, token, *args, **kwargs): except (TypeError, ValueError, OverflowError, User.DoesNotExist): user = None + if user and self.check_token(user, token): + # Token is valid — show a confirmation page that requires a POST to + # complete login. This prevents link-preview bots from consuming + # the token before the user clicks it. + return render( + request, + "users/activation/confirm.html", + { + "remember_me": "remember-me" in request.GET, + "next": request.GET.get(self.redirect_field_name, ""), + }, + ) + + return render(request, "users/activation/invalid.html") + + def post(self, request, uidb64, token, *args, **kwargs): + # If storage already has an authenticated user we are in the MFA step + # (user confirmed the link, now submitting their OTP). Delegate to the + # parent WizardView which handles the OTP form. + if self.storage.authenticated_user: + return super().post(request, *args, **kwargs) + + # Initial confirmation POST — validate token and log the user in. + try: + user = User.objects.get(pk=force_str(urlsafe_base64_decode(uidb64))) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + if user and self.check_token(user, token): user.backend = settings.CUSTOM_AUTH_BACKEND - # Check for "?remember-me" query param, set the session age to long if exists - if "remember-me" in request.GET: + if request.POST.get("remember_me"): self.request.session.set_expiry(settings.SESSION_COOKIE_AGE_LONG) if default_device(user): - # User has mfa, set the user details and redirect to 2fa login + # User has MFA — store details and redirect to OTP step. self.storage.reset() self.storage.authenticated_user = user self.storage.data["authentication_time"] = int(time.time()) return self.render_goto_step("token") - # No mfa, log the user in login(request, user) if redirect_url := get_redirect_url(request, self.redirect_field_name): @@ -668,10 +713,14 @@ def check_token(self, user, token): class PasswordlessSignupView(TemplateView): - """This view is used to capture the passwordless login token and log the user in. + """This view is used to capture the passwordless signup token and create the account. - If the token is valid, the user is logged in and redirected to the dashboard. - If the token is invalid, the user is shown invalid token page. + On GET the token is validated but the account is NOT created yet. A confirmation + page is shown requiring a click before account creation completes. This prevents + link-preview bots (e.g. MS Outlook's MicrosoftPreview) from consuming the + one-time token before the user clicks it. + + If the token is invalid, the user is shown the invalid token page. """ redirect_field_name = "next" @@ -681,14 +730,31 @@ def get(self, request, *args, **kwargs): token = kwargs.get("token") token_generator = PasswordlessSignupTokenGenerator() + if pending_signup and token_generator.check_token(pending_signup, token): + return render( + request, + "users/activation/confirm.html", + { + "is_signup": True, + "remember_me": "remember-me" in request.GET, + "next": request.GET.get(self.redirect_field_name, ""), + }, + ) + + return render(request, "users/activation/invalid.html") + + def post(self, request, *args, **kwargs): + pending_signup = self.get_pending_signup(kwargs.get("uidb64")) + token = kwargs.get("token") + token_generator = PasswordlessSignupTokenGenerator() + if pending_signup and token_generator.check_token(pending_signup, token): user = User.objects.create(email=pending_signup.email, is_active=True) user.set_unusable_password() user.save() pending_signup.delete() - # Check for "?remember-me" query param, set the session age to long if exists - if "remember-me" in request.GET: + if request.POST.get("remember_me"): self.request.session.set_expiry(settings.SESSION_COOKIE_AGE_LONG) user.backend = settings.CUSTOM_AUTH_BACKEND diff --git a/hypha/settings/base.py b/hypha/settings/base.py index e4feb55237..c12f5d2306 100644 --- a/hypha/settings/base.py +++ b/hypha/settings/base.py @@ -221,11 +221,17 @@ # NOTE: Ensure the packages in `requirements/translate.txt` have been installed! APPLICATION_TRANSLATIONS_ENABLED = env.bool("APPLICATION_TRANSLATIONS_ENABLED", False) -# Number of seconds that password reset and account activation links are valid (default 259200, 3 days). -PASSWORD_RESET_TIMEOUT = env.int("PASSWORD_RESET_TIMEOUT", 259200) +# Number of seconds that password reset are valid (default 900, 15 minutes). +PASSWORD_RESET_TIMEOUT = env.int("PASSWORD_RESET_TIMEOUT", 900) + +# Number of seconds that account activation links are valid (default 900, 15 minutes). +PASSWORD_ACTIVATION_TIMEOUT = env.int("PASSWORD_ACTIVATION_TIMEOUT", 900) # Timeout for passwordless login links (default 900, 15 minutes). -PASSWORDLESS_LOGIN_TIMEOUT = env.int("PASSWORDLESS_LOGIN_TIMEOUT", 900) # 15 minutes +PASSWORDLESS_LOGIN_TIMEOUT = env.int("PASSWORDLESS_LOGIN_TIMEOUT", 900) + +# Timeout for passwordless signup links (default 900, 15 minutes). +PASSWORDLESS_SIGNUP_TIMEOUT = env.int("PASSWORDLESS_SIGNUP_TIMEOUT", 900) # Enable users to create accounts without submitting an application. ENABLE_PUBLIC_SIGNUP = env.bool("ENABLE_PUBLIC_SIGNUP", True) @@ -235,10 +241,7 @@ # @deprecated: This setting is deprecated and will be removed in a future release. FORCE_LOGIN_FOR_APPLICATION = env.bool("FORCE_LOGIN_FOR_APPLICATION", True) -# Timeout for passwordless signup links (default 900, 15 minutes). -PASSWORDLESS_SIGNUP_TIMEOUT = env.int("PASSWORDLESS_SIGNUP_TIMEOUT", 900) # 15 minutes - -# Seconds to enter password on password page while email change/2FA change (default 120). +# Seconds to enter password on password page while email change/2FA change (default 120, 2 minutes). PASSWORD_PAGE_TIMEOUT = env.int("PASSWORD_PAGE_TIMEOUT", 120) # Template engines and options to be used with Django. diff --git a/hypha/templates/password_required.html b/hypha/templates/password_required.html index 92757856e4..e7736fe804 100644 --- a/hypha/templates/password_required.html +++ b/hypha/templates/password_required.html @@ -6,7 +6,7 @@ {% block body_class %}template-password-required{% endblock %} {% block content %} -

{% trans "Password required" %}

+

{% trans "Password required" %}

{% trans "Please enter the password to proceed." %}