Repository: frappe/frappe

Issue:
Redirect URL for (custom) social login
When adding a social login provider, the Redirect-URL is a read and write field, even though the routes are hardcoded in [oauth2_logins.py](https://github.com/frappe/frappe/blob/develop/frappe/integrations/oauth2_logins.py). Wouldn't it be better to make that field readonly?
What should its value be when using a custom login provider (also with response type code)? To me it seems like there is a route missing for that.


Discussion:
The redirect URL is constructed as 
`https://erp.ourdomain.net/api/method/frappe.integrations.oauth2_logins/myIDservice` 

See https://discuss.erpnext.com/t/oauth-client-login-button/29152/6?u=rasos
Thanks, I look forward for it to land in the development branch. It would be still nice though it the according input field would be automatically filled (and be read-only), otherwise it is very hard to find the correct value.

Resolving patch:
diff --git a/frappe/integrations/oauth2_logins.py b/frappe/integrations/oauth2_logins.py
--- a/frappe/integrations/oauth2_logins.py
+++ b/frappe/integrations/oauth2_logins.py
@@ -2,16 +2,14 @@
 # MIT License. See license.txt
 
 from __future__ import unicode_literals
-
 import frappe
 import frappe.utils
-from frappe.utils.oauth import (login_via_oauth2, login_via_oauth2_id_token,
-                                oauth_decoder)
-
+from frappe.utils.oauth import login_via_oauth2, login_via_oauth2_id_token
+import json
 
 @frappe.whitelist(allow_guest=True)
 def login_via_google(code, state):
-	login_via_oauth2("google", code, state, decoder=oauth_decoder)
+	login_via_oauth2("google", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
 def login_via_github(code, state):
@@ -19,20 +17,34 @@ def login_via_github(code, state):
 
 @frappe.whitelist(allow_guest=True)
 def login_via_facebook(code, state):
-	login_via_oauth2("facebook", code, state, decoder=oauth_decoder)
+	login_via_oauth2("facebook", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
 def login_via_frappe(code, state):
-	login_via_oauth2("frappe", code, state, decoder=oauth_decoder)
+	login_via_oauth2("frappe", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
 def login_via_office365(code, state):
-	login_via_oauth2_id_token("office_365", code, state, decoder=oauth_decoder)
+	login_via_oauth2_id_token("office_365", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
 def login_via_salesforce(code, state):
-	login_via_oauth2("salesforce", code, state, decoder=oauth_decoder)
+	login_via_oauth2("salesforce", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
-def login_via_fairlogin(code, state):
-	login_via_oauth2("fairlogin", code, state, decoder=oauth_decoder)
+def custom(code, state):
+	"""
+	Callback for processing code and state for user added providers
+
+	process social login from /api/method/frappe.integrations.custom/<provider>
+	"""
+	path = frappe.request.path[1:].split("/")
+	if len(path) == 4 and path[3]:
+		provider = path[3]
+		# Validates if provider doctype exists
+		if frappe.db.exists("Social Login Key", provider):
+			login_via_oauth2(provider, code, state, decoder=decoder_compat)
+
+def decoder_compat(b):
+	# https://github.com/litl/rauth/issues/145#issuecomment-31199471
+	return json.loads(bytes(b).decode("utf-8"))
diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py
--- a/frappe/utils/oauth.py
+++ b/frappe/utils/oauth.py
@@ -5,6 +5,7 @@
 import frappe
 import frappe.utils
 import json, jwt
+import base64
 from frappe import _
 from frappe.utils.password import get_decrypted_password
 from six import string_types
@@ -58,16 +59,17 @@ def get_oauth_keys(provider):
 			"client_secret": keys["client_secret"]
 		}
 
-def get_oauth2_authorize_url(provider):
+def get_oauth2_authorize_url(provider, redirect_to):
 	flow = get_oauth2_flow(provider)
 
-	state = { "site": frappe.utils.get_url(), "token": frappe.generate_hash() }
+	state = { "site": frappe.utils.get_url(), "token": frappe.generate_hash(), "redirect_to": redirect_to 	}
+
 	frappe.cache().set_value("{0}:{1}".format(provider, state["token"]), True, expires_in_sec=120)
 
 	# relative to absolute url
 	data = {
 		"redirect_uri": get_redirect_uri(provider),
-		"state": json.dumps(state)
+		"state": base64.b64encode(bytes(json.dumps(state).encode("utf-8")))
 	}
 
 	oauth2_providers = get_oauth2_providers()
@@ -167,7 +169,8 @@ def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=No
 		data = json.loads(data)
 
 	if isinstance(state, string_types):
-		state = json.loads(state)
+		state = base64.b64decode(state)
+		state = json.loads(state.decode("utf-8"))
 
 	if not (state and state["token"]):
 		frappe.respond_as_web_page(_("Invalid Request"), _("Token is missing"), http_status_code=417)
@@ -204,8 +207,13 @@ def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=No
 
 		frappe.response["login_token"] = login_token
 
+
 	else:
-		redirect_post_login(desk_user=frappe.local.response.get('message') == 'Logged In')
+		redirect_to = state.get("redirect_to")
+		redirect_post_login(
+			desk_user=frappe.local.response.get('message') == 'Logged In',
+			redirect_to=redirect_to,
+		)
 
 def update_oauth_user(user, data, provider):
 	if isinstance(data.get("location"), dict):
@@ -267,9 +275,10 @@ def update_oauth_user(user, data, provider):
 		save = True
 		user.set_social_login_userid(provider, userid="/".join(data["sub"].split("/")[-2:]))
 
-	elif provider=="fairlogin" and not user.get_social_login_userid(provider):
+	elif not user.get_social_login_userid(provider):
 		save = True
-		user.set_social_login_userid(provider, userid=data["preferred_username"])
+		user_id_property = frappe.db.get_value("Social Login Key", provider, "user_id_property") or "sub"
+		user.set_social_login_userid(provider, userid=data[user_id_property])
 
 	if save:
 		user.flags.ignore_permissions = True
@@ -285,14 +294,12 @@ def get_last_name(data):
 def get_email(data):
 	return data.get("email") or data.get("upn") or data.get("unique_name")
 
-def redirect_post_login(desk_user):
+def redirect_post_login(desk_user, redirect_to=None):
 	# redirect!
 	frappe.local.response["type"] = "redirect"
 
-	# the #desktop is added to prevent a facebook redirect bug
-	frappe.local.response["location"] = "/desk#desktop" if desk_user else "/me"
+	if not redirect_to:
+		# the #desktop is added to prevent a facebook redirect bug
+		redirect_to = "/desk#desktop" if desk_user else "/me"
 
-def oauth_decoder(data):
-	if isinstance(data, bytes):
-		data = data.decode("utf-8")
-	return json.loads(data)
+	frappe.local.response["location"] = redirect_to
diff --git a/frappe/www/login.py b/frappe/www/login.py
--- a/frappe/www/login.py
+++ b/frappe/www/login.py
@@ -4,19 +4,24 @@
 from __future__ import unicode_literals
 import frappe
 import frappe.utils
-from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, login_via_oauth2, login_via_oauth2_id_token, login_oauth_user as _login_oauth_user, redirect_post_login, oauth_decoder
+from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, login_via_oauth2, login_via_oauth2_id_token, login_oauth_user as _login_oauth_user, redirect_post_login
 import json
 from frappe import _
 from frappe.auth import LoginManager
 from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
 from frappe.utils.password import get_decrypted_password
 from frappe.utils.html_utils import get_icon_html
+from frappe.integrations.oauth2_logins import decoder_compat
 
 no_cache = True
 
 def get_context(context):
+	redirect_to = frappe.local.request.args.get("redirect-to")
+
 	if frappe.session.user != "Guest":
-		frappe.local.flags.redirect_location = "/" if frappe.session.data.user_type=="Website User" else "/desk"
+		if not redirect_to:
+			redirect_to = "/" if frappe.session.data.user_type=="Website User" else "/desk"
+		frappe.local.flags.redirect_location = redirect_to
 		raise frappe.Redirect
 
 	# get settings from site config
@@ -34,14 +39,14 @@ def get_context(context):
 			context.provider_logins.append({
 				"name": provider,
 				"provider_name": frappe.get_value("Social Login Key", provider, "provider_name"),
-				"auth_url": get_oauth2_authorize_url(provider),
+				"auth_url": get_oauth2_authorize_url(provider, redirect_to),
 				"icon": icon
 			})
 			context["social_login"] = True
 	ldap_settings = LDAPSettings.get_ldap_client_settings()
 	context["ldap_settings"] = ldap_settings
 
-	login_name_placeholder = [_("Email Address")]
+	login_name_placeholder = [_("Email address")]
 
 	if frappe.utils.cint(frappe.get_system_settings("allow_login_using_mobile_number")):
 		login_name_placeholder.append(_("Mobile number"))
@@ -55,7 +60,7 @@ def get_context(context):
 
 @frappe.whitelist(allow_guest=True)
 def login_via_google(code, state):
-	login_via_oauth2("google", code, state, decoder=oauth_decoder)
+	login_via_oauth2("google", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
 def login_via_github(code, state):
@@ -63,15 +68,15 @@ def login_via_github(code, state):
 
 @frappe.whitelist(allow_guest=True)
 def login_via_facebook(code, state):
-	login_via_oauth2("facebook", code, state, decoder=oauth_decoder)
+	login_via_oauth2("facebook", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
 def login_via_frappe(code, state):
-	login_via_oauth2("frappe", code, state, decoder=oauth_decoder)
+	login_via_oauth2("frappe", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
 def login_via_office365(code, state):
-	login_via_oauth2_id_token("office_365", code, state, decoder=oauth_decoder)
+	login_via_oauth2_id_token("office_365", code, state, decoder=decoder_compat)
 
 @frappe.whitelist(allow_guest=True)
 def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=None, generate_login_token=False):
@@ -91,4 +96,4 @@ def login_via_token(login_token):
 	frappe.local.form_dict.sid = sid
 	frappe.local.login_manager = LoginManager()
 
-	redirect_post_login(desk_user = frappe.db.get_value("User", frappe.session.user, "user_type")=="System User")
+	redirect_post_login(desk_user = frappe.db.get_value("User", frappe.session.user, "user_type")=="System User")
\ No newline at end of file


Test patch: