From db4a7504e5335a7c44e5a4e1c92beb6fd865710d Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 4 Jul 2025 13:17:23 +0530 Subject: [PATCH] fix: add hooks to handle cors --- frappe/app.py | 8 +-- frappe/hooks.py | 1 + .../oauth_settings/oauth_settings.json | 50 ++++++++++++++--- .../doctype/oauth_settings/oauth_settings.py | 1 + frappe/integrations/oauth2.py | 53 ++++++++++++++++++- frappe/integrations/utils.py | 2 +- 6 files changed, 101 insertions(+), 14 deletions(-) diff --git a/frappe/app.py b/frappe/app.py index 2bac39016e..e5a1fba4a7 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -275,10 +275,12 @@ def process_response(response: Response): def set_cors_headers(response): + allowed_origins = frappe.conf.allow_cors + if hasattr(frappe.local, "allow_cors"): + allowed_origins = frappe.local.allow_cors + if not ( - (allowed_origins := frappe.conf.allow_cors) - and (request := frappe.local.request) - and (origin := request.headers.get("Origin")) + allowed_origins and (request := frappe.local.request) and (origin := request.headers.get("Origin")) ): return diff --git a/frappe/hooks.py b/frappe/hooks.py index 992cb2a2e0..bebb1f7e73 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -413,6 +413,7 @@ before_request = [ "frappe.recorder.record", "frappe.monitor.start", "frappe.rate_limiter.apply", + "frappe.integrations.oauth2.set_cors_for_privileged_requests", ] after_request = [ diff --git a/frappe/integrations/doctype/oauth_settings/oauth_settings.json b/frappe/integrations/doctype/oauth_settings/oauth_settings.json index be8c13c590..f30124392f 100644 --- a/frappe/integrations/doctype/oauth_settings/oauth_settings.json +++ b/frappe/integrations/doctype/oauth_settings/oauth_settings.json @@ -6,15 +6,21 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "authorization_tab", "authorization_server_section", "show_auth_server_metadata", - "enable_dynamic_client_registration", "skip_authorization", + "column_break_ogmd", + "enable_dynamic_client_registration", + "allowed_origins_for_public_client_registration", + "resource_tab", + "config_section", + "show_protected_resource_metadata", + "column_break_wlfj", + "show_social_login_key_as_authorization_server", "resource_server_section", "resource_name", "resource_policy_uri", - "show_protected_resource_metadata", - "show_social_login_key_as_authorization_server", "column_break_zyte", "resource_documentation", "resource_tos_uri", @@ -22,10 +28,10 @@ ], "fields": [ { - "description": "These fields are used to provide resource server metadata to clients querying the \"/.well-known/oauth-protected-resource\" end point. For additional reference view: https://datatracker.ietf.org/doc/html/rfc9728", + "description": "These fields are used to provide resource server metadata to clients querying the \"well known protected resource\" end point.", "fieldname": "resource_server_section", "fieldtype": "Section Break", - "label": "Resource Server" + "label": "Metadata" }, { "default": "Frappe Framework Application", @@ -59,8 +65,7 @@ }, { "fieldname": "authorization_server_section", - "fieldtype": "Section Break", - "label": "Authorization Server" + "fieldtype": "Section Break" }, { "default": "1", @@ -102,13 +107,42 @@ "fieldname": "show_social_login_key_as_authorization_server", "fieldtype": "Check", "label": "Show Social Login Key as Authorization Server" + }, + { + "fieldname": "column_break_ogmd", + "fieldtype": "Column Break" + }, + { + "fieldname": "authorization_tab", + "fieldtype": "Tab Break", + "label": "Authorization" + }, + { + "fieldname": "resource_tab", + "fieldtype": "Tab Break", + "label": "Resource" + }, + { + "fieldname": "config_section", + "fieldtype": "Section Break", + "label": "Config" + }, + { + "fieldname": "column_break_wlfj", + "fieldtype": "Column Break" + }, + { + "description": "New line separated list of allowed public client URLs (eg https://frappe.io), or * to accept all.\n
\nPublic clients are restricted by default.", + "fieldname": "allowed_origins_for_public_client_registration", + "fieldtype": "Small Text", + "label": "Allowed Origins for Public Client Registration" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-07-04 11:20:15.528611", + "modified": "2025-07-04 12:05:50.723018", "modified_by": "Administrator", "module": "Integrations", "name": "OAuth Settings", diff --git a/frappe/integrations/doctype/oauth_settings/oauth_settings.py b/frappe/integrations/doctype/oauth_settings/oauth_settings.py index afffb32226..7133b2b1f5 100644 --- a/frappe/integrations/doctype/oauth_settings/oauth_settings.py +++ b/frappe/integrations/doctype/oauth_settings/oauth_settings.py @@ -14,6 +14,7 @@ class OAuthSettings(Document): if TYPE_CHECKING: from frappe.types import DF + allowed_origins_for_public_client_registration: DF.SmallText | None enable_dynamic_client_registration: DF.Check resource_documentation: DF.Data | None resource_name: DF.Data | None diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index 06329b88eb..2e2dc6fab5 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -258,7 +258,7 @@ def introspect_token(token=None, token_type_hint=None): def handle_wellknown(path: str): - """Path handler for /.well-known/ endpoints. Invoked in app.py""" + """Path handler for GET requests to /.well-known/ endpoints. Invoked in app.py""" if path.startswith("/.well-known/openid-configuration"): return get_openid_configuration() @@ -284,6 +284,7 @@ def get_authorization_server_metadata(): response = Response() response.mimetype = "application/json" response.data = frappe.as_json(_get_authorization_server_metadata()) + frappe.local.allow_cors = "*" return response @@ -321,7 +322,7 @@ def _get_authorization_server_metadata(): return metadata -@frappe.whitelist(allow_guest=True) +@frappe.whitelist(allow_guest=True, methods=["POST"]) def register_client(): """ Registers an OAuth client. @@ -473,3 +474,51 @@ def _del_none_values(d: dict): for k in list(d.keys()): if k in d and d[k] is None: del d[k] + + +def set_cors_for_privileged_requests(): + """ + Called in before_request hook, prevents failure of privileged requests, + for OPTIONS and: + 1. GET requests on /.well-known/ + 2. POST requests on /api/method/frappe.integrations.oauth2.register_client + + Point 2. also depends on OAuth Settings for dynamic client registration. + Without these, registration requests from public clients will fail due to + preflight requests failing. + """ + if ( + frappe.conf.allow_cors == "*" + or not frappe.local.request + or not frappe.local.request.headers.get("Origin") + ): + return + + if frappe.request.path.startswith("/.well-known/") and frappe.request.method in ("GET", "OPTIONS"): + frappe.local.allow_cors = "*" + return + + if ( + not frappe.request.path.startswith("/api/method/frappe.integrations.oauth2.register_client") + or frappe.request.method not in ("POST", "OPTIONS") + or not frappe.get_cached_value( + "OAuth Settings", + "OAuth Settings", + "enable_dynamic_client_registration", + ) + ): + return + + allowed = frappe.get_cached_value( + "OAuth Settings", + "OAuth Settings", + "allowed_origins_for_public_client_registration", + ) + if not allowed: + return + + allowed = allowed.strip().splitlines() + if "*" in allowed: + frappe.local.allow_cors = "*" + else: + frappe.local.allow_cors = allowed diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index e428b55b8d..a97ceba498 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -252,7 +252,7 @@ def create_new_oauth_client(client: OAuth2DynamicClientMetadata): if client.software_version: doc.software_version = client.software_version - doc.save() + doc.save(ignore_permissions=True) return doc