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