toolcraft 0.0.125 → 0.0.127
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.
- package/composition.json +7 -2
- package/dist/composition.json +7 -2
- package/dist/http.d.ts +10 -0
- package/dist/http.js +40 -0
- package/node_modules/@poe-code/agent-defs/README.md +1 -1
- package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/index.js +1 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/pi.d.ts +2 -0
- package/node_modules/@poe-code/agent-defs/dist/agents/pi.js +14 -0
- package/node_modules/@poe-code/agent-defs/dist/index.d.ts +1 -1
- package/node_modules/@poe-code/agent-defs/dist/index.js +1 -1
- package/node_modules/@poe-code/agent-defs/dist/registry.js +2 -1
- package/node_modules/@poe-code/agent-defs/dist/types.d.ts +1 -1
- package/node_modules/mcp-oauth-server/LICENSE +21 -0
- package/node_modules/mcp-oauth-server/README.md +192 -0
- package/node_modules/mcp-oauth-server/dist/index.d.ts +157 -0
- package/node_modules/mcp-oauth-server/dist/index.js +641 -0
- package/node_modules/mcp-oauth-server/package.json +34 -0
- package/node_modules/tiny-stdio-mcp-server/dist/composition.json +1 -1
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +6 -4
package/composition.json
CHANGED
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"version": "0.0.1",
|
|
77
77
|
"license": "MIT"
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
"name": "mcp-oauth-server",
|
|
81
|
+
"version": "0.1.0",
|
|
82
|
+
"license": "MIT"
|
|
83
|
+
},
|
|
79
84
|
{
|
|
80
85
|
"name": "sisteransi",
|
|
81
86
|
"version": "1.0.5",
|
|
@@ -103,7 +108,7 @@
|
|
|
103
108
|
},
|
|
104
109
|
{
|
|
105
110
|
"name": "toolcraft",
|
|
106
|
-
"version": "0.0.
|
|
111
|
+
"version": "0.0.127",
|
|
107
112
|
"license": "MIT"
|
|
108
113
|
},
|
|
109
114
|
{
|
|
@@ -113,7 +118,7 @@
|
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
"name": "toolcraft-schema",
|
|
116
|
-
"version": "0.0.
|
|
121
|
+
"version": "0.0.127",
|
|
117
122
|
"license": "MIT"
|
|
118
123
|
},
|
|
119
124
|
{
|
package/dist/composition.json
CHANGED
|
@@ -76,6 +76,11 @@
|
|
|
76
76
|
"version": "0.0.1",
|
|
77
77
|
"license": "MIT"
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
"name": "mcp-oauth-server",
|
|
81
|
+
"version": "0.1.0",
|
|
82
|
+
"license": "MIT"
|
|
83
|
+
},
|
|
79
84
|
{
|
|
80
85
|
"name": "sisteransi",
|
|
81
86
|
"version": "1.0.5",
|
|
@@ -103,7 +108,7 @@
|
|
|
103
108
|
},
|
|
104
109
|
{
|
|
105
110
|
"name": "toolcraft",
|
|
106
|
-
"version": "0.0.
|
|
111
|
+
"version": "0.0.127",
|
|
107
112
|
"license": "MIT"
|
|
108
113
|
},
|
|
109
114
|
{
|
|
@@ -113,7 +118,7 @@
|
|
|
113
118
|
},
|
|
114
119
|
{
|
|
115
120
|
"name": "toolcraft-schema",
|
|
116
|
-
"version": "0.0.
|
|
121
|
+
"version": "0.0.127",
|
|
117
122
|
"license": "MIT"
|
|
118
123
|
},
|
|
119
124
|
{
|
package/dist/http.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import "./node-require-shim.js";
|
|
2
2
|
import { type HttpListenOptions, type HttpServer, type HttpServerHandle, type HttpToolContext, type HttpTransportOptions } from "tiny-http-mcp-server/server";
|
|
3
3
|
import type { Group } from "./index.js";
|
|
4
|
+
import type { OAuthAuthorizationServer } from "mcp-oauth-server";
|
|
4
5
|
import { type RunMCPOptions } from "./mcp.js";
|
|
5
6
|
export type ToolcraftHTTPContext = HttpToolContext;
|
|
6
7
|
export type ToolcraftHTTPServer = HttpServer;
|
|
@@ -13,7 +14,16 @@ export interface RunHTTPMCPOptions<TServices extends object = Record<string, unk
|
|
|
13
14
|
path?: string;
|
|
14
15
|
requestServices?(context: ToolcraftHTTPContext): Partial<TServices> | Promise<Partial<TServices>>;
|
|
15
16
|
}
|
|
17
|
+
export interface HTTPMCPAuthorizationOptions {
|
|
18
|
+
authorizationServer: Pick<OAuthAuthorizationServer, "issuer" | "verifyAccessToken">;
|
|
19
|
+
resource: string;
|
|
20
|
+
requiredScopes?: readonly string[];
|
|
21
|
+
scopesSupported?: readonly string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function createHTTPMCPAuthorization(options: HTTPMCPAuthorizationOptions): import("tiny-http-mcp-server/server").TinyHttpMcpServerOAuthOptions;
|
|
16
24
|
export declare function createHTTPMCPServer<TServices extends object = Record<string, unknown>>(roots: Group<TServices> | Group<TServices>[], options: RunHTTPMCPOptions<TServices>): Promise<ToolcraftHTTPServer>;
|
|
17
25
|
export declare function runHTTPMCP<TServices extends object = Record<string, unknown>>(roots: Group<TServices> | Group<TServices>[], options: RunHTTPMCPOptions<TServices>): Promise<ToolcraftHTTPServerHandle>;
|
|
18
26
|
export type { HttpListenOptions, HttpObservabilityEvent, HttpObservabilityOptions, RequestAuthInfo, Session, SessionStore, StreamableHttpTransportOptions, TinyHttpMcpServerOAuthOptions, TokenVerifier, VerifiedAccessToken } from "tiny-http-mcp-server/server";
|
|
19
27
|
export { createJwksTokenVerifier, TokenVerificationError } from "tiny-http-mcp-server/server";
|
|
28
|
+
export { createAuthorizationInteractionSecurity, createInMemoryAuthorizationServerStore, createOAuthAuthorizationServer, verifyAuthorizationInteractionCsrf } from "mcp-oauth-server";
|
|
29
|
+
export type { AuthorizationInteraction, AuthorizationInteractionSecurity, AuthorizationServerStore, OAuthAuthorizationServer, OAuthAuthorizationServerOptions, VerifiedAuthorizationServerToken } from "mcp-oauth-server";
|
package/dist/http.js
CHANGED
|
@@ -2,6 +2,45 @@ import "./node-require-shim.js";
|
|
|
2
2
|
import { createHttpServer } from "tiny-http-mcp-server/server";
|
|
3
3
|
import { createMCPServerForTransport } from "./mcp.js";
|
|
4
4
|
import { enableSourceMaps } from "./stack-trim.js";
|
|
5
|
+
function toVerifiedAccessToken(token, issuer, verified) {
|
|
6
|
+
return {
|
|
7
|
+
token,
|
|
8
|
+
issuer,
|
|
9
|
+
audience: [verified.resource],
|
|
10
|
+
scopes: [...verified.scopes],
|
|
11
|
+
expiresAt: verified.expiresAt,
|
|
12
|
+
claims: {
|
|
13
|
+
sub: verified.subject,
|
|
14
|
+
client_id: verified.clientId,
|
|
15
|
+
aud: verified.resource,
|
|
16
|
+
jti: verified.tokenId
|
|
17
|
+
},
|
|
18
|
+
subject: verified.subject,
|
|
19
|
+
clientId: verified.clientId
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createHTTPMCPAuthorization(options) {
|
|
23
|
+
const issuer = options.authorizationServer.issuer;
|
|
24
|
+
const resource = new URL(options.resource).href;
|
|
25
|
+
return {
|
|
26
|
+
resource,
|
|
27
|
+
authorizationServers: [issuer],
|
|
28
|
+
requiredScopes: options.requiredScopes,
|
|
29
|
+
scopesSupported: options.scopesSupported ?? options.requiredScopes,
|
|
30
|
+
verifier: {
|
|
31
|
+
async verify(input) {
|
|
32
|
+
if (input.authorizationServers.length !== 1 || input.authorizationServers[0] !== issuer) {
|
|
33
|
+
throw new Error("authorization server issuer does not match");
|
|
34
|
+
}
|
|
35
|
+
if (new URL(input.resource).href !== resource) {
|
|
36
|
+
throw new Error("protected resource does not match");
|
|
37
|
+
}
|
|
38
|
+
const verified = await options.authorizationServer.verifyAccessToken(input.token, resource);
|
|
39
|
+
return toVerifiedAccessToken(input.token, issuer, verified);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
5
44
|
function createTransportOptions(options, serverOptions) {
|
|
6
45
|
return {
|
|
7
46
|
...serverOptions,
|
|
@@ -60,3 +99,4 @@ export async function runHTTPMCP(roots, options) {
|
|
|
60
99
|
return server.listenHttp(createListenOptions(options));
|
|
61
100
|
}
|
|
62
101
|
export { createJwksTokenVerifier, TokenVerificationError } from "tiny-http-mcp-server/server";
|
|
102
|
+
export { createAuthorizationInteractionSecurity, createInMemoryAuthorizationServerStore, createOAuthAuthorizationServer, verifyAuthorizationInteractionCsrf } from "mcp-oauth-server";
|
|
@@ -23,7 +23,7 @@ const codex = allAgents.find((agent) => agent.id === "codex");
|
|
|
23
23
|
- `parseAgentSpecifier(input)`: parses `agent` or `agent:model` input.
|
|
24
24
|
- `formatAgentSpecifier(specifier)`: formats an agent specifier.
|
|
25
25
|
- `normalizeAgentId(input)`: normalizes the agent part through the registry.
|
|
26
|
-
- Agent definition exports such as `codexAgent`, `claudeCodeAgent`, and `
|
|
26
|
+
- Agent definition exports such as `codexAgent`, `claudeCodeAgent`, `geminiCliAgent`, and `piAgent`.
|
|
27
27
|
|
|
28
28
|
## Config Options
|
|
29
29
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const piAgent = {
|
|
2
|
+
id: "pi",
|
|
3
|
+
name: "pi",
|
|
4
|
+
aliases: ["pi-agent"],
|
|
5
|
+
label: "Pi",
|
|
6
|
+
summary: "Pi coding agent (spawn-only; uses local Pi auth/settings).",
|
|
7
|
+
binaryName: "pi",
|
|
8
|
+
branding: {
|
|
9
|
+
colors: {
|
|
10
|
+
dark: "#F2F2F2",
|
|
11
|
+
light: "#242424"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { AgentDefinition, ApiShapeId, OtelCaptureDefinition } from "./types.js";
|
|
2
2
|
export type { AgentSpecifier } from "./specifier.js";
|
|
3
|
-
export { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
3
|
+
export { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, piAgent, poeAgentAgent } from "./agents/index.js";
|
|
4
4
|
export { allAgents, resolveAgentId } from "./registry.js";
|
|
5
5
|
export { parseAgentSpecifier, formatAgentSpecifier, normalizeAgentId } from "./specifier.js";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
1
|
+
export { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, piAgent, poeAgentAgent } from "./agents/index.js";
|
|
2
2
|
export { allAgents, resolveAgentId } from "./registry.js";
|
|
3
3
|
export { parseAgentSpecifier, formatAgentSpecifier, normalizeAgentId } from "./specifier.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, poeAgentAgent } from "./agents/index.js";
|
|
1
|
+
import { claudeCodeAgent, claudeDesktopAgent, codexAgent, cursorAgent, geminiCliAgent, openCodeAgent, kimiAgent, gooseAgent, piAgent, poeAgentAgent } from "./agents/index.js";
|
|
2
2
|
function freezeAgent(agent) {
|
|
3
3
|
if (agent.aliases !== undefined) {
|
|
4
4
|
Object.freeze(agent.aliases);
|
|
@@ -25,6 +25,7 @@ export const allAgents = Object.freeze([
|
|
|
25
25
|
freezeAgent(openCodeAgent),
|
|
26
26
|
freezeAgent(kimiAgent),
|
|
27
27
|
freezeAgent(gooseAgent),
|
|
28
|
+
freezeAgent(piAgent),
|
|
28
29
|
freezeAgent(poeAgentAgent)
|
|
29
30
|
]);
|
|
30
31
|
const lookup = new Map();
|
|
@@ -13,7 +13,7 @@ export interface AgentDefinition {
|
|
|
13
13
|
binaryName?: string;
|
|
14
14
|
readonly apiShapes?: readonly ApiShapeId[];
|
|
15
15
|
readonly otelCapture?: OtelCaptureDefinition;
|
|
16
|
-
configPath
|
|
16
|
+
configPath?: string;
|
|
17
17
|
readonly configPaths?: {
|
|
18
18
|
readonly darwin: string;
|
|
19
19
|
readonly linux: string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Poe Platform
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# mcp-oauth-server
|
|
2
|
+
|
|
3
|
+
Production OAuth 2.1 authorization-server primitives for MCP applications.
|
|
4
|
+
|
|
5
|
+
The package provides:
|
|
6
|
+
|
|
7
|
+
- authorization code with mandatory PKCE `S256`
|
|
8
|
+
- RFC 8414 authorization-server metadata
|
|
9
|
+
- RFC 7591 dynamic registration for public MCP clients
|
|
10
|
+
- exact registered redirect URI validation
|
|
11
|
+
- RFC 8707 resource indicators and audience-bound JWT access tokens
|
|
12
|
+
- JWKS publication and short-lived `at+jwt` access tokens
|
|
13
|
+
- rotating refresh tokens with replay-triggered family revocation
|
|
14
|
+
- token and grant revocation
|
|
15
|
+
- asynchronous durable storage interfaces
|
|
16
|
+
- an application-owned browser authorization interaction
|
|
17
|
+
- CSRF, state, nonce, and secure cookie helpers
|
|
18
|
+
- a Toolcraft adapter through `createHTTPMCPAuthorization()` from `toolcraft/http`
|
|
19
|
+
|
|
20
|
+
## Availability
|
|
21
|
+
|
|
22
|
+
This package is bundled privately inside the published `toolcraft` package and
|
|
23
|
+
is supported through the `toolcraft/http` exports. It is not published as a
|
|
24
|
+
standalone npm package because that npm package name is owned outside this
|
|
25
|
+
repository.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
31
|
+
import { exportJWK } from "jose";
|
|
32
|
+
import {
|
|
33
|
+
createHTTPMCPAuthorization,
|
|
34
|
+
createOAuthAuthorizationServer,
|
|
35
|
+
runHTTPMCP,
|
|
36
|
+
type AuthorizationInteraction,
|
|
37
|
+
type AuthorizationServerStore
|
|
38
|
+
} from "toolcraft/http";
|
|
39
|
+
|
|
40
|
+
const { privateKey, publicKey } = generateKeyPairSync("ec", {
|
|
41
|
+
namedCurve: "P-256"
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const store: AuthorizationServerStore = createDurableStore();
|
|
45
|
+
const interaction: AuthorizationInteraction = {
|
|
46
|
+
async start({ transaction }) {
|
|
47
|
+
return renderApplicationAuthorizationPage(transaction);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const authorizationServer = createOAuthAuthorizationServer({
|
|
52
|
+
issuer: "https://auth.example.com",
|
|
53
|
+
resources: ["https://mcp.example.com/mcp"],
|
|
54
|
+
signingKey: {
|
|
55
|
+
algorithm: "ES256",
|
|
56
|
+
keyId: "2026-07",
|
|
57
|
+
privateKey,
|
|
58
|
+
publicJwk: await exportJWK(publicKey)
|
|
59
|
+
},
|
|
60
|
+
store,
|
|
61
|
+
interaction
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await runHTTPMCP(groups, {
|
|
65
|
+
oauth: createHTTPMCPAuthorization({
|
|
66
|
+
authorizationServer,
|
|
67
|
+
resource: "https://mcp.example.com/mcp",
|
|
68
|
+
requiredScopes: ["mcp.read"]
|
|
69
|
+
}),
|
|
70
|
+
requestServices(context) {
|
|
71
|
+
if (context.auth === undefined) throw new Error("OAuth subject required");
|
|
72
|
+
return {
|
|
73
|
+
babyDaybook: loadBabyDaybookSession(context.auth.subject)
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Route requests for `/.well-known/oauth-authorization-server`,
|
|
80
|
+
`/.well-known/jwks.json`, `/register`, `/authorize`, `/token`, and `/revoke`
|
|
81
|
+
to `authorizationServer.handle(request)` in the application HTTP framework.
|
|
82
|
+
|
|
83
|
+
## Application Authorization Interaction
|
|
84
|
+
|
|
85
|
+
`interaction.start({ request, transaction })` owns the browser experience. It
|
|
86
|
+
may render a login page, redirect to an upstream identity provider, or render a
|
|
87
|
+
one-time credential completion form. The application finishes the OAuth flow
|
|
88
|
+
only after authenticating its user:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const completion = await authorizationServer.completeAuthorization({
|
|
92
|
+
transactionId,
|
|
93
|
+
subject: applicationUserId,
|
|
94
|
+
scopes: approvedScopes
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return Response.redirect(completion.redirectUrl);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
For an Apple `intent://callback?...` completion flow:
|
|
101
|
+
|
|
102
|
+
1. Generate the application CSRF token, upstream state, and nonce with
|
|
103
|
+
`createAuthorizationInteractionSecurity()`.
|
|
104
|
+
2. Bind the upstream state to the pending authorization transaction in the
|
|
105
|
+
application's durable store.
|
|
106
|
+
3. Render the callback paste form with the hidden CSRF token and returned
|
|
107
|
+
`Set-Cookie` value.
|
|
108
|
+
4. On submission, bound the request body, verify CSRF with
|
|
109
|
+
`verifyAuthorizationInteractionCsrf()`, validate the exact intent callback
|
|
110
|
+
shape and state, and consume the application transaction once.
|
|
111
|
+
5. Exchange the one-time callback immediately. Never log or persist the
|
|
112
|
+
callback, password, authorization code, access token, or refresh token in
|
|
113
|
+
plaintext.
|
|
114
|
+
6. Persist only the encrypted upstream refresh session under the approved OAuth
|
|
115
|
+
subject, then call `completeAuthorization()`.
|
|
116
|
+
|
|
117
|
+
The application credential is deliberately absent from all package APIs so it
|
|
118
|
+
cannot accidentally enter OAuth transaction or token storage.
|
|
119
|
+
|
|
120
|
+
## Durable Storage
|
|
121
|
+
|
|
122
|
+
Production deployments must implement `AuthorizationServerStore`. The
|
|
123
|
+
interface covers:
|
|
124
|
+
|
|
125
|
+
- dynamic clients
|
|
126
|
+
- browser authorization transactions
|
|
127
|
+
- authorization grants
|
|
128
|
+
- one-time authorization codes
|
|
129
|
+
- access-token revocation records
|
|
130
|
+
- refresh-token families
|
|
131
|
+
|
|
132
|
+
`takeAuthorizationTransaction()`, `takeAuthorizationCode()`, and
|
|
133
|
+
`rotateRefreshToken()` must be atomic. Refresh replay detection depends on the
|
|
134
|
+
store retaining rotated token-family relationships and revoking the family
|
|
135
|
+
when an old token is presented again.
|
|
136
|
+
|
|
137
|
+
`createInMemoryAuthorizationServerStore()` is intended for tests and local
|
|
138
|
+
development only. It is not durable and must not be used in production.
|
|
139
|
+
|
|
140
|
+
## Subject Isolation
|
|
141
|
+
|
|
142
|
+
The access token `sub` claim is the only identity passed to Toolcraft as
|
|
143
|
+
`context.auth.subject`. Application service lookup must require that subject
|
|
144
|
+
and load only credentials stored under the same subject. Do not fall back to a
|
|
145
|
+
shared environment credential when a subject session is missing.
|
|
146
|
+
|
|
147
|
+
Grant and token revocation use opaque identifiers and token hashes. Raw
|
|
148
|
+
authorization codes, access tokens, and refresh tokens are never stored by the
|
|
149
|
+
provided store contract.
|
|
150
|
+
|
|
151
|
+
## Security Requirements
|
|
152
|
+
|
|
153
|
+
- Terminate TLS before exposing any endpoint publicly.
|
|
154
|
+
- Keep signing private keys outside source control and rotate keys with an
|
|
155
|
+
overlap period long enough for issued access tokens to expire.
|
|
156
|
+
- Use a durable store implementation with transactional or compare-and-swap
|
|
157
|
+
semantics for one-time consumption and refresh rotation.
|
|
158
|
+
- Keep access-token lifetimes short. The default is five minutes.
|
|
159
|
+
- Set restrictive CSP, `Referrer-Policy: no-referrer`, and `Cache-Control:
|
|
160
|
+
no-store` on application authorization pages.
|
|
161
|
+
- Require exact redirect URI matches. Do not add wildcard or prefix matching.
|
|
162
|
+
- Treat every OAuth subject as a separate security boundary.
|
|
163
|
+
- Encrypt application-owned upstream refresh tokens at rest with a
|
|
164
|
+
production key-management service.
|
|
165
|
+
- Redact authorization headers, cookies, codes, callbacks, and tokens from
|
|
166
|
+
logs and traces.
|
|
167
|
+
|
|
168
|
+
## Configuration
|
|
169
|
+
|
|
170
|
+
`createOAuthAuthorizationServer(options)` accepts:
|
|
171
|
+
|
|
172
|
+
| Option | Required | Default | Description |
|
|
173
|
+
| --- | --- | --- | --- |
|
|
174
|
+
| `issuer` | yes | none | HTTPS authorization-server issuer. Loopback HTTP is allowed for local development. |
|
|
175
|
+
| `resources` | yes | none | Exact protected resource identifiers accepted through the OAuth `resource` parameter. |
|
|
176
|
+
| `signingKey` | yes | none | Current `ES256` or `RS256` private key, public JWK, and key id. |
|
|
177
|
+
| `additionalPublicJwks` | no | `[]` | Previous public signing keys retained in JWKS during a key-rotation overlap. |
|
|
178
|
+
| `store` | yes | none | Durable `AuthorizationServerStore` implementation. |
|
|
179
|
+
| `interaction` | yes | none | Application-owned browser authorization hook. |
|
|
180
|
+
| `accessTokenTtlSeconds` | no | `300` | Signed access-token lifetime. |
|
|
181
|
+
| `authorizationCodeTtlSeconds` | no | `60` | One-time authorization-code lifetime. |
|
|
182
|
+
| `authorizationTransactionTtlSeconds` | no | `600` | Pending browser interaction lifetime. |
|
|
183
|
+
| `refreshTokenTtlSeconds` | no | `2592000` | Rotating refresh-token lifetime. |
|
|
184
|
+
| `maxRequestBodyBytes` | no | `65536` | Maximum DCR, token, and revocation request body size. |
|
|
185
|
+
| `now` | no | `Date.now` | Clock override for tests. |
|
|
186
|
+
| `randomToken` | no | cryptographic random | Opaque identifier generator override for tests. |
|
|
187
|
+
|
|
188
|
+
## Environment Variables
|
|
189
|
+
|
|
190
|
+
This package reads no environment variables. Signing keys, durable database
|
|
191
|
+
connections, encryption keys, issuer URLs, and protected resource identifiers
|
|
192
|
+
must be passed explicitly by the application.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type KeyObject } from "node:crypto";
|
|
2
|
+
import { type JWK } from "jose";
|
|
3
|
+
export interface OAuthClientRecord {
|
|
4
|
+
id: string;
|
|
5
|
+
redirectUris: readonly string[];
|
|
6
|
+
createdAt: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AuthorizationTransactionRecord {
|
|
9
|
+
id: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
redirectUri: string;
|
|
12
|
+
codeChallenge: string;
|
|
13
|
+
resource: string;
|
|
14
|
+
scopes: readonly string[];
|
|
15
|
+
state?: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
expiresAt: number;
|
|
18
|
+
}
|
|
19
|
+
export interface AuthorizationCodeRecord {
|
|
20
|
+
tokenHash: string;
|
|
21
|
+
grantId: string;
|
|
22
|
+
clientId: string;
|
|
23
|
+
subject: string;
|
|
24
|
+
redirectUri: string;
|
|
25
|
+
codeChallenge: string;
|
|
26
|
+
resource: string;
|
|
27
|
+
scopes: readonly string[];
|
|
28
|
+
expiresAt: number;
|
|
29
|
+
}
|
|
30
|
+
export interface AuthorizationGrantRecord {
|
|
31
|
+
id: string;
|
|
32
|
+
clientId: string;
|
|
33
|
+
subject: string;
|
|
34
|
+
resource: string;
|
|
35
|
+
scopes: readonly string[];
|
|
36
|
+
createdAt: number;
|
|
37
|
+
revokedAt?: number;
|
|
38
|
+
}
|
|
39
|
+
export interface RefreshTokenRecord {
|
|
40
|
+
tokenHash: string;
|
|
41
|
+
familyId: string;
|
|
42
|
+
grantId: string;
|
|
43
|
+
clientId: string;
|
|
44
|
+
subject: string;
|
|
45
|
+
resource: string;
|
|
46
|
+
scopes: readonly string[];
|
|
47
|
+
createdAt: number;
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
status: "active" | "rotated" | "revoked";
|
|
50
|
+
}
|
|
51
|
+
export interface AccessTokenRecord {
|
|
52
|
+
tokenHash: string;
|
|
53
|
+
tokenId: string;
|
|
54
|
+
grantId: string;
|
|
55
|
+
subject: string;
|
|
56
|
+
clientId: string;
|
|
57
|
+
resource: string;
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
revokedAt?: number;
|
|
60
|
+
}
|
|
61
|
+
export type RefreshTokenRotationResult = {
|
|
62
|
+
status: "rotated";
|
|
63
|
+
previous: RefreshTokenRecord;
|
|
64
|
+
} | {
|
|
65
|
+
status: "replay";
|
|
66
|
+
} | {
|
|
67
|
+
status: "invalid";
|
|
68
|
+
};
|
|
69
|
+
export interface AuthorizationServerStore {
|
|
70
|
+
putClient(client: OAuthClientRecord): Promise<void>;
|
|
71
|
+
getClient(clientId: string): Promise<OAuthClientRecord | undefined>;
|
|
72
|
+
putAuthorizationTransaction(transaction: AuthorizationTransactionRecord): Promise<void>;
|
|
73
|
+
takeAuthorizationTransaction(transactionId: string): Promise<AuthorizationTransactionRecord | undefined>;
|
|
74
|
+
putAuthorizationCode(code: AuthorizationCodeRecord): Promise<void>;
|
|
75
|
+
takeAuthorizationCode(tokenHash: string): Promise<AuthorizationCodeRecord | undefined>;
|
|
76
|
+
putGrant(grant: AuthorizationGrantRecord): Promise<void>;
|
|
77
|
+
getGrant(grantId: string): Promise<AuthorizationGrantRecord | undefined>;
|
|
78
|
+
putAccessToken(token: AccessTokenRecord): Promise<void>;
|
|
79
|
+
getAccessToken(tokenHash: string): Promise<AccessTokenRecord | undefined>;
|
|
80
|
+
putRefreshToken(token: RefreshTokenRecord): Promise<void>;
|
|
81
|
+
rotateRefreshToken(tokenHash: string, replacementTokenHash: string, now: number, expiresAt: number): Promise<RefreshTokenRotationResult>;
|
|
82
|
+
revokeToken(tokenHash: string, now: number): Promise<void>;
|
|
83
|
+
revokeGrant(grantId: string, now: number): Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
export interface AuthorizationInteractionStartContext {
|
|
86
|
+
request: Request;
|
|
87
|
+
transaction: AuthorizationTransactionRecord;
|
|
88
|
+
}
|
|
89
|
+
export interface AuthorizationInteraction {
|
|
90
|
+
start(context: AuthorizationInteractionStartContext): Promise<Response> | Response;
|
|
91
|
+
}
|
|
92
|
+
export interface OAuthAuthorizationServerSigningKey {
|
|
93
|
+
algorithm: "ES256" | "RS256";
|
|
94
|
+
keyId: string;
|
|
95
|
+
privateKey: KeyObject;
|
|
96
|
+
publicJwk: JWK;
|
|
97
|
+
}
|
|
98
|
+
export interface OAuthAuthorizationServerOptions {
|
|
99
|
+
issuer: string;
|
|
100
|
+
resources: readonly string[];
|
|
101
|
+
signingKey: OAuthAuthorizationServerSigningKey;
|
|
102
|
+
additionalPublicJwks?: readonly JWK[];
|
|
103
|
+
store: AuthorizationServerStore;
|
|
104
|
+
interaction: AuthorizationInteraction;
|
|
105
|
+
accessTokenTtlSeconds?: number;
|
|
106
|
+
authorizationCodeTtlSeconds?: number;
|
|
107
|
+
authorizationTransactionTtlSeconds?: number;
|
|
108
|
+
refreshTokenTtlSeconds?: number;
|
|
109
|
+
maxRequestBodyBytes?: number;
|
|
110
|
+
now?: () => number;
|
|
111
|
+
randomToken?: () => string;
|
|
112
|
+
}
|
|
113
|
+
export interface CompleteAuthorizationInput {
|
|
114
|
+
transactionId: string;
|
|
115
|
+
subject: string;
|
|
116
|
+
scopes?: readonly string[];
|
|
117
|
+
}
|
|
118
|
+
export interface CompleteAuthorizationResult {
|
|
119
|
+
redirectUrl: URL;
|
|
120
|
+
grantId: string;
|
|
121
|
+
}
|
|
122
|
+
export interface AuthorizationInteractionSecurity {
|
|
123
|
+
csrfToken: string;
|
|
124
|
+
state: string;
|
|
125
|
+
nonce: string;
|
|
126
|
+
setCookie: string;
|
|
127
|
+
}
|
|
128
|
+
export interface AuthorizationInteractionSecurityOptions {
|
|
129
|
+
cookieName?: string;
|
|
130
|
+
maxAgeSeconds?: number;
|
|
131
|
+
randomToken?: () => string;
|
|
132
|
+
}
|
|
133
|
+
export interface VerifyAuthorizationInteractionCsrfInput {
|
|
134
|
+
cookieHeader: string | null;
|
|
135
|
+
submittedToken: string;
|
|
136
|
+
cookieName?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface OAuthAuthorizationServer {
|
|
139
|
+
issuer: string;
|
|
140
|
+
handle(request: Request): Promise<Response>;
|
|
141
|
+
completeAuthorization(input: CompleteAuthorizationInput): Promise<CompleteAuthorizationResult>;
|
|
142
|
+
denyAuthorization(transactionId: string, error?: string): Promise<URL>;
|
|
143
|
+
revokeGrant(grantId: string): Promise<void>;
|
|
144
|
+
verifyAccessToken(token: string, resource: string): Promise<VerifiedAuthorizationServerToken>;
|
|
145
|
+
}
|
|
146
|
+
export interface VerifiedAuthorizationServerToken {
|
|
147
|
+
subject: string;
|
|
148
|
+
clientId: string;
|
|
149
|
+
resource: string;
|
|
150
|
+
scopes: readonly string[];
|
|
151
|
+
tokenId: string;
|
|
152
|
+
expiresAt: number;
|
|
153
|
+
}
|
|
154
|
+
export declare function createAuthorizationInteractionSecurity(options?: AuthorizationInteractionSecurityOptions): AuthorizationInteractionSecurity;
|
|
155
|
+
export declare function verifyAuthorizationInteractionCsrf(input: VerifyAuthorizationInteractionCsrfInput): boolean;
|
|
156
|
+
export declare function createInMemoryAuthorizationServerStore(): AuthorizationServerStore;
|
|
157
|
+
export declare function createOAuthAuthorizationServer(options: OAuthAuthorizationServerOptions): OAuthAuthorizationServer;
|