Repository: frappe/frappe

Issue:
OAuth flow omits port on redirect
## Version 

ERPNext: v10.1.60 (master)
Frappe Framework: v10.1.53 (master)

## How to reproduce

* Run virtualbox image locally, 
* create an oAuth client with any name and any redirect URI (leave everything else to default),
* Access the `/api/method/frappe.integrations.oauth2.authorize` endpoint in your browser:

```
http://localhost:8080/api/method/frappe.integrations.oauth2.authorize?client_id={your client id}&scope=all&redirect_uri={your redirect uri}&state=4444&response_type=code
```
* Authenticate and allow access. 

## Problem

You will get redirected to

```
http://localhost/api/method/frappe.integrations.oauth2.approve?state=4444&redirect_uri={your redirect uri}&response_type=code&client_id={your client id}&scope=all&data=
```

which **does not exist**, because the port is not specified.

## Expected

Redirect to

```
http://localhost:8080/api/method/frappe.integrations.oauth2.approve?state=4444&redirect_uri={your redirect uri}&response_type=code&client_id={your client id}&scope=all&data=
```


Discussion:
This is still happening in v12.14.0 fwiw

Resolving patch:
diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py
--- a/frappe/integrations/oauth2.py
+++ b/frappe/integrations/oauth2.py
@@ -1,41 +1,50 @@
 from __future__ import unicode_literals
-import frappe, json
-from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer
+
+import hashlib
+import json
+from urllib.parse import quote, urlencode, urlparse
+
+import jwt
 from oauthlib.oauth2 import FatalClientError, OAuth2Error
-from werkzeug import url_fix
-from six.moves.urllib.parse import quote, urlencode, urlparse
-from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings
+
+import frappe
 from frappe import _
+from frappe.oauth import OAuthWebRequestValidator, WebApplicationServer
+from frappe.integrations.doctype.oauth_provider_settings.oauth_provider_settings import get_oauth_settings
 
 def get_oauth_server():
 	if not getattr(frappe.local, 'oauth_server', None):
 		oauth_validator = OAuthWebRequestValidator()
-		frappe.local.oauth_server  = WebApplicationServer(oauth_validator)
+		frappe.local.oauth_server = WebApplicationServer(oauth_validator)
 
 	return frappe.local.oauth_server
 
-def get_urlparams_from_kwargs(param_kwargs):
+def sanitize_kwargs(param_kwargs):
 	arguments = param_kwargs
-	if arguments.get("data"):
-		arguments.pop("data")
-	if arguments.get("cmd"):
-		arguments.pop("cmd")
+	arguments.pop('data', None)
+	arguments.pop('cmd', None)
 
-	return urlencode(arguments)
+	return arguments
 
 @frappe.whitelist()
 def approve(*args, **kwargs):
 	r = frappe.request
-	uri = url_fix(r.url.replace("+"," "))
-	http_method = r.method
-	body = r.get_data()
-	headers = r.headers
 
 	try:
-		scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(uri, http_method, body, headers)
-
-		headers, body, status = get_oauth_server().create_authorization_response(uri=frappe.flags.oauth_credentials['redirect_uri'], \
-				body=body, headers=headers, scopes=scopes, credentials=frappe.flags.oauth_credentials)
+		scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(
+			r.url,
+			r.method,
+			r.get_data(),
+			r.headers
+		)
+
+		headers, body, status = get_oauth_server().create_authorization_response(
+			uri=frappe.flags.oauth_credentials['redirect_uri'],
+			body=r.get_data(),
+			headers=r.headers,
+			scopes=scopes,
+			credentials=frappe.flags.oauth_credentials
+		)
 		uri = headers.get('Location', None)
 
 		frappe.local.response["type"] = "redirect"
@@ -47,34 +56,28 @@ def approve(*args, **kwargs):
 		return e
 
 @frappe.whitelist(allow_guest=True)
-def authorize(*args, **kwargs):
-	#Fetch provider URL from settings
-	oauth_settings = get_oauth_settings()
-	params = get_urlparams_from_kwargs(kwargs)
-	request_url = urlparse(frappe.request.url)
-	success_url = request_url.scheme + "://" + request_url.netloc + "/api/method/frappe.integrations.oauth2.approve?" + params
+def authorize(**kwargs):
+	success_url = "/api/method/frappe.integrations.oauth2.approve?" + encode_params(sanitize_kwargs(kwargs))
 	failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied"
 
-	if frappe.session['user']=='Guest':
+	if frappe.session.user == 'Guest':
 		#Force login, redirect to preauth again.
 		frappe.local.response["type"] = "redirect"
-		frappe.local.response["location"] = "/login?redirect-to=/api/method/frappe.integrations.oauth2.authorize?" + quote(params.replace("+"," "))
-
-	elif frappe.session['user']!='Guest':
+		frappe.local.response["location"] = "/login?" + encode_params({'redirect-to': frappe.request.url})
+	else:
 		try:
 			r = frappe.request
-			uri = url_fix(r.url)
-			http_method = r.method
-			body = r.get_data()
-			headers = r.headers
-
-			scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(uri, http_method, body, headers)
+			scopes, frappe.flags.oauth_credentials = get_oauth_server().validate_authorization_request(
+				r.url,
+				r.method,
+				r.get_data(),
+				r.headers
+			)
 
 			skip_auth = frappe.db.get_value("OAuth Client", frappe.flags.oauth_credentials['client_id'], "skip_authorization")
 			unrevoked_tokens = frappe.get_all("OAuth Bearer Token", filters={"status":"Active"})
 
-			if skip_auth or (oauth_settings["skip_authorization"] == "Auto" and len(unrevoked_tokens)):
-
+			if skip_auth or (get_oauth_settings().skip_authorization == "Auto" and unrevoked_tokens):
 				frappe.local.response["type"] = "redirect"
 				frappe.local.response["location"] = success_url
 			else:
@@ -87,7 +90,6 @@ def authorize(*args, **kwargs):
 				})
 				resp_html = frappe.render_template("templates/includes/oauth_confirmation.html", response_html_params)
 				frappe.respond_as_web_page("Confirm Access", resp_html)
-
 		except FatalClientError as e:
 			return e
 		except OAuth2Error as e:
@@ -95,20 +97,20 @@ def authorize(*args, **kwargs):
 
 @frappe.whitelist(allow_guest=True)
 def get_token(*args, **kwargs):
-	r = frappe.request
-
-	uri = url_fix(r.url)
-	http_method = r.method
-	body = r.form
-	headers = r.headers
-	
 	#Check whether frappe server URL is set
 	frappe_server_url = frappe.db.get_value("Social Login Key", "frappe", "base_url") or None
 	if not frappe_server_url:
 		frappe.throw(_("Please set Base URL in Social Login Key for Frappe"))
 
 	try:
-		headers, body, status = get_oauth_server().create_token_response(uri, http_method, body, headers, frappe.flags.oauth_credentials)
+		r = frappe.request
+		headers, body, status = get_oauth_server().create_token_response(
+			r.url,
+			r.method,
+			r.form,
+			r.headers,
+			frappe.flags.oauth_credentials
+		)
 		out = frappe._dict(json.loads(body))
 		if not out.error and "openid" in out.scope:
 			token_user = frappe.db.get_value("OAuth Bearer Token", out.access_token, "user")
@@ -116,7 +118,7 @@ def get_token(*args, **kwargs):
 			client_secret = frappe.db.get_value("OAuth Client", token_client, "client_secret")
 			if token_user in ["Guest", "Administrator"]:
 				frappe.throw(_("Logged in as Guest or Administrator"))
-			import hashlib
+
 			id_token_header = {
 				"typ":"jwt",
 				"alg":"HS256"
@@ -128,9 +130,10 @@ def get_token(*args, **kwargs):
 				"iss": frappe_server_url,
 				"at_hash": frappe.oauth.calculate_at_hash(out.access_token, hashlib.sha256)
 			}
-			import jwt
+
 			id_token_encoded = jwt.encode(id_token, client_secret, algorithm='HS256', headers=id_token_header)
-			out.update({"id_token":str(id_token_encoded)})
+			out.update({"id_token": str(id_token_encoded)})
+
 		frappe.local.response = out
 
 	except FatalClientError as e:
@@ -140,12 +143,12 @@ def get_token(*args, **kwargs):
 @frappe.whitelist(allow_guest=True)
 def revoke_token(*args, **kwargs):
 	r = frappe.request
-	uri = url_fix(r.url)
-	http_method = r.method
-	body = r.form
-	headers = r.headers
-
-	headers, body, status = get_oauth_server().create_revocation_response(uri, headers=headers, body=body, http_method=http_method)
+	headers, body, status = get_oauth_server().create_revocation_response(
+		r.url,
+		headers=r.headers,
+		body=r.form,
+		http_method=r.method
+	)
 
 	frappe.local.response['http_status_code'] = status
 	if status == 200:
@@ -174,15 +177,22 @@ def openid_profile(*args, **kwargs):
 			"email": name,
 			"picture": picture
 		})
-	
+
 	frappe.local.response = user_profile
 
 def validate_url(url_string):
 	try:
 		result = urlparse(url_string)
-		if result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]:
-			return True
-		else:
-			return False
+		return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"]
 	except:
-		return False
\ No newline at end of file
+		return False
+
+def encode_params(params):
+	"""
+	Encode a dict of params into a query string.
+
+	Use `quote_via=urllib.parse.quote` so that whitespaces will be encoded as
+	`%20` instead of as `+`. This is needed because oauthlib cannot handle `+`
+	as a whitespace.
+	"""
+	return urlencode(params, quote_via=quote)
diff --git a/frappe/oauth.py b/frappe/oauth.py
--- a/frappe/oauth.py
+++ b/frappe/oauth.py
@@ -148,7 +148,7 @@ def authenticate_client(self, request, *args, **kwargs):
 			print("Failed body authentication: Application %s does not exist".format(cid=request.client_id))
 
 		cookie_dict = get_cookie_dict_from_headers(request)
-		user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest"
+		user_id = unquote(cookie_dict.get('user_id').value) if 'user_id' in cookie_dict else "Guest"
 		return frappe.session.user == user_id
 
 	def authenticate_client_id(self, client_id, request, *args, **kwargs):


Test patch:
diff --git a/frappe/tests/test_oauth20.py b/frappe/tests/test_oauth20.py
--- a/frappe/tests/test_oauth20.py
+++ b/frappe/tests/test_oauth20.py
@@ -6,6 +6,7 @@
 from frappe.test_runner import make_test_records
 from six.moves.urllib.parse import urlparse, parse_qs, urljoin
 from urllib.parse import urlencode, quote
+from frappe.integrations.oauth2 import encode_params
 
 class TestOAuth20(unittest.TestCase):
 
@@ -232,13 +233,3 @@ def login(session):
 def get_full_url(endpoint):
 	"""Turn '/endpoint' into 'http://127.0.0.1:8000/endpoint'."""
 	return urljoin(frappe.utils.get_url(), endpoint)
-
-def encode_params(params):
-	"""
-	Encode a dict of params into a query string.
-
-	Use `quote_via=urllib.parse.quote` so that whitespaces will be encoded as
-	`%20` instead of as `+`. This is needed because oauthlib cannot handle `+`
-	as a whitespace.
-	"""
-	return urlencode(params, quote_via=quote)