@foldrun/core 9.4.0 → 9.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type OAuthRole = "client" | "server";
|
|
2
|
+
export type OAuthStatus = "supported" | "planned" | "refused" | "not-needed";
|
|
3
|
+
/** A test that proves a row: the repo, the file in it, and the test's name
|
|
4
|
+
* as written (a substring of the title is enough). */
|
|
5
|
+
export interface OAuthProof {
|
|
6
|
+
repo: "foldrun-core" | "foldrun-platform" | "foldrun-web" | "foldrun-cli";
|
|
7
|
+
file: string;
|
|
8
|
+
test: string;
|
|
9
|
+
}
|
|
10
|
+
export interface OAuthCapability {
|
|
11
|
+
id: string;
|
|
12
|
+
role: OAuthRole;
|
|
13
|
+
/** What it is, in a person's words. */
|
|
14
|
+
title: string;
|
|
15
|
+
/** The RFC or spec that defines it. */
|
|
16
|
+
spec: string;
|
|
17
|
+
status: OAuthStatus;
|
|
18
|
+
/** How to use it (supported), what is coming (planned), or why not. */
|
|
19
|
+
note: string;
|
|
20
|
+
proof?: OAuthProof[];
|
|
21
|
+
}
|
|
22
|
+
export declare const OAUTH_CAPABILITIES: OAuthCapability[];
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Every kind of OAuth foldrun meets, in each role, and what foldrun does
|
|
2
|
+
// about it: supported (with the tests that prove it), planned, refused (with
|
|
3
|
+
// the reason a person sees), or not needed. The single source the customer
|
|
4
|
+
// page `foldrun-docs/oauth.md` is generated from (`node scripts/oauth-doc.mjs`)
|
|
5
|
+
// — the same pattern as frontmatter-fields.ts. tests/oauth-capabilities.test.ts
|
|
6
|
+
// fails when a supported row names a test that does not exist, or the page is
|
|
7
|
+
// stale.
|
|
8
|
+
//
|
|
9
|
+
// The roles:
|
|
10
|
+
// client an agent reaches Google, Slack, a bank — foldrun holds the token
|
|
11
|
+
// server an AI app signs in to /api/mcp — foldrun issues the token
|
|
12
|
+
const core = (file, test) => ({ repo: "foldrun-core", file, test });
|
|
13
|
+
const platform = (file, test) => ({ repo: "foldrun-platform", file, test });
|
|
14
|
+
const web = (file, test) => ({ repo: "foldrun-web", file, test });
|
|
15
|
+
const cli = (file, test) => ({ repo: "foldrun-cli", file, test });
|
|
16
|
+
const KC = "tests/e2e-oauth-keycloak.test.ts";
|
|
17
|
+
export const OAUTH_CAPABILITIES = [
|
|
18
|
+
// ------------------------------------------------------------ client
|
|
19
|
+
{
|
|
20
|
+
id: "client.authorization-code-pkce", role: "client", title: "Authorization code with PKCE (S256)", spec: "RFC 6749 §4.1, RFC 7636",
|
|
21
|
+
status: "supported", note: "Settings → Connections or `foldrun connect`. PKCE is on for every connection.",
|
|
22
|
+
proof: [platform("tests/oauth-connect.test.ts", "PKCE: on by default"), platform(KC, "authorization code + PKCE S256 + PAR"), cli("tests/cli-connect.test.ts", "connect: PKCE by default")],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "client.refresh-rotation", role: "client", title: "Refresh, and refresh-token rotation", spec: "RFC 6749 §6, RFC 9700 §2.2.2",
|
|
26
|
+
status: "supported", note: "Refreshed before every use; a rotated refresh token replaces the stored one.",
|
|
27
|
+
proof: [core("tests/secret-types.test.ts", "a rotated refresh token is saved where the old one was"), core("tests/oauth-provider-fixtures.test.ts", "rotating refresh tokens: every refresh returns a new one"), platform(KC, "the refresh works and rotates")],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "client.client-credentials", role: "client", title: "Client credentials (machine to machine)", spec: "RFC 6749 §4.4",
|
|
31
|
+
status: "supported", note: "The \"OAuth — client credentials\" connection, or `foldrun secrets set NAME --kind m2m`.",
|
|
32
|
+
proof: [platform(KC, "client credentials (machine to machine)")],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "client.secret-post", role: "client", title: "Client secret in the form body", spec: "RFC 6749 §2.3.1",
|
|
36
|
+
status: "supported", note: "The default (`token_auth: client_secret_post`).",
|
|
37
|
+
proof: [core("tests/oauth-token.test.ts", "client_secret_post (and absent with a secret)"), platform(KC, "client_secret_post")],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "client.secret-basic", role: "client", title: "Client secret as HTTP Basic", spec: "RFC 6749 §2.3.1",
|
|
41
|
+
status: "supported", note: "`token_auth: client_secret_basic`, or `--token-auth basic`. Notion, Spotify, Reddit and Zoom need it.",
|
|
42
|
+
proof: [core("tests/oauth-token.test.ts", "a Basic-only token endpoint"), platform("tests/oauth-connect.test.ts", "token_auth client_secret_basic"), platform(KC, "client_secret_basic"), cli("tests/cli-connect.test.ts", "connect --token-auth basic")],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "client.public", role: "client", title: "Public app (no secret), PKCE only", spec: "RFC 9700 §2.1.1",
|
|
46
|
+
status: "supported", note: "Leave the client secret empty.",
|
|
47
|
+
proof: [platform("tests/oauth-connect.test.ts", "a public app (no secret) connects with PKCE"), platform(KC, "a public app (no secret)")],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "client.discovery", role: "client", title: "Metadata discovery from an issuer", spec: "RFC 8414, OpenID Connect Discovery",
|
|
51
|
+
status: "supported", note: "The Issuer field's Discover, or `--issuer`.",
|
|
52
|
+
proof: [core("tests/oauth-discovery-revoke.test.ts", "an RFC 8414 server"), core("tests/oauth-discovery-revoke.test.ts", "refused: a document naming another issuer"), platform(KC, "discovery (RFC 8414 / OIDC)"), cli("tests/cli-connect.test.ts", "connect --issuer: the metadata fills the URLs")],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "client.iss-parameter", role: "client", title: "Issuer in the authorization response (mix-up defence)", spec: "RFC 9207",
|
|
56
|
+
status: "supported", note: "With an issuer, the `iss` that comes back must be it.",
|
|
57
|
+
proof: [platform("tests/oauth-connect.test.ts", "iss: a pinned issuer must come back exactly"), platform(KC, "a callback whose iss is not the realm is refused"), cli("tests/cli-connect.test.ts", "a callback whose iss is another server is refused")],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "client.revocation", role: "client", title: "Revocation when a connection ends", spec: "RFC 7009",
|
|
61
|
+
status: "supported", note: "Deleting a connection, or reconnecting it with another app, revokes the old grant.",
|
|
62
|
+
proof: [core("tests/oauth-discovery-revoke.test.ts", "revoking: the refresh token, then the cached access token"), web("tests/secrets-revoke.test.ts", "deleting revokes"), platform(KC, "revocation (RFC 7009)")],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "client.par", role: "client", title: "Pushed authorization requests", spec: "RFC 9126",
|
|
66
|
+
status: "supported", note: "Used whenever the provider has a PAR endpoint.",
|
|
67
|
+
proof: [platform("tests/oauth-connect.test.ts", "PAR (RFC 9126)"), platform(KC, "authorization code + PKCE S256 + PAR"), cli("tests/cli-connect.test.ts", "with a PAR endpoint")],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "client.token-response", role: "client", title: "Token responses as providers send them", spec: "RFC 6749 §5.1",
|
|
71
|
+
status: "supported", note: "JSON or form-encoded, `expires_in` as a string, Slack's `authed_user`.",
|
|
72
|
+
proof: [core("tests/oauth-token.test.ts", "Slack: a user token alone"), core("tests/oauth-token.test.ts", "a form-encoded answer"), core("tests/oauth-provider-fixtures.test.ts", "no expires_in; instance_url the agent needs"), core("tests/oauth-provider-fixtures.test.ts", "form-encoded unless asked for JSON")],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: "client.jwt-bearer", role: "client", title: "JWT bearer grant (service accounts)", spec: "RFC 7523 §2.1",
|
|
76
|
+
status: "supported", note: "The service-account connection (`--kind service-account`).",
|
|
77
|
+
proof: [core("tests/secret-types.test.ts", "service account with a subject")],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: "client.rar", role: "client", title: "Rich authorization requests", spec: "RFC 9396",
|
|
81
|
+
status: "supported", note: "`authorization_details` as an extra authorize parameter (a JSON value).",
|
|
82
|
+
proof: [platform("tests/oauth-connect.test.ts", "authorization_details (RFC 9396)")],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "client.private-key-jwt", role: "client", title: "Signed client assertion (private_key_jwt)", spec: "RFC 7523 §2.2",
|
|
86
|
+
status: "planned", note: "Part E: Entra certificates, Okta, FAPI.",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "client.device", role: "client", title: "Device authorization", spec: "RFC 8628",
|
|
90
|
+
status: "planned", note: "Part F: `foldrun connect --device`.",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "client.token-exchange", role: "client", title: "Token exchange", spec: "RFC 8693",
|
|
94
|
+
status: "planned", note: "Part I: a `token-exchange` secret kind. Cloud STS is covered today by the keyless identity kind.",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "client.dpop", role: "client", title: "DPoP sender-constrained tokens", spec: "RFC 9449",
|
|
98
|
+
status: "planned", note: "Part H: Bluesky/atproto, Okta DPoP.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "client.mtls", role: "client", title: "Mutual-TLS client auth and bound tokens", spec: "RFC 8705",
|
|
102
|
+
status: "planned", note: "Part H: the client-cert secret at the token endpoint.",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "client.ciba", role: "client", title: "Client-initiated backchannel authentication", spec: "OpenID CIBA",
|
|
106
|
+
status: "not-needed", note: "No provider an agent needs uses it.",
|
|
107
|
+
},
|
|
108
|
+
// ------------------------------------------------------------ server (/api/mcp)
|
|
109
|
+
{
|
|
110
|
+
id: "server.authorization-code-pkce", role: "server", title: "Authorization code with PKCE (S256)", spec: "OAuth 2.1, MCP authorization",
|
|
111
|
+
status: "supported", note: "How claude.ai and other AI apps sign in to /api/mcp.",
|
|
112
|
+
proof: [web("tests/mcp-oauth.test.ts", "the code: PKCE, once, for its own app and redirect"), web("tests/mcp-sdk-oauth.test.ts", "the MCP SDK: 401 → discovery → registration")],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "server.refresh-rotation", role: "server", title: "Refresh rotation, reuse revokes", spec: "OAuth 2.1 §4.3.1",
|
|
116
|
+
status: "supported", note: "A refresh token used twice ends the connection.",
|
|
117
|
+
proof: [web("tests/mcp-oauth.test.ts", "refresh rotates; a refresh token used twice ends the connection")],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "server.client-auth", role: "server", title: "Client secret (Basic or body), or a public app", spec: "RFC 6749 §2.3.1",
|
|
121
|
+
status: "supported", note: "Registered apps choose `none`, `client_secret_post` or `client_secret_basic`.",
|
|
122
|
+
proof: [web("tests/mcp-oauth.test.ts", "a confidential app proves itself with its secret")],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: "server.metadata", role: "server", title: "Authorization server and resource metadata", spec: "RFC 8414, RFC 9728",
|
|
126
|
+
status: "supported", note: "`/.well-known/oauth-authorization-server` and `/.well-known/oauth-protected-resource`.",
|
|
127
|
+
proof: [web("tests/mcp-oauth.test.ts", "discovery: the resource names its server")],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "server.registration", role: "server", title: "Dynamic registration and client metadata documents", spec: "RFC 7591, CIMD",
|
|
131
|
+
status: "supported", note: "An app registers itself, or names itself by an https URL.",
|
|
132
|
+
proof: [web("tests/mcp-oauth.test.ts", "registration: https or loopback redirect URIs only")],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "server.resource-indicators", role: "server", title: "Resource indicators", spec: "RFC 8707",
|
|
136
|
+
status: "supported", note: "Tokens are bound to /api/mcp.",
|
|
137
|
+
proof: [web("tests/mcp-oauth.test.ts", "the access token opens /api/mcp and its tools")],
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "server.iss-parameter", role: "server", title: "Issuer in the authorization response", spec: "RFC 9207",
|
|
141
|
+
status: "supported", note: "Every answer carries `iss`.",
|
|
142
|
+
proof: [web("tests/mcp-oauth.test.ts", "the approval page's request")],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "server.revocation", role: "server", title: "Revocation endpoint", spec: "RFC 7009",
|
|
146
|
+
status: "supported", note: "`POST /api/mcp/oauth/revoke`: an app ends its own connection.",
|
|
147
|
+
proof: [web("tests/mcp-oauth.test.ts", "revocation (RFC 7009): advertised")],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: "server.client-credentials", role: "server", title: "Client credentials for machine clients", spec: "RFC 6749 §4.4",
|
|
151
|
+
status: "planned", note: "Part G: machine MCP clients an admin registers.",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "server.private-key-jwt", role: "server", title: "Signed client assertion (private_key_jwt)", spec: "RFC 7523 §2.2",
|
|
155
|
+
status: "planned", note: "Part E: apps that declare `jwks` or `jwks_uri`.",
|
|
156
|
+
},
|
|
157
|
+
// ------------------------------------------------------------ refused, both roles
|
|
158
|
+
{
|
|
159
|
+
id: "refused.implicit", role: "client", title: "Implicit grant", spec: "RFC 9700 §2.1.2",
|
|
160
|
+
status: "refused", note: "Tokens in the browser's URL leak; OAuth 2.1 removes it.",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "refused.password", role: "client", title: "Resource owner password grant", spec: "RFC 9700 §2.4",
|
|
164
|
+
status: "refused", note: "foldrun never holds a person's password for another service.",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
id: "refused.pkce-plain", role: "server", title: "PKCE `plain`", spec: "RFC 9700 §2.1.1",
|
|
168
|
+
status: "refused", note: "Only S256 is accepted: `plain` protects nothing if the request is seen.",
|
|
169
|
+
proof: [web("tests/mcp-oauth.test.ts", "the approval page's request")],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "refused.wildcard-redirect", role: "server", title: "Wildcard or prefix redirect matching", spec: "RFC 9700 §4.1",
|
|
173
|
+
status: "refused", note: "Redirect URIs match exactly (a loopback port may vary).",
|
|
174
|
+
proof: [web("tests/mcp-oauth.test.ts", "the approval page's request")],
|
|
175
|
+
},
|
|
176
|
+
];
|
package/package.json
CHANGED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Every kind of OAuth foldrun meets, in each role, and what foldrun does
|
|
2
|
+
// about it: supported (with the tests that prove it), planned, refused (with
|
|
3
|
+
// the reason a person sees), or not needed. The single source the customer
|
|
4
|
+
// page `foldrun-docs/oauth.md` is generated from (`node scripts/oauth-doc.mjs`)
|
|
5
|
+
// — the same pattern as frontmatter-fields.ts. tests/oauth-capabilities.test.ts
|
|
6
|
+
// fails when a supported row names a test that does not exist, or the page is
|
|
7
|
+
// stale.
|
|
8
|
+
//
|
|
9
|
+
// The roles:
|
|
10
|
+
// client an agent reaches Google, Slack, a bank — foldrun holds the token
|
|
11
|
+
// server an AI app signs in to /api/mcp — foldrun issues the token
|
|
12
|
+
|
|
13
|
+
export type OAuthRole = "client" | "server";
|
|
14
|
+
export type OAuthStatus = "supported" | "planned" | "refused" | "not-needed";
|
|
15
|
+
|
|
16
|
+
/** A test that proves a row: the repo, the file in it, and the test's name
|
|
17
|
+
* as written (a substring of the title is enough). */
|
|
18
|
+
export interface OAuthProof {
|
|
19
|
+
repo: "foldrun-core" | "foldrun-platform" | "foldrun-web" | "foldrun-cli";
|
|
20
|
+
file: string;
|
|
21
|
+
test: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OAuthCapability {
|
|
25
|
+
id: string;
|
|
26
|
+
role: OAuthRole;
|
|
27
|
+
/** What it is, in a person's words. */
|
|
28
|
+
title: string;
|
|
29
|
+
/** The RFC or spec that defines it. */
|
|
30
|
+
spec: string;
|
|
31
|
+
status: OAuthStatus;
|
|
32
|
+
/** How to use it (supported), what is coming (planned), or why not. */
|
|
33
|
+
note: string;
|
|
34
|
+
proof?: OAuthProof[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const core = (file: string, test: string): OAuthProof => ({ repo: "foldrun-core", file, test });
|
|
38
|
+
const platform = (file: string, test: string): OAuthProof => ({ repo: "foldrun-platform", file, test });
|
|
39
|
+
const web = (file: string, test: string): OAuthProof => ({ repo: "foldrun-web", file, test });
|
|
40
|
+
const cli = (file: string, test: string): OAuthProof => ({ repo: "foldrun-cli", file, test });
|
|
41
|
+
const KC = "tests/e2e-oauth-keycloak.test.ts";
|
|
42
|
+
|
|
43
|
+
export const OAUTH_CAPABILITIES: OAuthCapability[] = [
|
|
44
|
+
// ------------------------------------------------------------ client
|
|
45
|
+
{
|
|
46
|
+
id: "client.authorization-code-pkce", role: "client", title: "Authorization code with PKCE (S256)", spec: "RFC 6749 §4.1, RFC 7636",
|
|
47
|
+
status: "supported", note: "Settings → Connections or `foldrun connect`. PKCE is on for every connection.",
|
|
48
|
+
proof: [platform("tests/oauth-connect.test.ts", "PKCE: on by default"), platform(KC, "authorization code + PKCE S256 + PAR"), cli("tests/cli-connect.test.ts", "connect: PKCE by default")],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "client.refresh-rotation", role: "client", title: "Refresh, and refresh-token rotation", spec: "RFC 6749 §6, RFC 9700 §2.2.2",
|
|
52
|
+
status: "supported", note: "Refreshed before every use; a rotated refresh token replaces the stored one.",
|
|
53
|
+
proof: [core("tests/secret-types.test.ts", "a rotated refresh token is saved where the old one was"), core("tests/oauth-provider-fixtures.test.ts", "rotating refresh tokens: every refresh returns a new one"), platform(KC, "the refresh works and rotates")],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "client.client-credentials", role: "client", title: "Client credentials (machine to machine)", spec: "RFC 6749 §4.4",
|
|
57
|
+
status: "supported", note: "The \"OAuth — client credentials\" connection, or `foldrun secrets set NAME --kind m2m`.",
|
|
58
|
+
proof: [platform(KC, "client credentials (machine to machine)")],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "client.secret-post", role: "client", title: "Client secret in the form body", spec: "RFC 6749 §2.3.1",
|
|
62
|
+
status: "supported", note: "The default (`token_auth: client_secret_post`).",
|
|
63
|
+
proof: [core("tests/oauth-token.test.ts", "client_secret_post (and absent with a secret)"), platform(KC, "client_secret_post")],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "client.secret-basic", role: "client", title: "Client secret as HTTP Basic", spec: "RFC 6749 §2.3.1",
|
|
67
|
+
status: "supported", note: "`token_auth: client_secret_basic`, or `--token-auth basic`. Notion, Spotify, Reddit and Zoom need it.",
|
|
68
|
+
proof: [core("tests/oauth-token.test.ts", "a Basic-only token endpoint"), platform("tests/oauth-connect.test.ts", "token_auth client_secret_basic"), platform(KC, "client_secret_basic"), cli("tests/cli-connect.test.ts", "connect --token-auth basic")],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "client.public", role: "client", title: "Public app (no secret), PKCE only", spec: "RFC 9700 §2.1.1",
|
|
72
|
+
status: "supported", note: "Leave the client secret empty.",
|
|
73
|
+
proof: [platform("tests/oauth-connect.test.ts", "a public app (no secret) connects with PKCE"), platform(KC, "a public app (no secret)")],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "client.discovery", role: "client", title: "Metadata discovery from an issuer", spec: "RFC 8414, OpenID Connect Discovery",
|
|
77
|
+
status: "supported", note: "The Issuer field's Discover, or `--issuer`.",
|
|
78
|
+
proof: [core("tests/oauth-discovery-revoke.test.ts", "an RFC 8414 server"), core("tests/oauth-discovery-revoke.test.ts", "refused: a document naming another issuer"), platform(KC, "discovery (RFC 8414 / OIDC)"), cli("tests/cli-connect.test.ts", "connect --issuer: the metadata fills the URLs")],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "client.iss-parameter", role: "client", title: "Issuer in the authorization response (mix-up defence)", spec: "RFC 9207",
|
|
82
|
+
status: "supported", note: "With an issuer, the `iss` that comes back must be it.",
|
|
83
|
+
proof: [platform("tests/oauth-connect.test.ts", "iss: a pinned issuer must come back exactly"), platform(KC, "a callback whose iss is not the realm is refused"), cli("tests/cli-connect.test.ts", "a callback whose iss is another server is refused")],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "client.revocation", role: "client", title: "Revocation when a connection ends", spec: "RFC 7009",
|
|
87
|
+
status: "supported", note: "Deleting a connection, or reconnecting it with another app, revokes the old grant.",
|
|
88
|
+
proof: [core("tests/oauth-discovery-revoke.test.ts", "revoking: the refresh token, then the cached access token"), web("tests/secrets-revoke.test.ts", "deleting revokes"), platform(KC, "revocation (RFC 7009)")],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "client.par", role: "client", title: "Pushed authorization requests", spec: "RFC 9126",
|
|
92
|
+
status: "supported", note: "Used whenever the provider has a PAR endpoint.",
|
|
93
|
+
proof: [platform("tests/oauth-connect.test.ts", "PAR (RFC 9126)"), platform(KC, "authorization code + PKCE S256 + PAR"), cli("tests/cli-connect.test.ts", "with a PAR endpoint")],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "client.token-response", role: "client", title: "Token responses as providers send them", spec: "RFC 6749 §5.1",
|
|
97
|
+
status: "supported", note: "JSON or form-encoded, `expires_in` as a string, Slack's `authed_user`.",
|
|
98
|
+
proof: [core("tests/oauth-token.test.ts", "Slack: a user token alone"), core("tests/oauth-token.test.ts", "a form-encoded answer"), core("tests/oauth-provider-fixtures.test.ts", "no expires_in; instance_url the agent needs"), core("tests/oauth-provider-fixtures.test.ts", "form-encoded unless asked for JSON")],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "client.jwt-bearer", role: "client", title: "JWT bearer grant (service accounts)", spec: "RFC 7523 §2.1",
|
|
102
|
+
status: "supported", note: "The service-account connection (`--kind service-account`).",
|
|
103
|
+
proof: [core("tests/secret-types.test.ts", "service account with a subject")],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "client.rar", role: "client", title: "Rich authorization requests", spec: "RFC 9396",
|
|
107
|
+
status: "supported", note: "`authorization_details` as an extra authorize parameter (a JSON value).",
|
|
108
|
+
proof: [platform("tests/oauth-connect.test.ts", "authorization_details (RFC 9396)")],
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: "client.private-key-jwt", role: "client", title: "Signed client assertion (private_key_jwt)", spec: "RFC 7523 §2.2",
|
|
112
|
+
status: "planned", note: "Part E: Entra certificates, Okta, FAPI.",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: "client.device", role: "client", title: "Device authorization", spec: "RFC 8628",
|
|
116
|
+
status: "planned", note: "Part F: `foldrun connect --device`.",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "client.token-exchange", role: "client", title: "Token exchange", spec: "RFC 8693",
|
|
120
|
+
status: "planned", note: "Part I: a `token-exchange` secret kind. Cloud STS is covered today by the keyless identity kind.",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "client.dpop", role: "client", title: "DPoP sender-constrained tokens", spec: "RFC 9449",
|
|
124
|
+
status: "planned", note: "Part H: Bluesky/atproto, Okta DPoP.",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "client.mtls", role: "client", title: "Mutual-TLS client auth and bound tokens", spec: "RFC 8705",
|
|
128
|
+
status: "planned", note: "Part H: the client-cert secret at the token endpoint.",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "client.ciba", role: "client", title: "Client-initiated backchannel authentication", spec: "OpenID CIBA",
|
|
132
|
+
status: "not-needed", note: "No provider an agent needs uses it.",
|
|
133
|
+
},
|
|
134
|
+
// ------------------------------------------------------------ server (/api/mcp)
|
|
135
|
+
{
|
|
136
|
+
id: "server.authorization-code-pkce", role: "server", title: "Authorization code with PKCE (S256)", spec: "OAuth 2.1, MCP authorization",
|
|
137
|
+
status: "supported", note: "How claude.ai and other AI apps sign in to /api/mcp.",
|
|
138
|
+
proof: [web("tests/mcp-oauth.test.ts", "the code: PKCE, once, for its own app and redirect"), web("tests/mcp-sdk-oauth.test.ts", "the MCP SDK: 401 → discovery → registration")],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "server.refresh-rotation", role: "server", title: "Refresh rotation, reuse revokes", spec: "OAuth 2.1 §4.3.1",
|
|
142
|
+
status: "supported", note: "A refresh token used twice ends the connection.",
|
|
143
|
+
proof: [web("tests/mcp-oauth.test.ts", "refresh rotates; a refresh token used twice ends the connection")],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: "server.client-auth", role: "server", title: "Client secret (Basic or body), or a public app", spec: "RFC 6749 §2.3.1",
|
|
147
|
+
status: "supported", note: "Registered apps choose `none`, `client_secret_post` or `client_secret_basic`.",
|
|
148
|
+
proof: [web("tests/mcp-oauth.test.ts", "a confidential app proves itself with its secret")],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "server.metadata", role: "server", title: "Authorization server and resource metadata", spec: "RFC 8414, RFC 9728",
|
|
152
|
+
status: "supported", note: "`/.well-known/oauth-authorization-server` and `/.well-known/oauth-protected-resource`.",
|
|
153
|
+
proof: [web("tests/mcp-oauth.test.ts", "discovery: the resource names its server")],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "server.registration", role: "server", title: "Dynamic registration and client metadata documents", spec: "RFC 7591, CIMD",
|
|
157
|
+
status: "supported", note: "An app registers itself, or names itself by an https URL.",
|
|
158
|
+
proof: [web("tests/mcp-oauth.test.ts", "registration: https or loopback redirect URIs only")],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: "server.resource-indicators", role: "server", title: "Resource indicators", spec: "RFC 8707",
|
|
162
|
+
status: "supported", note: "Tokens are bound to /api/mcp.",
|
|
163
|
+
proof: [web("tests/mcp-oauth.test.ts", "the access token opens /api/mcp and its tools")],
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "server.iss-parameter", role: "server", title: "Issuer in the authorization response", spec: "RFC 9207",
|
|
167
|
+
status: "supported", note: "Every answer carries `iss`.",
|
|
168
|
+
proof: [web("tests/mcp-oauth.test.ts", "the approval page's request")],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "server.revocation", role: "server", title: "Revocation endpoint", spec: "RFC 7009",
|
|
172
|
+
status: "supported", note: "`POST /api/mcp/oauth/revoke`: an app ends its own connection.",
|
|
173
|
+
proof: [web("tests/mcp-oauth.test.ts", "revocation (RFC 7009): advertised")],
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: "server.client-credentials", role: "server", title: "Client credentials for machine clients", spec: "RFC 6749 §4.4",
|
|
177
|
+
status: "planned", note: "Part G: machine MCP clients an admin registers.",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "server.private-key-jwt", role: "server", title: "Signed client assertion (private_key_jwt)", spec: "RFC 7523 §2.2",
|
|
181
|
+
status: "planned", note: "Part E: apps that declare `jwks` or `jwks_uri`.",
|
|
182
|
+
},
|
|
183
|
+
// ------------------------------------------------------------ refused, both roles
|
|
184
|
+
{
|
|
185
|
+
id: "refused.implicit", role: "client", title: "Implicit grant", spec: "RFC 9700 §2.1.2",
|
|
186
|
+
status: "refused", note: "Tokens in the browser's URL leak; OAuth 2.1 removes it.",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "refused.password", role: "client", title: "Resource owner password grant", spec: "RFC 9700 §2.4",
|
|
190
|
+
status: "refused", note: "foldrun never holds a person's password for another service.",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "refused.pkce-plain", role: "server", title: "PKCE `plain`", spec: "RFC 9700 §2.1.1",
|
|
194
|
+
status: "refused", note: "Only S256 is accepted: `plain` protects nothing if the request is seen.",
|
|
195
|
+
proof: [web("tests/mcp-oauth.test.ts", "the approval page's request")],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "refused.wildcard-redirect", role: "server", title: "Wildcard or prefix redirect matching", spec: "RFC 9700 §4.1",
|
|
199
|
+
status: "refused", note: "Redirect URIs match exactly (a loopback port may vary).",
|
|
200
|
+
proof: [web("tests/mcp-oauth.test.ts", "the approval page's request")],
|
|
201
|
+
},
|
|
202
|
+
];
|