Commit Graph
7 Commits
Author SHA1 Message Date
Marc-André Bombeck 3c6cb8b08d fix(mcp): record why a client fails to resolve at /authorize
CIMD client resolution swallowed every fetch failure as ssrf_blocked and
/authorize logged a bare invalid event, so an unreachable metadata host
was indistinguishable from a genuine SSRF block and the consent flow
returned invalid_client with no diagnosable cause.

Split the resolution failure into ssrf_blocked (a real private-host
refusal) and fetch_failed (timeout / connect error), carrying the
concrete SafeFetchError kind and cause as a short non-secret detail.
Thread the resolution reason and detail into the authorize.invalid
annotation behind a pinned meta shape, and cache a resolved CIMD
document for up to 24h so the consent flow does not re-fetch on every
hit.
2026-06-28 15:18:12 +02:00
Marc-André Bombeck 660dffef37 feat(mcp): accept the OIDC application_type in dynamic client registration
Honour the OIDC dynamic-registration application_type field on the
register endpoint: validate it to the closed set (web/native) when
present, reject a malformed value with invalid_client_metadata, default
to web when omitted, and echo the resolved value back in the
client-information response.

Forward-compatible with OIDC-style clients without changing the
redirect-URI rules (still HTTPS-or-loopback, authoritatively matched at
authorize) or any other registration behaviour.
2026-06-28 01:00:51 +02:00
Marc-André Bombeck 01e2373255 feat(mcp): identify the authorization-server issuer per RFC 9207
Advertise authorization_response_iss_parameter_supported in the
authorization-server metadata and echo the iss parameter on every
authorization response — both the success redirect carrying the code and
the access_denied error redirect. A client can now pin the response to
this server's issuer and reject a code substituted by a look-alike
authorization server in a multi-server deployment.

Additive and backward-compatible: existing clients that ignore iss are
unaffected; PKCE S256-only, audience binding, redirect-URI exact match
and the rate limits are untouched.
2026-06-28 01:00:45 +02:00
Marc-André Bombeck ddfe9170bd feat(mcp): allow minting and consenting to health:write connector tokens
Let a user opt a connector token into logging. The settings card gains a
write toggle; the mint endpoint accepts a closed scope choice (read, or
read_write) and builds the permission array explicitly — it can mint only
[health:read] or [health:read, health:write], never a wildcard or any other
grant. The OAuth flow grants health:write only when a client requests it, and
the consent screen states in plain language that the app will be able to read
AND log/write health data when write is requested; the token endpoint grants
exactly the approved scopes and never widens. Read-only behaviour is
unchanged. Add the English and German strings for the new toggle and warning.
2026-06-27 17:38:51 +02:00
Marc-André Bombeck 22eb9cba0d fix(mcp): close OAuth 2.1 resource-server gaps before the remote surface ships
Harden the remote MCP endpoint and the in-repo OAuth bridge against the
adversarial review of the milestone branch. The two ship-blockers made the
"read-only, revocable" promise false; the rest tightened the surface.

Audience binding at the resource server: a token whose only grant is the new
`health:read` scope is the MCP read credential. The REST edge now accepts it on
safe (read) methods and refuses it on writes/deletes with 403, so a connector
token can read over `/mcp` and GET routes but can never POST/DELETE the record
despite a "read-only" consent. The `/mcp` resolver is unaffected.

Revocable, theft-detectable refresh tokens: refresh artifacts now anchor to a
persistent connection row. Revoking a connection stops every future refresh and
kills its access tokens; a replayed (already-rotated) refresh is treated as
theft and revokes the whole connection. Connections surface in settings as the
unit a user can disconnect. The old access token is revoked on rotation.

Also: fail closed on the OAuth/`/mcp` surface when no canonical origin is
configured and mark the discovery documents non-cacheable; honour the global API
kill-switch on `/mcp`, `/token`, `/authorize`, and `/register`; cap the CIMD and
registration body reads while streaming instead of after; show the verified
client origin on consent; validate CIMD redirect-URI schemes; assert same-origin
on the consent POST; and add a daily prune for expired connector tokens.

New migration 0195 adds the connection anchor and links access tokens to it.
2026-06-27 15:09:11 +02:00
Marc-André Bombeck 06363771ee feat(settings): add an MCP connector card with health:read tokens
A dedicated Settings card co-locates the off-by-default MCP module toggle
with token management for connecting an external assistant:

- Mint / list / revoke a dedicated `health:read`-scoped token (shown once,
  copy to clipboard) via new `/api/mcp/tokens` routes — never the
  `medication:ingest` scope, never the wildcard.
- Surface the connector URL and the enable switch in one place, wired to
  the existing module-preference endpoint.
- New section wired into the Settings shell next to API & Tokens, with
  EN + DE strings (and the other locales) under `settings.mcp` and
  `settings.sections.mcp`.
2026-06-27 14:27:30 +02:00
Marc-André Bombeck 7ec3bf0dad feat(mcp): add an OAuth 2.1 authorization-server bridge for remote connectors
Let a remote MCP client (Claude.ai, ChatGPT) self-register and connect to
the `/mcp` endpoint over OAuth 2.1 + PKCE, bridged onto the existing `hlk_`
Bearer model — no parallel identity store.

- Discovery: RFC 9728 Protected Resource Metadata and RFC 8414 Authorization
  Server Metadata at `/.well-known/oauth-protected-resource` and
  `/.well-known/oauth-authorization-server` (added to the proxy allowlist).
- `/api/mcp/oauth/authorize` — consent gate backed by the existing session;
  a code is only issued on an authenticated POST decision, never on a GET.
- `/api/mcp/oauth/token` — authorization-code and refresh-token grants with
  mandatory PKCE S256, single-use codes, and rotating refresh tokens. The
  access token is an ordinary short-lived `hlk_` token scoped `health:read`.
- `/api/mcp/oauth/register` — RFC 7591 DCR fallback; CIMD (SEP-991) is the
  preferred path and resolves client metadata through `safeFetch`.
- Audience binding (RFC 8707): every token is bound to the canonical `/mcp`
  resource URI; a mismatched `resource` is rejected at authorize and token.
- The grant artifacts (code, refresh, DCR client id) are HMAC-signed,
  self-describing, and domain-separated; the central log redactors scrub
  them plus the PKCE verifier.
2026-06-27 14:27:09 +02:00