diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 625cca1504..52ff5e38e4 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -43,6 +43,7 @@
"password_settings",
"logout_on_password_reset",
"force_user_to_reset_password",
+ "reset_password_link_expiry_duration",
"password_reset_limit",
"column_break_31",
"enable_password_policy",
@@ -495,12 +496,19 @@
"fieldname": "disable_change_log_notification",
"fieldtype": "Check",
"label": "Disable Change Log Notification"
+ },
+ {
+ "default": "1200",
+ "fieldname": "reset_password_link_expiry_duration",
+ "fieldtype": "Duration",
+ "label": "Reset Password Link Expiry Duration",
+ "non_negative": 1
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2022-05-09 18:53:35.218721",
+ "modified": "2022-05-19 00:00:18.095269",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index 151dd40308..d5e0a108b5 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
+import time
import unittest
from unittest.mock import patch
@@ -256,7 +257,8 @@ class TestUser(unittest.TestCase):
@Team
and
-
+
@Unknown Team
please check
@@ -365,7 +367,7 @@ class TestUser(unittest.TestCase):
self.assertEqual(update_password(new_password, key=test_user.reset_password_key), "/app")
self.assertEqual(
update_password(new_password, key="wrong_key"),
- "The Link specified has either been used before or Invalid",
+ "The reset password link has either been used before or is invalid",
)
# password verification should fail with old password
@@ -374,7 +376,6 @@ class TestUser(unittest.TestCase):
# reset password
update_password(old_password, old_password=new_password)
-
self.assertRaisesRegex(
frappe.exceptions.ValidationError, "Invalid key type", update_password, "test", 1, ["like", "%"]
)
@@ -434,6 +435,21 @@ class TestUser(unittest.TestCase):
[m.get("module_name") for m in get_modules_from_all_apps()],
)
+ def test_reset_password_link_expiry(self):
+ new_password = "new_password"
+ # set the reset password expiry to 1 second
+ frappe.db.set_value(
+ "System Settings", "System Settings", "reset_password_link_expiry_duration", 1
+ )
+ frappe.set_user("testpassword@example.com")
+ test_user = frappe.get_doc("User", "testpassword@example.com")
+ test_user.reset_password()
+ time.sleep(1) # sleep for 1 sec to expire the reset link
+ self.assertEqual(
+ update_password(new_password, key=test_user.reset_password_key),
+ "The reset password link has been expired",
+ )
+
def delete_contact(user):
frappe.db.delete("Contact", {"email_id": user})
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 642a392a58..42122ebfda 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -43,6 +43,7 @@
"new_password",
"logout_all_sessions",
"reset_password_key",
+ "last_reset_password_key_generated_on",
"last_password_reset_date",
"redirect_url",
"document_follow_notifications_section",
@@ -613,6 +614,14 @@
"label": "Module Profile",
"options": "Module Profile"
},
+ {
+ "description": "Stores the datetime when the last reset password key was generated.",
+ "fieldname": "last_reset_password_key_generated_on",
+ "fieldtype": "Datetime",
+ "hidden": 1,
+ "label": "Last Reset Password Key Generated On",
+ "read_only": 1
+ },
{
"fieldname": "column_break_75",
"fieldtype": "Column Break"
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index e81f5ecd99..1ff5c98a91 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1,5 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
+from datetime import timedelta
+
from bs4 import BeautifulSoup
import frappe
@@ -276,6 +278,7 @@ class User(Document):
key = random_string(32)
self.db_set("reset_password_key", key)
+ self.db_set("last_reset_password_key_generated_on", now_datetime())
url = "/update-password?key=" + key
if password_expired:
@@ -780,16 +783,27 @@ def _get_user_for_update_password(key, old_password):
# verify old password
result = frappe._dict()
if key:
- result.user = frappe.db.get_value("User", {"reset_password_key": key})
- if not result.user:
- result.message = _("The Link specified has either been used before or Invalid")
-
+ user = frappe.db.get_value(
+ "User", {"reset_password_key": key}, ["name", "last_reset_password_key_generated_on"]
+ )
+ result.user, last_reset_password_key_generated_on = user or (None, None)
+ if result.user:
+ reset_password_link_expiry = cint(
+ frappe.db.get_single_value("System Settings", "reset_password_link_expiry_duration")
+ )
+ if (
+ reset_password_link_expiry
+ and now_datetime()
+ > last_reset_password_key_generated_on + timedelta(seconds=reset_password_link_expiry)
+ ):
+ result.message = _("The reset password link has been expired")
+ else:
+ result.message = _("The reset password link has either been used before or is invalid")
elif old_password:
# verify old password
frappe.local.login_manager.check_password(frappe.session.user, old_password)
user = frappe.session.user
result.user = user
-
return result