Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9a079e2
docs(specs): Spec 107 — server edition SSO front door hardened for re…
Dumbris Sep 16, 2026
9fdc58d
test(spec-107): PR-A red tests, server-tag lint job and release notice
Dumbris Sep 16, 2026
43d301d
refactor(serveredition): freeze the latent credential-injection chain…
Dumbris Sep 16, 2026
c255986
fix(storage): enforce the agent-token cap per owner, not per deployme…
Dumbris Sep 16, 2026
3bf4394
fix(spec-107): address adversarial review of PR-A
Dumbris Sep 16, 2026
36e0517
fix(spec-107): codex round 1 for PR-A
Dumbris Sep 16, 2026
e158121
fix(spec-107): codex round 2 for PR-A
Dumbris Sep 16, 2026
03ef19d
fix(spec-107): codex round 3 for PR-A
Dumbris Sep 16, 2026
7a4e8fe
fix(spec-107): codex round 4 for PR-A
Dumbris Sep 16, 2026
fb389e6
Merge origin/main (b1777e865) into 107-a-freeze-cut
Dumbris Sep 16, 2026
14611c9
docs(spec-107): PR-A verification and task ticks
Dumbris Sep 16, 2026
c741f82
docs(roadmap): link PR-A
Dumbris Sep 16, 2026
6cf5427
test(oauthserver): fake OIDC identity provider and server-edition dev…
Dumbris Sep 16, 2026
412dc3a
test(serveredition): red tests for the generic OIDC provider, subject…
Dumbris Sep 16, 2026
c74bfb2
feat(serveredition): generic OIDC provider with JWKS-verified ID toke…
Dumbris Sep 16, 2026
1d88938
test(serveredition): red tests for trusted proxies, public URL, cooki…
Dumbris Sep 16, 2026
07ff5c4
feat(serveredition): front door behind an ingress — public URL, trust…
Dumbris Sep 16, 2026
45d302a
feat(config,telemetry): hot-reload clauses, docs and OAS for the PR-B…
Dumbris Sep 16, 2026
4b8baa3
fix(spec-107): gate fixes for PR-B
Dumbris Sep 16, 2026
516606d
docs(spec-107): PR-B automated-check evidence
Dumbris Sep 16, 2026
fd27d27
fix(spec-107): cross-review round 1 for PR-B
Dumbris Sep 16, 2026
30a93bb
fix(spec-107): cross-review round 2 for PR-B
Dumbris Sep 16, 2026
732329c
fix(spec-107): cross-review round 3 for PR-B
Dumbris Sep 16, 2026
8666dab
fix(spec-107): cross-review round 3 for PR-B
Dumbris Sep 16, 2026
8f2e3d4
fix(spec-107): cross-review round 4 for PR-B
Dumbris Sep 16, 2026
1c976e7
fix(spec-107): cross-review round 5 for PR-B
Dumbris Sep 16, 2026
5ad3a08
fix(spec-107): cross-review round 6 for PR-B
Dumbris Sep 16, 2026
873145b
docs(spec-107): PR-B real-instance verification, gate re-run, task ticks
Dumbris Sep 16, 2026
f3103fd
docs(roadmap): link PR-B
Dumbris Sep 16, 2026
717e580
fix(ci): pull forward the Windows heap-corruption + Pass-2 race fix (…
Dumbris Sep 16, 2026
b4ab0f9
docs(spec-107): record the CI red/fix cycle for PR-B
Dumbris Sep 16, 2026
152f5d5
docs(spec-107): record PR-B CI green
Dumbris Sep 16, 2026
2c35085
fix(spec-107): cross-review round 7 for PR-B
Dumbris Sep 17, 2026
2fdb86b
fix(spec-107): cross-review round 8 for PR-B
Dumbris Sep 17, 2026
1e161d0
test(serveredition): two-fixture non-disclosure harness and single-pr…
Dumbris Sep 17, 2026
e114cc4
test(scope): red tests for group grants, owner resolution, scoped ind…
Dumbris Sep 17, 2026
49b9db9
feat(scope): one entitlement predicate with IdP-group grants, owner r…
Dumbris Sep 17, 2026
fa968e2
test(httpapi,frontend): red tests for the tenant session principal, r…
Dumbris Sep 17, 2026
7561c6b
feat(httpapi,web): tenant session principal on core REST, profiles/SS…
Dumbris Sep 17, 2026
1e379ff
docs(spec-107): access block reference, agent-token invariant, hot-re…
Dumbris Sep 17, 2026
6afc4d0
fix(spec-107): gate fixes for PR-C
Dumbris Sep 17, 2026
bf06b43
fix(spec-107): cross-review round 1 for PR-C
Dumbris Sep 17, 2026
58d2d30
docs(spec-107): record PR-C cross-review round 1
Dumbris Sep 17, 2026
c2e963a
fix(spec-107): cross-review round 2 for PR-C
Dumbris Sep 17, 2026
aac5053
docs(spec-107): record PR-C cross-review round 2
Dumbris Sep 17, 2026
e9776e8
docs(spec-107): PR-C real-instance verification, gate re-run, task ticks
Dumbris Sep 17, 2026
180c249
docs(roadmap): link PR-C
Dumbris Sep 17, 2026
4cdc766
fix(spec-107): cross-review round 3 for PR-C
Dumbris Sep 17, 2026
d735ec7
docs(spec-107): record PR-C cross-review round 3
Dumbris Sep 17, 2026
ff19749
fix(spec-107): wait for PhaseReady before seeding the group-scope fix…
Dumbris Sep 17, 2026
9985d4a
Merge remote-tracking branch 'origin/main' into 107-c-group-allowlist
Dumbris Sep 18, 2026
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
12 changes: 12 additions & 0 deletions .github/RELEASE_NOTICE.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,15 @@ When `server_edition.enabled` is true, `/mcp` now behaves as if `require_mcp_aut
- **Refusals are uniform**: every denied login renders one generic `403` page ("Sign-in was not permitted") with a reference id; the reason (`email_unverified`, `subject_mismatch`, `state_invalid`, …) reaches only the server log and the `auth_event` line under that id. IdP-side failures (`discovery_failed`, `provider_error`) and proxy-side failures after verification (`internal_error`) render a `503` "Sign-in is temporarily unavailable" instead, so an outage is never shown as "not permitted" (FR-024).
- **Login page label and edition probe**: public `GET /api/v1/auth/provider` returns only `{"display_name": "…"}` — never the issuer, client id, tenant, scopes or domains — so the Web UI labels the sign-in button and detects the edition before login; the personal build answers `404` (FR-030).
- Guide: [multi-user authentication](https://docs.mcpproxy.app/development/server-edition-multiuser-auth/).

## Server edition: group-based server access, and credential minting is now session-cookie-only

A tenant — anyone who signs in through the team's IdP rather than through the API key or the local socket — now sees, uses, mints tokens for, connects to and diagnoses exactly the servers their IdP group grants (spec 107, FR-004/FR-009), on the REST API, the Web UI and every agent token they own. Administrators (API key, socket, and any `admin_email` user) are unaffected.

- **New config**: `server_edition.access.group_servers` maps an IdP group name to a list of shared server names (or `["*"]` for every shared server); `access.default_servers` covers a user whose groups match no key. A user's grant is the union of their groups' entries; a server must be **both shared and granted** to appear for them — sharing alone is not enough once the map is active. **The `access` block itself must be present to change anything**: an *absent* block keeps today's `Shared`-only behaviour (every shared server visible to every tenant, unchanged); a *present* block — even `{}`, with no `group_servers`/`default_servers` entries — is deny-all for every tenant that matches none of it. Add the block to narrow access; there is no config that widens it beyond `Shared`.
- **Non-disclosing**: a server outside a tenant's grant does not exist for them on any surface — not its name, tools, prompts, counts, status, logs or activity. This applies to core REST (`/api/v1/*`, `/events`), the Web UI and agent tokens a tenant owns.
- **Agent tokens narrow on every use, not just at mint**: an owned token's effective scope is recomputed from the user's *current* groups and the *current* `access` map on every authentication (one `GetUser` per call) — so **enabling, editing or removing an `access` entry takes effect on the token's very next call**, without rotating or re-minting it. The same is true for a tenant's session on core REST and its SSE stream (the per-frame refresher re-resolves the principal, so un-sharing narrows the next frame). Widening a user's access (the IdP adds them to a group, or the map is edited to include them) only takes effect once they sign in again — groups themselves refresh only at login.
- **Credential-minting doors are session-cookie-only**: `POST /api/v1/auth/token` (JWT renewal), `POST /api/v1/user/tokens` and `POST /api/v1/user/tokens/{name}/regenerate` now accept only the browser session cookie — a bearer JWT or an agent token gets `401`. A derived credential can no longer mint another credential, closing the chain where a JWT could renew itself indefinitely and then mint a 30-day agent token in its last second. `POST /api/v1/user/tokens` also now caps `expires_in` at 365 days (previously unbounded), matching core `/api/v1/tokens`. The practical freshness bound is now **session TTL + the longer of JWT TTL and the longest-lived owned agent token (≤ 365 days)**; an administrator `disable` still takes effect immediately.
- **Subject-rebind procedure for a re-created IdP account**: if your IdP re-creates a user's account (new subject, same email), the login is refused (`subject_mismatch`) rather than silently taking over the existing record. An administrator re-arms the binding by disabling the user and then re-enabling them — this arms a single-use, persisted rebind window — and the user's *next successful login* accepts the new subject and rebinds automatically. No other action is needed and no record is deleted.
- **Tenant Web UI**: a signed-in tenant now gets a working dashboard, server list and activity view built entirely from the session cookie and the tenant-allowed routes — no `?apikey=`, no calls to administrator-only or global-state endpoints (`/info`, `/routing`, `/docker/status`, `/connect`, `/stats/tokens`, `/security/overview`, `/onboarding/state`, core `/activity*`, core `/config`); those cards, chips and pages are hidden rather than issued-and-403'd — Settings stays an administrator-only page (its own personal-server and token management live under `/my/servers`, `/my/tokens`). Diagnostics and history use the tenant-scoped `/user/diagnostics` and `/user/activity` endpoints. The `access` map is edited by an administrator through Settings' Raw JSON tab and shown read-only as group chips on the admin server page and on `AdminUsers`.
- No action needed if you do not set `server_edition.access` at all: every tenant keeps seeing every `Shared` server exactly as before this release, on group grants alone. To start restricting tenants by IdP group, add the `access` block — from that point on, only a matching group entry (or `default_servers`) grants a shared server; a present-but-empty block denies every tenant until you populate it. Administrators are unaffected either way.
14 changes: 7 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,16 +426,16 @@ graph LR
classDef done fill:#1f7a1f,stroke:#0d3d0d,color:#ffffff;
classDef in_progress fill:#1f6feb,stroke:#0b3d91,color:#ffffff;
classDef todo fill:#6e7781,stroke:#3d4248,color:#ffffff;
class sso_pr_a_freeze_cut done;
class sso_pr_b_oidc_front_door in_progress;
class sso_pr_c_group_allowlist,sso_pr_d_audit_line todo;
class sso_pr_a_freeze_cut,sso_pr_b_oidc_front_door done;
class sso_pr_c_group_allowlist in_progress;
class sso_pr_d_audit_line todo;
```

| Task | Status | Refs |
| --- | --- | --- |
| PR-A freeze/cut latent code + config normaliser + per-owner token cap (US5, US6) | 🟢 Done | #1287 |
| PR-B generic OIDC provider + front door behind ingress + telemetry v13 (US2, US7) | 🔵 In progress | #1292 |
| PR-C one entitlement predicate, group grants, tenant Web UI session principal (US1, US4) | ⚪ Todo | — |
| PR-B generic OIDC provider + front door behind ingress + telemetry v13 (US2, US7) | 🟢 Done | #1292 |
| PR-C one entitlement predicate, group grants, tenant Web UI session principal (US1, US4) | 🔵 In progress | #1293 |
| PR-D attributable JSONL audit line + auth_event + config/doctor/metrics (US3) | ⚪ Todo | — |

</details>
Expand Down Expand Up @@ -896,7 +896,7 @@ graph LR
| Telemetry v7: honest funnel + churn instrumentation | In progress | P1 | — | [080-telemetry-v7-churn](./specs/080-telemetry-v7-churn/) | |
| MCP protocol upgrade to 2026-07-28 revision | In progress | P1 | 19/81 (23%) | [058-mcp-2026-upgrade](./specs/058-mcp-2026-upgrade/) | |
| Planning/docs truth automation | In progress | P2 | — | | |
| Spec 107 server edition SSO front door hardened for real IdPs | In progress | P2 | 69/126 (55%) | [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | |
| Spec 107 server edition SSO front door hardened for real IdPs | In progress | P2 | 100/126 (79%) | [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | |
| Discovery-quality eval harness (Spec 065 second half) | In progress | P3 | — | [065-evaluation-foundation](./specs/065-evaluation-foundation/) | |
| tpa-db: versioned TPA signature database for the offline scanner | Todo | P1 | — | [101-tpa-db](./specs/101-tpa-db/) | |
| Auto routing mode: budget-fitted tool surface per session (spec 104) | Todo | P1 | — | [104-auto-routing-mode](./specs/104-auto-routing-mode/) | |
Expand Down Expand Up @@ -1038,4 +1038,4 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—`
| [104-auto-routing-mode](./specs/104-auto-routing-mode/) | — | — |
| [105-agent-scope-hardening](./specs/105-agent-scope-hardening/) | `in-flight` | 12/109 (11%) |
| [106-security-residual-fixes](./specs/106-security-residual-fixes/) | `shipped` | 18/19 (95%) |
| [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | `in-flight` | 69/126 (55%) |
| [107-server-edition-sso-hardening](./specs/107-server-edition-sso-hardening/) | `in-flight` | 100/126 (79%) |
11 changes: 10 additions & 1 deletion docs/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ strings emitted at boot, by `PATCH /api/v1/config` and by `/config/apply`
| `oauth.groups_claim` | string | `"groups"` | Restart | `oidc` only. Name of the ID-token (then userinfo) claim carrying group memberships. Accepted shapes: a flat JSON array of strings or a single string; anything else is treated as absent. Compared as exact strings by the group → server map |
| `oauth.email_verified_policy` | `refuse_false` \| `require_true` \| `ignore` | `refuse_false` | Restart | `server_edition.oauth.email_verified_policy must be one of: refuse_false, require_true, ignore`. See the cost note below |
| `oauth.display_name` | string | provider family name | Restart | Login-button label; at most 64 characters (`server_edition.oauth.display_name must be at most 64 characters`). It is the **only** field returned by the public `GET /api/v1/auth/provider` probe (never the issuer, client id, tenant, scopes or domains) |
| `access` | object | absent (Shared-only semantics) | **Live** | Absent = today's behaviour, unchanged: every tenant sees every `shared` server. **Present = active**, with no silent allow-all: a tenant sees a shared server only through a group grant or `default_servers`; a user whose groups match no key and who has no default grant sees none. Read live through the config provider on every entitlement decision, so it hot-reloads (see [Group access map](/development/server-edition-multiuser-auth#group-access-map-server_editionaccess-and-entitlement-spec-107-pr-c)) |
| `access.group_servers` | map[string]string[] | `{}` | Live | Group value (compared exactly, case-sensitive) → admin-config server names, or `"*"` for every shared server. Non-empty only with `oauth.provider: "oidc"` — `server_edition.access.group_servers requires oauth.provider "oidc" (legacy providers yield no groups)`. A group with no map entry contributes nothing (silent, not an error) |
| `access.default_servers` | string[] | `[]` (no default grant) | Live | The grant for a user whose stored groups match no `group_servers` key. Absent, `null` and `[]` all mean "no default"; `"*"` is honoured here too |

An access-map entry (in `group_servers` or `default_servers`) that names no
configured server is accepted, not refused — it may be written ahead of the
server it names — but `mcpproxy doctor` reports it: `server_edition.access
names N server(s) that match no configured server (…): those entries grant
nothing until a server with that exact name exists`.

#### `email_verified_policy` — what each value costs

Expand Down Expand Up @@ -372,7 +381,7 @@ See [Upstream Servers](/configuration/upstream-servers) for detailed server conf

MCPProxy watches the configuration file for changes and automatically reloads when modifications are detected. No restart is required for most configuration changes.

Exceptions that require a restart include `listen`, `data_dir`, `api_key`, the TLS block, the three `http_*_timeout` options, and — in the Server edition — every `server_edition` key except `admin_emails` (see [Server Edition](#server-edition)). `trusted_proxies` is live.
Exceptions that require a restart include `listen`, `data_dir`, `api_key`, the TLS block, the three `http_*_timeout` options, and — in the Server edition — every `server_edition` key except `admin_emails` and `access` (see [Server Edition](#server-edition)). `trusted_proxies` is live.

## Environment Variable Overrides

Expand Down
Loading
Loading