You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part 3 of 4 for #159 (see the design review). Depends on parts 1 and 2 — this wires them into the token endpoint and completes the end-to-end flow. Re-scoped 2026-07-09 for CIMD-first (#161 rewrite): client identity resolves through #167's resolveClient() — a CIMD document for the agent fleet, or a stored (DCR) record as back-compat — the grant is agnostic to which.
Resource binding: accept the RFC 8707 resource parameter, defaulting to the configured canonical resource; exact-match, fail-closed (no prefix/wildcard) against what the client is registered for.
Issued token: existing RS256 mint path (mintTokenPair with issueRefresh: false); sub = client_id (RFC 9068 §2.2) — document that for this grant sub is a client identity, not an end user; never a refresh token.
TTL knob:mcp.clientCredentials.accessTokenTtl (default 300, i.e. ≤ 5 min per the issue's req 2); global accessTokenTtl default unchanged.
Audit/hooks: reuse oauth.mcp.token.issued; onMCPTokenIssued hook type gains 'client_credentials' — public plugin API, documented.
End-to-end: register (gated) → signed assertion → token → authenticated /mcp request through withMCPAuth. Plus: replayed assertion rejected; wrong-resource rejected; default resource honored; unregistered-resource rejected; TTL knob honored (expires_in, exp); no refresh_token in the response; client_id/iss mismatch rejected; audit event + hook fired with type: 'client_credentials'; discovery metadata updated; Basic/ops-auth cannot reach the grant without a valid assertion (issue req 6, at the endpoint layer).
Integration coverage inherited from #160 (must land here, not in #165): the mcp_assertion_jtis primitives are unit-tested against a mocked harper, so their real-storage behavior is only exercised once this grant wires them into an HTTP path against a real Harper child process. This grant's e2e integration test is the first thing to drive the table, so it must assert (a) the replay guard round-trips on real storage — a second presentation of the same assertion is rejected across a real DB read/write, not just the mock — and (b) @createdTime actually populates mcp_assertion_jtis.created_at with a Harper-assigned epoch-ms value (confirming the directive does more than load). #165 confirms the directive loads (schema boots in the integration fixture); it cannot confirm it populates because nothing writes the table until this grant exists.
Dependencies
Parts 1 and 2 (#160 — merged, #161 as re-scoped) and #167 (CIMD resolution layer).
Part 3 of 4 for #159 (see the design review). Depends on parts 1 and 2 — this wires them into the token endpoint and completes the end-to-end flow. Re-scoped 2026-07-09 for CIMD-first (#161 rewrite): client identity resolves through #167's
resolveClient()— a CIMD document for the agent fleet, or a stored (DCR) record as back-compat — the grant is agnostic to which.Scope
Token endpoint (
src/lib/mcp/token.ts) + config/discovery/docs:grant_type=client_credentialsbranch: requiresclient_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer+client_assertion;client_idrequired in the form body and must match assertioniss/sub; client resolved viaresolveClient()(feat: Client ID Metadata Documents with SSRF-guarded resolution + consent interstitial (#166) #167 — CIMD document or stored record), then client auth via part 1's verifier against the resolvedjwks;jtichecked through the replay store; the resolved client must carryprivate_key_jwt+client_credentials, and CIMD-resolved clients must pass the client_credentials (2/4): CIMD-first client resolution for private_key_jwt agents (DCR back-compat optional) #161allowedHostsgate.resourceparameter, defaulting to the configured canonical resource; exact-match, fail-closed (no prefix/wildcard) against what the client is registered for.mintTokenPairwithissueRefresh: false);sub=client_id(RFC 9068 §2.2) — document that for this grantsubis a client identity, not an end user; never a refresh token.mcp.clientCredentials.accessTokenTtl(default 300, i.e. ≤ 5 min per the issue's req 2); globalaccessTokenTtldefault unchanged.oauth.mcp.token.issued;onMCPTokenIssuedhooktypegains'client_credentials'— public plugin API, documented.grant_types_supported+=client_credentials;token_endpoint_auth_methods_supported+=private_key_jwt; advertisetoken_endpoint_auth_signing_alg_values_supported: ["EdDSA"]./mcpcall).Tests
End-to-end: register (gated) → signed assertion → token → authenticated
/mcprequest throughwithMCPAuth. Plus: replayed assertion rejected; wrong-resource rejected; default resource honored; unregistered-resource rejected; TTL knob honored (expires_in,exp); norefresh_tokenin the response;client_id/issmismatch rejected; audit event + hook fired withtype: 'client_credentials'; discovery metadata updated; Basic/ops-auth cannot reach the grant without a valid assertion (issue req 6, at the endpoint layer).Integration coverage inherited from #160 (must land here, not in #165): the
mcp_assertion_jtisprimitives are unit-tested against a mockedharper, so their real-storage behavior is only exercised once this grant wires them into an HTTP path against a real Harper child process. This grant's e2e integration test is the first thing to drive the table, so it must assert (a) the replay guard round-trips on real storage — a second presentation of the same assertion is rejected across a real DB read/write, not just the mock — and (b)@createdTimeactually populatesmcp_assertion_jtis.created_atwith a Harper-assigned epoch-ms value (confirming the directive does more than load). #165 confirms the directive loads (schema boots in the integration fixture); it cannot confirm it populates because nothing writes the table until this grant exists.Dependencies
Parts 1 and 2 (#160 — merged, #161 as re-scoped) and #167 (CIMD resolution layer).