fix: add hooks to handle cors

This commit is contained in:
18alantom 2025-07-04 13:17:23 +05:30
parent e76c1830e1
commit db4a7504e5
No known key found for this signature in database
GPG key ID: 942F199B7FFF4BF7
6 changed files with 101 additions and 14 deletions

View file

@ -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

View file

@ -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 = [

View file

@ -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 <code>https://frappe.io</code>), or <code>*</code> to accept all.\n<br>\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",

View file

@ -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

View file

@ -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

View file

@ -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