Skip to content

fix: normalize OAuth discovery URLs to use host_name config#186

Merged
buildswithpaul merged 3 commits into
buildswithpaul:developfrom
rachana-promantia:fix/oauth-discovery-https-normalization
Jun 8, 2026
Merged

fix: normalize OAuth discovery URLs to use host_name config#186
buildswithpaul merged 3 commits into
buildswithpaul:developfrom
rachana-promantia:fix/oauth-discovery-https-normalization

Conversation

@rachana-promantia

Copy link
Copy Markdown
Contributor

Summary

OAuth discovery endpoint was returning http:// URLs even when host_name
was configured as https://. This breaks hosted OAuth clients like Claude
that reject non-HTTPS token endpoints.

Type of Change

  • Bug fix

Related Issues

Fixes #156

Checklist

  • I have targeted the develop branch (not main)
  • My code follows the existing code style
  • I have tested my changes locally

@buildswithpaul

buildswithpaul commented May 15, 2026

Copy link
Copy Markdown
Owner

Thanks for picking this up — I hit the same issue on a customer's GKE deployment (Gateway API in front, plain HTTP to the Frappe nginx pod) and your fix would have resolved it. A couple of suggestions before this lands:

1. The PR includes unrelated changes from a stale develop

The diff currently touches:

  • frappe_assistant_core/api/handlers/prompts.py and prompt_template.py (Jinja SandboxedEnvironment)
  • frappe_assistant_core/utils/audit_trail.py and hooks.py (audit-trail doc_events removal)
  • plugins/data_science/tools/extract_file_content.py (tesseract OCR backend)
  • assistant_core_settings.json (tesseract option) and prompt_category.json (role change)
  • pyproject.toml (chardet constraint)
  • docs/architecture/*docs/internals/* rename and all the doc cross-references
  • tests/test_prompt_template_sandbox.py (new test file)

These all correspond to commits already on upstream develop (3c1768a Jinja sandbox, fbcce43 audit log, f7d7200 tesseract, 3c63675 chardet, b480d4a docs rename). I think your branch was forked before those landed, so a git fetch upstream && git rebase upstream/develop should drop them all from the diff and leave just the OAuth normalization. PRs scoped to a single concern review much faster and bisect cleanly later.

2. The OAuth normalization itself — please extend it to all discovery endpoints, not just authorization_server_metadata

The current patch fixes /.well-known/oauth-authorization-server, but the same http-from-request-scheme drift exists in every other discovery endpoint in this file:

  • openid_configuration() (oauth_discovery.py:55) calls Frappe's openid_configuration first and then adds MCP keys via get_server_url(). The Frappe-inherited keys (issuer, authorization_endpoint, token_endpoint, revocation_endpoint, introspection_endpoint, userinfo_endpoint) come back with the request scheme, identical to the authorization_server_metadata problem. Today's PR doesn't touch this path, so /.well-known/openid-configuration will still return mixed http/https URLs after the fix.
  • _get_frappe_authorization_server_metadata() v15 fallback (line 154) — same get_server_url() issue.
  • mcp_discovery() (line 110) and protected_resource_metadata() (line 264) — both call get_server_url(). The http → https rewrite logic is missing here too.

Also a correctness concern with the current patch: metadata["issuer"] = frappe_url + "/" and the unconditional http → https rewrite are problematic for two reasons:

  • issuer MUST exactly match across all discovery endpoints per RFC 8414 §3 and per the OIDC discovery spec. If authorization_server_metadata returns https://example.com/ but openid_configuration (unpatched) returns http://example.com, OAuth clients that validate issuer consistency will reject the flow.
  • Unconditional http → https rewrite will break local dev on http://localhost:8000. It should be gated on something like frappe.conf.get("force_https") or the result of host_name being explicitly https://.

Suggested approach: one helper, used everywhere

Extract a single _get_public_base_url() and use it from every endpoint in this module. This guarantees issuer stays consistent across all four .well-known endpoints and the per-endpoint http:// problem can't drift back:

def _get_public_base_url() -> str:
    """
    Resolve the canonical public base URL for OAuth/MCP discovery metadata.

    Resolution order:
      1. site_config `host_name` — explicit operator override; used verbatim
         if it includes a scheme (`https://example.com`).
      2. `frappe.oauth.get_server_url()` — existing behavior (Social Login Key
         "frappe" base_url, then frappe.request.url).
      3. If `force_https` is set in site_config and the result still starts
         with `http://`, upgrade to `https://`. Gated so localhost dev still
         works.

    Trailing slash is stripped so callers can append `/api/method/...` safely.
    """
    host_name = frappe.conf.get("host_name")
    if host_name and "://" in host_name:
        base = host_name
    else:
        base = get_server_url()

    if frappe.conf.get("force_https") and base.startswith("http://"):
        base = "https://" + base[len("http://"):]

    return base.rstrip("/")

Then replace get_server_url() at every call site in this file — there are five:

And in both openid_configuration() (after frappe_openid_config()) and authorization_server_metadata() (after _get_frappe_authorization_server_metadata()), overwrite the six Frappe-inherited URL keys with values built from _get_public_base_url() — same list you have today, applied to both functions:

frappe_url = _get_public_base_url()
metadata["issuer"] = frappe_url
metadata["authorization_endpoint"] = f"{frappe_url}/api/method/frappe.integrations.oauth2.authorize"
metadata["token_endpoint"] = f"{frappe_url}/api/method/frappe.integrations.oauth2.get_token"
metadata["revocation_endpoint"] = f"{frappe_url}/api/method/frappe.integrations.oauth2.revoke_token"
metadata["introspection_endpoint"] = f"{frappe_url}/api/method/frappe.integrations.oauth2.introspect_token"
metadata["userinfo_endpoint"] = f"{frappe_url}/api/method/frappe.integrations.oauth2.openid_profile"

Note: I'd drop the trailing slash on issuer (frappe_url not frappe_url + "/") — RFC 8414 examples have no trailing slash, and the OIDC issuer comparison is byte-exact, so consistency across endpoints is what matters. Current Frappe core returns no trailing slash here too.

Why this matters for our deployment

In our case the underlying cause is gunicorn defaulting to --forwarded-allow-ips=127.0.0.1, so when the Frappe nginx pod forwards X-Forwarded-Proto: https from a non-loopback pod IP, gunicorn drops it and frappe.request.scheme stays http. This is the standard situation on any Helm-deployed Frappe where nginx and gunicorn are in separate pods. The Social Login Key "frappe" base_url workaround patches the bug for some endpoints but not all (since authorization_server_metadata doesn't consult it), which is exactly why a customer ended up at issue #156. Centralizing this in _get_public_base_url() makes the fix robust against that whole class of proxy-trust misconfigurations.

@rachana-promantia rachana-promantia force-pushed the fix/oauth-discovery-https-normalization branch from 4b0b595 to 190e961 Compare May 18, 2026 10:13
- Use host_name from site config if available
- Force https:// when host_name is configured
- Fixes http -> https mismatch for production deployments
- Fixes buildswithpaul#156
- Add _get_public_base_url() helper that respects host_name and force_https
- Replace get_server_url() at all 5 call sites in oauth_discovery.py
- Override Frappe-inherited URL keys in both openid_configuration() and
  authorization_server_metadata() to ensure issuer consistency
- Drop trailing slash on issuer per RFC 8414
- Fixes buildswithpaul#156
@rachana-promantia rachana-promantia force-pushed the fix/oauth-discovery-https-normalization branch from 01d1c26 to 3b23ba4 Compare June 8, 2026 05:33
@buildswithpaul buildswithpaul merged commit 81b5f53 into buildswithpaul:develop Jun 8, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants