Repository: frappe/frappe

Issue:
option to disable user-pass based login
Use case: I have single signon configured as social login, so I don't want to enable user-pass based login on my site. 


Discussion:
Should this option be given in the Website Settings document under the Login page section? And the option should be allowed only if a Social Login Key exists?

What if the administrator wants to login?
> the option should be allowed only if a Social Login Key exists?

Yes, to prevent accidental lock-out. 


> What if the administrator wants to login?

`bench --site site browse --user Administrator`


> Should this option be given in the Website Settings document under the Login page section?

System settings would be better option. Login/auth stuff is all over the place but "website" is truly indended for "website" part of framework which doesn't have anything to do with login. 
> System settings would be better option. Login/auth stuff is all over the place but "website" is truly indended for "website" part of framework which doesn't have anything to do with login.

Should I create a new section in System Settings called "Login" or "Authentication"?. There are two other sections "Password" and "Two Factor Authentication" as well.

Also, should the user be prevented from deleting all Social Login Keys if password auth is disabled? 
> Also, should the user be prevented from deleting all Social Login Keys if password auth is disabled?

This will be bit much IMO, warning is fine. 😄 

Resolving patch:
diff --git a/frappe/auth.py b/frappe/auth.py
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -140,6 +140,9 @@ def __init__(self):
 				self.set_user_info()
 
 	def login(self):
+		if frappe.get_system_settings("disable_user_pass_login"):
+			frappe.throw(_("Login with username and password is not allowed."), frappe.AuthenticationError)
+
 		# clear cache
 		frappe.clear_cache(user=frappe.form_dict.get("usr"))
 		user, pwd = get_cached_user_pass()
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -43,6 +43,22 @@ def validate(self):
 		):
 			frappe.flags.update_last_reset_password_date = True
 
+		self.validate_user_pass_login()
+
+	def validate_user_pass_login(self):
+		if not self.disable_user_pass_login:
+			return
+
+		social_login_enabled = frappe.db.exists("Social Login Key", {"enable_social_login": 1})
+		ldap_enabled = frappe.db.get_single_value("LDAP Settings", "enabled")
+
+		if not (social_login_enabled or ldap_enabled):
+			frappe.throw(
+				_(
+					"Please enable atleast one Social Login Key or LDAP before disabling username/password based login."
+				)
+			)
+
 	def on_update(self):
 		self.set_defaults()
 
diff --git a/frappe/www/login.py b/frappe/www/login.py
--- a/frappe/www/login.py
+++ b/frappe/www/login.py
@@ -7,6 +7,7 @@
 from frappe.auth import LoginManager
 from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
 from frappe.integrations.oauth2_logins import decoder_compat
+from frappe.utils import cint
 from frappe.utils.html_utils import get_icon_html
 from frappe.utils.jinja import guess_is_path
 from frappe.utils.oauth import (
@@ -40,7 +41,8 @@ def get_context(context):
 	context.for_test = "login.html"
 	context["title"] = "Login"
 	context["provider_logins"] = []
-	context["disable_signup"] = frappe.utils.cint(frappe.get_website_settings("disable_signup"))
+	context["disable_signup"] = cint(frappe.get_website_settings("disable_signup"))
+	context["disable_user_pass_login"] = cint(frappe.get_system_settings("disable_user_pass_login"))
 	context["logo"] = frappe.get_website_settings("app_logo") or frappe.get_hooks("app_logo_url")[-1]
 	context["app_name"] = (
 		frappe.get_website_settings("app_name") or frappe.get_system_settings("app_name") or _("Frappe")


Test patch:
diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py
--- a/frappe/tests/test_auth.py
+++ b/frappe/tests/test_auth.py
@@ -98,6 +98,7 @@ def test_allow_login_using_username_and_mobile(self):
 
 	def test_deny_multiple_login(self):
 		self.set_system_settings("deny_multiple_sessions", 1)
+		self.addCleanup(self.set_system_settings, "deny_multiple_sessions", 0)
 
 		first_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
 		first_login.get_list("ToDo")
@@ -114,6 +115,14 @@ def test_deny_multiple_login(self):
 			second_login.get_list("ToDo")
 		third_login.get_list("ToDo")
 
+	def test_disable_user_pass_login(self):
+		FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password).get_list("ToDo")
+		self.set_system_settings("disable_user_pass_login", 1)
+		self.addCleanup(self.set_system_settings, "disable_user_pass_login", 0)
+
+		with self.assertRaises(Exception):
+			FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password).get_list("ToDo")
+
 
 class TestLoginAttemptTracker(FrappeTestCase):
 	def test_account_lock(self):