Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ test/fmsg-docker.e2e.test.ts real two-host run, gated by FMSG_E2E=1
- stdout is the stdio protocol channel: log with `console.error` only.
- Public OSS repo: never name a specific identity provider; use `example.com` in examples.

Combined `oauth+api-key` mode routes `Bearer fmsgk_…` to the API-key path and everything else to
OAuth, with separate providers, caches and MCP handlers; never let one resolve the other's callers.
OAuth mode validates incoming tokens for the exact MCP audience and exchanges them for a
separate Web API token. Never forward the incoming JWT or send `X-FMSG-Act-As`. Keep scope
classification in `src/oauth/scopes.ts` synchronized with tools; reply needs read and write.
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

- Add HTTP `FMSG_MCP_AUTH_MODE=oauth+api-key`, serving OAuth and API-key callers on one endpoint.
`Bearer fmsgk_...` requests use API-key authentication; all others, including unauthenticated
requests, use OAuth with its discovery challenge and scope checks. Providers and caches stay
separate. Rejected API keys are also challenged with the protected-resource metadata URL.
`api-key` and `oauth` modes are unchanged. `HttpServerHandle.providers` exposes both providers.
- Add optional `FMSG_API_PUBLIC_URL`: the Web API URL `whoami` reports to users, while requests
keep using `FMSG_API_URL`. It defaults to `FMSG_API_URL`, except that HTTP mode no longer
reports a cleartext (internal or loopback) upstream URL. `whoami`'s `api_url` is therefore
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ configured `Authorization: Bearer fmsgk_...` header. Each caller supplies its ow
header means a shared fmsg identity. For browser sign-in without user API keys, configure
[HTTP OAuth](docs/oauth.md): the operator supplies an issuer, resource URI and exchange client.
Users add the public MCP URL in a host supporting that issuer's client registration method.
`FMSG_MCP_AUTH_MODE=oauth+api-key` accepts both OAuth and API-key callers on the same URL.

For [Claude Code over HTTP](https://code.claude.com/docs/en/mcp):

Expand Down Expand Up @@ -155,8 +156,8 @@ attach resources; prompts `chat` and `reply` script the wait → reply loop and
| `FMSG_API_URL` | — | Base URL of the fmsg Web API (required) |
| `FMSG_API_KEY` | — | `fmsgk_…` key; stdio mode only |
| `FMSG_API_PUBLIC_URL` | `FMSG_API_URL` (HTTP mode: only if HTTPS) | Web API URL reported to users by `whoami`; never used for requests. Set it when the server reaches the Web API at an internal address. Without it, HTTP mode reports no URL for a cleartext `FMSG_API_URL`. Validated like `FMSG_API_URL` |
| `FMSG_MCP_AUTH_MODE` | `api-key` | HTTP authentication: `api-key` or `oauth`; see [OAuth settings](docs/oauth.md#operator-configuration) |
| `FMSG_MCP_PUBLIC_URL` | OAuth resource URL, otherwise unset | Public MCP endpoint, including `/mcp`; enables the HTTP download-link tool. HTTPS required except loopback; in OAuth mode must equal `FMSG_MCP_OAUTH_RESOURCE_URL` |
| `FMSG_MCP_AUTH_MODE` | `api-key` | HTTP authentication: `api-key`, `oauth`, or `oauth+api-key` to accept both on one endpoint; see [OAuth settings](docs/oauth.md#operator-configuration) and [combined mode](docs/oauth.md#combined-oauth-and-api-key-mode) |
| `FMSG_MCP_PUBLIC_URL` | OAuth resource URL, otherwise unset | Public MCP endpoint, including `/mcp`; enables the HTTP download-link tool. HTTPS required except loopback; when OAuth is enabled must equal `FMSG_MCP_OAUTH_RESOURCE_URL` |
| `FMSG_ALLOW_INSECURE_HTTP` | disabled | Set to `1` only to permit cleartext API access on a trusted development/private network; loopback HTTP is allowed by default |
| `FMSG_DEFAULT_DOMAIN` | — | Lets short names resolve: `bob` → `@bob@<domain>` |
| `FMSG_DIRECTORY` | — | JSON file mapping short names to full addresses |
Expand All @@ -165,7 +166,7 @@ attach resources; prompts `chat` and `reply` script the wait → reply loop and
| `FMSG_MCP_HOST` / `FMSG_MCP_PORT` | `127.0.0.1` / `8765` | HTTP bind address (or `--http host:port`) |
| `FMSG_MCP_ALLOWED_HOSTS` | loopback names | Comma-separated `Host` header allowlist; required for non-loopback binds |
| `FMSG_MCP_ALLOWED_ORIGINS` | same origin; loopback origins on loopback binds | Comma-separated browser origins including scheme and port; an explicit list replaces the loopback default; hostname-only values are rejected |
| `FMSG_MCP_KEY_CACHE_MAX` / `FMSG_MCP_KEY_CACHE_TTL_SECONDS` | `500` / `1800` | HTTP client cache bound; TTL applies only to API-key mode |
| `FMSG_MCP_KEY_CACHE_MAX` / `FMSG_MCP_KEY_CACHE_TTL_SECONDS` | `500` / `1800` | HTTP client cache bound; TTL applies only to API-key callers |

The API key is exchanged for a short-lived access token that the server renews automatically.
API URLs must not contain credentials, query strings or fragments. Authenticated requests do not
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ rather than a public issue.

- Messaging authorization, grants, address status, quotas and acceptance remain the responsibility
of fmsg-webapi and the host services. Each MCP request uses its caller's upstream identity.
- Over stdio the API key comes from the environment. HTTP callers supply their own bearer keys or OAuth access tokens, according to the configured mode.
- Over stdio the API key comes from the environment. HTTP callers supply their own bearer keys or OAuth access tokens, according to the configured mode. Combined `oauth+api-key` mode routes `fmsgk_` keys to API-key authentication and all other credentials to OAuth, with separate caches.
Keys and JWTs are retained in process memory for token renewal; hashes index the HTTP client cache.
This server does not intentionally persist them. Idle entries expire on access and periodic sweeps
(at most 30 seconds apart). Evicted or invalidated clients close immediately if idle; active requests
Expand Down
8 changes: 4 additions & 4 deletions docs/http-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

This example runs Caddy and fmsg-mcp on the same machine. Replace `mcp.example.com` with your DNS
name pointing to that machine; Caddy needs access to ports 80/443 for automatic public TLS.
Keep port 8765 bound to loopback. The command below uses API keys; for OAuth add the
[OAuth configuration](oauth.md#operator-configuration).
Keep port 8765 bound to loopback. The command below uses API keys; for OAuth, or OAuth and API
keys on the same endpoint, add the [OAuth configuration](oauth.md#operator-configuration).

```sh
FMSG_API_URL=https://api.example.com \
Expand Down Expand Up @@ -54,8 +54,8 @@ allow response idle time beyond `FMSG_MCP_WAIT_MAX_SECONDS` with assembly headro
allowed and denied Host/Origin requests, unauthenticated 401 responses, CORS preflight, a read,
and cancellation through the actual deployed proxy before advertising that deployment.

The metadata routes are public in OAuth mode and must reach the server for MCP authorization
discovery. In API-key mode they return 404. When OAuth is configured, its resource URL supplies
The metadata routes are public in `oauth` and `oauth+api-key` modes and must reach the server for
MCP authorization discovery. In API-key mode they return 404. When OAuth is configured, its resource URL supplies
the public same-origin value, so explicitly listing that origin is optional.

## Binary attachment downloads
Expand Down
27 changes: 24 additions & 3 deletions docs/oauth.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# HTTP OAuth

OAuth is an optional HTTP authentication mode. API-key HTTP and stdio remain available with
`FMSG_MCP_AUTH_MODE=api-key` (the default). Each endpoint uses one mode; run separate instances
if you need both. There is no API-key fallback in OAuth mode.
`FMSG_MCP_AUTH_MODE=api-key` (the default). `FMSG_MCP_AUTH_MODE=oauth` accepts only OAuth access
tokens; there is no API-key fallback. `FMSG_MCP_AUTH_MODE=oauth+api-key` serves both kinds of
caller on one endpoint; see [combined mode](#combined-oauth-and-api-key-mode).

A user adds the public MCP URL to a compatible host, signs in with the configured authorization
server, and consents to messaging scopes. The host handles authorization-code/PKCE and refresh;
Expand Down Expand Up @@ -57,6 +58,26 @@ Use the [TLS deployment recipe](http-deployment.md), including its metadata rout
on other origins need `FMSG_MCP_ALLOWED_ORIGINS`. The configured resource origin is accepted as
same-origin behind a TLS proxy; forwarded headers never select an issuer, resource or token endpoint.

## Combined OAuth and API-key mode

`FMSG_MCP_AUTH_MODE=oauth+api-key` (HTTP only) accepts OAuth and API-key callers at the same
public MCP URL. It requires the complete OAuth configuration above. Each request is routed by its
`Authorization` header:

- `Bearer fmsgk_...` is handled exactly as in API-key mode. The key is exchanged at the Web API
and is never sent to the authorization server. OAuth scopes do not apply; the Web API decides
access as for any API-key caller.
- Any other bearer token, or a missing or malformed header, is handled exactly as in OAuth mode,
including scope checks. Unauthenticated requests receive the OAuth `401` challenge with
`resource_metadata`, so hosts can discover sign-in.

The two kinds of caller use separate providers and caches: a request is only ever served by the
mechanism that authenticated it, and a JWT is never treated as an API key. Protected-resource
metadata is served as in OAuth mode, and attachment downloads accept either credential. A rejected
API key returns `401` with `error="invalid_token"` and the same `resource_metadata`, without a
`scope` parameter, so a host can offer sign-in instead. API-key callers need a host that sends a
configured `Authorization` header; hosts that only support MCP authorization sign in with OAuth.

## Discovery and client onboarding

Public [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728.html) metadata is served at
Expand Down Expand Up @@ -126,7 +147,7 @@ Each incoming token has its own client and cache, even when two tokens name the
The cache lifetime is the earliest of `expires_in`, exchanged JWT expiry, incoming JWT expiry
and five minutes from the exchange request. Renewal normally starts halfway through that lifetime.
`FMSG_MCP_KEY_CACHE_MAX` bounds retained client entries; idle OAuth entries are evicted after five
minutes. `FMSG_MCP_KEY_CACHE_TTL_SECONDS` applies only to API-key mode and cannot extend OAuth
minutes. `FMSG_MCP_KEY_CACHE_TTL_SECONDS` applies only to API-key callers and cannot extend OAuth
credentials. Active requests retain their own leases through cache eviction. No token is persisted.

Web API `401` retries once after re-exchange; `403` never retries. An expiry timer closes an OAuth
Expand Down
Loading
Loading