_consent_page renders a hard-coded product name into the approve/deny form:
enlace_auth/auth/oauth_server.py:461 (name elided here — read the line):
<p><strong>{client}</strong> is requesting access to your «HARD-CODED PRODUCT NAME» data as
<strong>{email}</strong>.</p>
The authorization server is shared by every MCP connector on a platform — one router
serves every resource. So that single string is what a user reads while authorizing any
connector. It is correct for exactly one of them and wrong for all the others.
git log -L461,461:enlace_auth/auth/oauth_server.py dates it to 77b6881 ("OAuth 2.1
authorization server for MCP connectors", #12), when the platform had exactly one
connector. It was never parameterised. The deployment now runs three, and every new
connector inherits the first one's name.
This is live, not latent
- The consent page renders whenever
require_consent is true — the default in both
enlace_auth/config.py:63 and enlace_auth/auth/oauth_server.py:145 — and the platform
config that consumes it does not override it.
- Path:
GET /auth/oauth/authorize (oauth_server.py:319) → login redirect if no session →
_resource_allowed (:342) → _consent_page (:351) → the string above.
It was reported from real use: a user authorizing one connector saw a different connector's
product name.
Why this is more than cosmetic
resource_allowlist exists so that a connector can be restricted to a specific set of users
(oauth_server.py:146; docstring :155-163). A connector gated that way has a deliberately
narrow audience — yet the shared consent page announces its product name to everyone who
authorizes any other connector on the same platform, including users who are not on its
allowlist. Per-connector access control at the token layer, undone by a global string at the
consent layer.
Structural fix — key the display name on resource
The connector's identity is already in hand at render time: _Authorized.resource
(oauth_server.py:129, populated at :302). It is the same key resource_allowlist is
keyed by, and it is the right key, because a connector only accepts tokens whose aud is
that exact resource.
- Add
resource_display_names: dict[str, str] to the OAuth server config
(enlace_auth/config.py, alongside resource_allowlist at :64); thread it through
make_oauth_server_router (oauth_server.py:146) and enlace_auth/plugin.py:302,
normalising keys with the same .rstrip("/") the allowlist already uses
(oauth_server.py:165-169).
_consent_page renders display_names.get(resource).
- The fallback must be generic — never a product name. e.g. "… is requesting access to
your data on this platform." Any default that names a specific connector reintroduces
this bug for the next one. _denied_page (oauth_server.py:443) is already generic and
is the right precedent.
enlace_auth/appmeta.py:55 already carries a per-app display_name overlay, but it is keyed
by app name, not by OAuth resource URL — useful as a source for the value, not as the key.
The upstream half (making the connector's declared title the single source of this string,
rather than a second hand-maintained copy in platform config) is i2mint/enlace_connector#3.
Test it so it cannot come back
tests/test_oauth_server.py already drives a full consent flow (_build / _register). Add:
- a mapped resource → the rendered HTML contains its display name and does not contain
any other configured connector's;
- an unmapped resource → the generic fallback, and no product name at all.
A test that merely asserts the consent page renders, or that a form field is present, passes
with this bug in place — which is how it survived three connectors.
One note while you are rewriting that template
pages._page escapes only the page title; this module does not import html.escape, and
_consent_page interpolates several request-supplied values straight into HTML attributes.
Escape everything you interpolate while you are in there.
_consent_pagerenders a hard-coded product name into the approve/deny form:enlace_auth/auth/oauth_server.py:461(name elided here — read the line):The authorization server is shared by every MCP connector on a platform — one router
serves every
resource. So that single string is what a user reads while authorizing anyconnector. It is correct for exactly one of them and wrong for all the others.
git log -L461,461:enlace_auth/auth/oauth_server.pydates it to 77b6881 ("OAuth 2.1authorization server for MCP connectors", #12), when the platform had exactly one
connector. It was never parameterised. The deployment now runs three, and every new
connector inherits the first one's name.
This is live, not latent
require_consentis true — the default in bothenlace_auth/config.py:63andenlace_auth/auth/oauth_server.py:145— and the platformconfig that consumes it does not override it.
GET /auth/oauth/authorize(oauth_server.py:319) → login redirect if no session →_resource_allowed(:342) →_consent_page(:351) → the string above.It was reported from real use: a user authorizing one connector saw a different connector's
product name.
Why this is more than cosmetic
resource_allowlistexists so that a connector can be restricted to a specific set of users(
oauth_server.py:146; docstring:155-163). A connector gated that way has a deliberatelynarrow audience — yet the shared consent page announces its product name to everyone who
authorizes any other connector on the same platform, including users who are not on its
allowlist. Per-connector access control at the token layer, undone by a global string at the
consent layer.
Structural fix — key the display name on
resourceThe connector's identity is already in hand at render time:
_Authorized.resource(
oauth_server.py:129, populated at:302). It is the same keyresource_allowlistiskeyed by, and it is the right key, because a connector only accepts tokens whose
audisthat exact resource.
resource_display_names: dict[str, str]to the OAuth server config(
enlace_auth/config.py, alongsideresource_allowlistat:64); thread it throughmake_oauth_server_router(oauth_server.py:146) andenlace_auth/plugin.py:302,normalising keys with the same
.rstrip("/")the allowlist already uses(
oauth_server.py:165-169)._consent_pagerendersdisplay_names.get(resource).your data on this platform." Any default that names a specific connector reintroduces
this bug for the next one.
_denied_page(oauth_server.py:443) is already generic andis the right precedent.
enlace_auth/appmeta.py:55already carries a per-appdisplay_nameoverlay, but it is keyedby app name, not by OAuth resource URL — useful as a source for the value, not as the key.
The upstream half (making the connector's declared title the single source of this string,
rather than a second hand-maintained copy in platform config) is i2mint/enlace_connector#3.
Test it so it cannot come back
tests/test_oauth_server.pyalready drives a full consent flow (_build/_register). Add:any other configured connector's;
A test that merely asserts the consent page renders, or that a form field is present, passes
with this bug in place — which is how it survived three connectors.
One note while you are rewriting that template
pages._pageescapes only the page title; this module does not importhtml.escape, and_consent_pageinterpolates several request-supplied values straight into HTML attributes.Escape everything you interpolate while you are in there.