cauthy-auth 0.15.0 → 0.16.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.
- package/README.md +8 -1
- package/dist/cli.js +118 -10
- package/dist/mcp.d.mts +36 -1
- package/dist/mcp.d.ts +36 -1
- package/dist/mcp.js +71 -5
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.mjs +70 -5
- package/dist/mcp.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ import 'cauthy-auth/styles.css'
|
|
|
101
101
|
|
|
102
102
|
Or draw them with your own `components` and `classNames`, and pass `unstyled`. The dashboard can start the look from your stylesheet (shadcn/ui and Tailwind, Stackmyth, or variables of your own); `npx cauthy-auth theme --css src/app/globals.css` reads it the same way from a terminal.
|
|
103
103
|
|
|
104
|
-
Connect your application to Cauthy in one step, from Cauthy's dashboard ("Connect a System") or from a terminal. In the application's folder, `init` reads its framework (Next.js, TanStack Start, SvelteKit, Express, Hono, Fastify), makes the client, writes every variable to its env file, `CAUTHY_SECRET` included, and lays down the route and a sign-in page, never over a file that is there:
|
|
104
|
+
Connect your application to Cauthy in one step, from Cauthy's dashboard ("Connect a System") or from a terminal. In the application's folder, `init` reads its framework (Next.js, TanStack Start, React Router or Remix, SvelteKit, Vite + React, Express, Hono, Fastify), makes the client, writes every variable to its env file, `CAUTHY_SECRET` included, and lays down the route and a sign-in page, never over a file that is there:
|
|
105
105
|
|
|
106
106
|
```bash
|
|
107
107
|
npx cauthy-auth init
|
|
@@ -131,6 +131,13 @@ const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:
|
|
|
131
131
|
app.use(auth.middleware()) // or auth.protect(handler) for Request/Response
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
+
When that server calls another of your servers for the person, it trades the agent's token for one for that server (token exchange, RFC 8693). Delegate the call and create the server's credentials in the dashboard first:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const auth = mcpAuth({ resource, clientId: process.env.CAUTHY_MCP_CLIENT_ID, clientSecret: process.env.CAUTHY_MCP_CLIENT_SECRET })
|
|
138
|
+
const { accessToken } = await auth.exchange(req.agent, 'https://api.example.com', ['orders:read'])
|
|
139
|
+
```
|
|
140
|
+
|
|
134
141
|
The full guide is in Cauthy's documentation, under "Auth SDK".
|
|
135
142
|
|
|
136
143
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -57,33 +57,41 @@ var import_node_path = require("path");
|
|
|
57
57
|
var FRAMEWORK_NAMES = {
|
|
58
58
|
next: "Next.js",
|
|
59
59
|
"tanstack-start": "TanStack Start",
|
|
60
|
+
"react-router": "React Router",
|
|
60
61
|
sveltekit: "SvelteKit",
|
|
61
62
|
express: "Express",
|
|
62
63
|
hono: "Hono",
|
|
63
|
-
fastify: "Fastify"
|
|
64
|
+
fastify: "Fastify",
|
|
65
|
+
"vite-react": "Vite + React"
|
|
64
66
|
};
|
|
65
67
|
function detect(pkg, has) {
|
|
66
68
|
const deps = { ...pkg.devDependencies, ...pkg.dependencies };
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
+
const remix = Boolean(deps["@remix-run/react"] || deps["@remix-run/node"]);
|
|
70
|
+
const framework = deps.next ? "next" : deps["@tanstack/react-start"] ? "tanstack-start" : deps["@react-router/dev"] || remix ? "react-router" : deps["@sveltejs/kit"] ? "sveltekit" : deps.vite && deps.react && !deps.express && !deps.fastify ? "vite-react" : deps.hono ? "hono" : deps.fastify ? "fastify" : deps.express ? "express" : null;
|
|
71
|
+
const port = framework === "sveltekit" || framework === "vite-react" || framework === "react-router" && !(remix && !deps.vite) ? 5173 : 3e3;
|
|
69
72
|
let root = has("src") ? "src" : "";
|
|
70
73
|
let nextPagesOnly = false;
|
|
71
74
|
if (framework === "next") {
|
|
72
75
|
root = has("src/app") ? "src" : has("app") ? "" : has("src/pages") ? "src" : "";
|
|
73
76
|
nextPagesOnly = !has("src/app") && !has("app");
|
|
74
77
|
}
|
|
75
|
-
if (framework === "tanstack-start" || framework === "sveltekit") root = "src";
|
|
78
|
+
if (framework === "tanstack-start" || framework === "sveltekit" || framework === "vite-react") root = "src";
|
|
79
|
+
if (framework === "react-router") root = "app";
|
|
76
80
|
const major = /(\d+)/.exec(deps.next ?? "");
|
|
81
|
+
const needs = ["cauthy-auth", ...framework === "vite-react" ? ["hono", "@hono/node-server"] : []];
|
|
77
82
|
return {
|
|
78
83
|
framework,
|
|
79
84
|
...nextPagesOnly ? { nextPagesOnly } : {},
|
|
80
85
|
root,
|
|
86
|
+
...framework === "react-router" && remix ? { remix } : {},
|
|
87
|
+
...framework === "react-router" ? { fileRoutes: remix || Boolean(deps["@react-router/fs-routes"]) } : {},
|
|
81
88
|
typescript: has("tsconfig.json") || Boolean(deps.typescript),
|
|
82
89
|
esm: pkg.type === "module",
|
|
83
90
|
...framework === "next" && major ? { nextMajor: Number(major[1]) } : {},
|
|
84
91
|
devUrl: `http://localhost:${port}`,
|
|
85
92
|
system: systemName(pkg.name),
|
|
86
93
|
installed: Boolean(deps["cauthy-auth"]),
|
|
94
|
+
toInstall: needs.filter((name) => !deps[name]),
|
|
87
95
|
packageManager: has("pnpm-lock.yaml") ? "pnpm" : has("yarn.lock") ? "yarn" : has("bun.lockb") || has("bun.lock") ? "bun" : "npm"
|
|
88
96
|
};
|
|
89
97
|
}
|
|
@@ -100,9 +108,9 @@ function importPath(from, to) {
|
|
|
100
108
|
function scaffold(d) {
|
|
101
109
|
const ext = d.typescript ? "ts" : "js";
|
|
102
110
|
const jsx = d.typescript ? "tsx" : "jsx";
|
|
103
|
-
const install = `${d.packageManager === "npm" ? "npm install" : `${d.packageManager} add`}
|
|
111
|
+
const install = `${d.packageManager === "npm" ? "npm install" : `${d.packageManager} add`} ${d.toInstall.join(" ")}`;
|
|
104
112
|
const dev = `${d.packageManager === "npm" ? "npm run" : d.packageManager} dev`;
|
|
105
|
-
const steps = d.
|
|
113
|
+
const steps = d.toInstall.length === 0 ? [] : [`Install the ${d.toInstall.length === 1 ? "package" : "packages"}: ${install}`];
|
|
106
114
|
const files = [];
|
|
107
115
|
const signIn = `${d.devUrl}/sign-in`;
|
|
108
116
|
const hosted = `${d.devUrl}/api/auth/login`;
|
|
@@ -159,6 +167,49 @@ export const Route = createFileRoute('/sign-in')({ component: SignInPage })
|
|
|
159
167
|
steps.push(`Start the application: ${dev}`, `Open ${signIn}`);
|
|
160
168
|
return { files, steps, envFile: ".env", open: signIn };
|
|
161
169
|
}
|
|
170
|
+
case "react-router": {
|
|
171
|
+
const authFile = join(d.root, `auth.server.${ext}`);
|
|
172
|
+
files.push({ path: authFile, content: PLAIN_AUTH });
|
|
173
|
+
const route = join(d.root, "routes", `api.auth.$.${ext}`);
|
|
174
|
+
const param = d.typescript ? "{ request }: { request: Request }" : "{ request }";
|
|
175
|
+
files.push({ path: route, content: `// Serves cauthy-auth: sign-in, return, session, sign-out.
|
|
176
|
+
import { auth } from '${importPath(route, authFile)}'
|
|
177
|
+
|
|
178
|
+
export const loader = (${param}) => auth.handler(request)
|
|
179
|
+
export const action = (${param}) => auth.handler(request)
|
|
180
|
+
` });
|
|
181
|
+
const page = join(d.root, "routes", `sign-in.${jsx}`);
|
|
182
|
+
files.push({ path: page, content: reactSignInPage("", "export default function SignInPage") });
|
|
183
|
+
if (!d.fileRoutes) {
|
|
184
|
+
const rel = (p) => import_node_path.posix.relative(d.root, p);
|
|
185
|
+
steps.push(`Add the two routes to ${join(d.root, `routes.${ext}`)}:
|
|
186
|
+
|
|
187
|
+
route('api/auth/*', '${rel(route)}'),
|
|
188
|
+
route('sign-in', '${rel(page)}'),`);
|
|
189
|
+
}
|
|
190
|
+
steps.push(`Start the application: ${dev}`, `Open ${signIn}`);
|
|
191
|
+
return { files, steps, envFile: ".env", open: signIn };
|
|
192
|
+
}
|
|
193
|
+
case "vite-react": {
|
|
194
|
+
const app = join("server", `app.${ext}`);
|
|
195
|
+
files.push({ path: join("server", `auth.${ext}`), content: PLAIN_AUTH });
|
|
196
|
+
files.push({ path: app, content: viteApp(d.typescript) });
|
|
197
|
+
files.push({ path: join("server", `index.${ext}`), content: VITE_SERVER });
|
|
198
|
+
files.push({ path: join("server", `vite.${ext}`), content: viteDev(d.typescript, `/${app}`) });
|
|
199
|
+
const page = join(d.root, `SignInPage.${jsx}`);
|
|
200
|
+
files.push({ path: page, content: reactSignInPage("", "export function SignInPage") });
|
|
201
|
+
const run = d.typescript ? "npx tsx server/index.ts" : "node server/index.js";
|
|
202
|
+
steps.push(
|
|
203
|
+
`Add the plugin that serves /api while developing to vite.config.${ext}:
|
|
204
|
+
|
|
205
|
+
import { cauthyDev } from './server/vite'
|
|
206
|
+
plugins: [react(), cauthyDev()]`,
|
|
207
|
+
`Start the application: ${dev}`,
|
|
208
|
+
`Open ${hosted}: Cauthy's sign-in page, and back. For a screen of your own, show <SignInPage /> from ${page} at /sign-in.`,
|
|
209
|
+
`In production, build the pages (vite build) and run ${run}: it serves them and /api.`
|
|
210
|
+
);
|
|
211
|
+
return { files, steps, envFile: ".env", open: hosted };
|
|
212
|
+
}
|
|
162
213
|
case "sveltekit": {
|
|
163
214
|
const authFile = join(d.root, "lib/server", `auth.${ext}`);
|
|
164
215
|
files.push({ path: authFile, content: PLAIN_AUTH });
|
|
@@ -231,6 +282,62 @@ const { createCauthy } = require('cauthy-auth')
|
|
|
231
282
|
|
|
232
283
|
exports.auth = createCauthy()
|
|
233
284
|
`;
|
|
285
|
+
function viteApp(ts) {
|
|
286
|
+
return `// The application's server side: cauthy-auth's routes under /api/auth, and
|
|
287
|
+
// c.get('session') on every other /api request. While developing it runs
|
|
288
|
+
// inside Vite (server/vite); in production server/index serves it with the
|
|
289
|
+
// built pages.
|
|
290
|
+
import { Hono } from 'hono'
|
|
291
|
+
import { cauthyHono, requireSession${ts ? ", type CauthyVariables" : ""} } from 'cauthy-auth/hono'
|
|
292
|
+
import { auth } from './auth.js'
|
|
293
|
+
|
|
294
|
+
export const app = new Hono${ts ? "<{ Variables: CauthyVariables }>" : ""}()
|
|
295
|
+
app.use('/api/*', cauthyHono(auth))
|
|
296
|
+
|
|
297
|
+
// An API only a signed-in person may call: a 401 for anybody else.
|
|
298
|
+
app.get('/api/me', requireSession, (c) => c.json(c.get('session')${ts ? "!" : ""}.user))
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
var VITE_SERVER = `// The application in production: the pages Vite built, and the API.
|
|
302
|
+
import { serve } from '@hono/node-server'
|
|
303
|
+
import { serveStatic } from '@hono/node-server/serve-static'
|
|
304
|
+
import { app } from './app.js'
|
|
305
|
+
|
|
306
|
+
app.use('/*', serveStatic({ root: './dist' }))
|
|
307
|
+
// Any other path is a page of the single-page app.
|
|
308
|
+
app.get('*', serveStatic({ path: './dist/index.html' }))
|
|
309
|
+
|
|
310
|
+
const port = Number(process.env.PORT ?? 3000)
|
|
311
|
+
serve({ fetch: app.fetch, port }, () => console.log(\`http://localhost:\${port}\`))
|
|
312
|
+
`;
|
|
313
|
+
function viteDev(ts, app) {
|
|
314
|
+
return `// While developing, Vite serves the pages and hands /api to the server's
|
|
315
|
+
// app, on the same address: no proxy, and Cauthy sends people back to the
|
|
316
|
+
// address the browser is on.
|
|
317
|
+
//
|
|
318
|
+
// import { cauthyDev } from './server/vite'
|
|
319
|
+
// plugins: [react(), cauthyDev()]
|
|
320
|
+
import { getRequestListener } from '@hono/node-server'
|
|
321
|
+
import { loadEnv${ts ? ", type Plugin" : ""} } from 'vite'
|
|
322
|
+
|
|
323
|
+
export function cauthyDev()${ts ? ": Plugin" : ""} {
|
|
324
|
+
return {
|
|
325
|
+
name: 'cauthy-dev',
|
|
326
|
+
configureServer(server) {
|
|
327
|
+
// The variables in the env file, which createCauthy() reads from the environment.
|
|
328
|
+
Object.assign(process.env, loadEnv(server.config.mode, server.config.envDir || server.config.root, 'CAUTHY_'))
|
|
329
|
+
server.middlewares.use((req, res, next) => {
|
|
330
|
+
if (!req.url?.startsWith('/api/')) return next()
|
|
331
|
+
// Loaded through Vite: a change to the server's files applies on the next request.
|
|
332
|
+
server.ssrLoadModule('${app}')
|
|
333
|
+
.then(({ app }) => getRequestListener(app.fetch)(req, res))
|
|
334
|
+
.catch(next)
|
|
335
|
+
})
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
234
341
|
function reactSignInPage(head, declaration, tail = "") {
|
|
235
342
|
return `${head}// The application's own sign-in page. <SignIn /> is the ready-made form: the
|
|
236
343
|
// workspace's providers, passkeys, the second step and a sign-in link, drawn
|
|
@@ -912,9 +1019,10 @@ var INIT_HELP = `cauthy-auth init [options]
|
|
|
912
1019
|
|
|
913
1020
|
Connects an application to Cauthy and leaves signing in working. Run it in
|
|
914
1021
|
the application's folder: it reads which framework the application is made
|
|
915
|
-
with (Next.js, TanStack Start,
|
|
916
|
-
client in Cauthy, writes the variables to
|
|
917
|
-
route and a sign-in page. A file that is
|
|
1022
|
+
with (Next.js, TanStack Start, React Router or Remix, SvelteKit, Vite + React,
|
|
1023
|
+
Express, Hono, Fastify), makes its client in Cauthy, writes the variables to
|
|
1024
|
+
its env file, and lays down the route and a sign-in page. A file that is
|
|
1025
|
+
already there is never written over.
|
|
918
1026
|
|
|
919
1027
|
--system <name> The system's name: lowercase letters, digits, - or _.
|
|
920
1028
|
Default: the package's name.
|
|
@@ -976,7 +1084,7 @@ ${INIT_HELP}`);
|
|
|
976
1084
|
const system = given["--system"] ?? found?.system;
|
|
977
1085
|
const appUrl = given["--app-url"] ?? found?.devUrl;
|
|
978
1086
|
if (!system || !appUrl) fail("no package.json here: give --system and --app-url\n\n" + INIT_HELP);
|
|
979
|
-
if (found?.framework) process.stdout.write(`Found ${FRAMEWORK_NAMES[found.framework]}.
|
|
1087
|
+
if (found?.framework) process.stdout.write(`Found ${found.remix ? "Remix" : FRAMEWORK_NAMES[found.framework]}.
|
|
980
1088
|
`);
|
|
981
1089
|
const target = given["--write"] ?? plan?.envFile;
|
|
982
1090
|
let had = "";
|
package/dist/mcp.d.mts
CHANGED
|
@@ -16,6 +16,11 @@ interface AgentIdentity {
|
|
|
16
16
|
expiresAt: string;
|
|
17
17
|
/** The token as it arrived. */
|
|
18
18
|
token: string;
|
|
19
|
+
/**
|
|
20
|
+
* Set when the token came by token exchange: the server that called this
|
|
21
|
+
* one for the person (the `act` claim's subject).
|
|
22
|
+
*/
|
|
23
|
+
actor?: string;
|
|
19
24
|
}
|
|
20
25
|
interface McpAuthOptions {
|
|
21
26
|
/** This server's address, as registered in Cauthy. Default: `CAUTHY_MCP_RESOURCE`. */
|
|
@@ -43,6 +48,29 @@ interface McpAuthOptions {
|
|
|
43
48
|
};
|
|
44
49
|
/** The fetch to call Cauthy with. Default: the global one. */
|
|
45
50
|
fetch?: typeof fetch;
|
|
51
|
+
/**
|
|
52
|
+
* This server's client_id and secret at Cauthy, for token exchange (AI
|
|
53
|
+
* agents → the server → Server credentials). Defaults:
|
|
54
|
+
* `CAUTHY_MCP_CLIENT_ID`, `CAUTHY_MCP_CLIENT_SECRET`.
|
|
55
|
+
*/
|
|
56
|
+
clientId?: string;
|
|
57
|
+
clientSecret?: string;
|
|
58
|
+
}
|
|
59
|
+
/** What a token exchange answers. */
|
|
60
|
+
interface ExchangedToken {
|
|
61
|
+
/** The token for the other server, as the person. Send it as `Authorization: Bearer`. */
|
|
62
|
+
accessToken: string;
|
|
63
|
+
/** What it allows there. */
|
|
64
|
+
scopes: string[];
|
|
65
|
+
/** UTC, ISO 8601. Never later than the agent's own token. */
|
|
66
|
+
expiresAt: string;
|
|
67
|
+
}
|
|
68
|
+
/** A token exchange Cauthy refused, with the OAuth error it answered. */
|
|
69
|
+
declare class CauthyExchangeError extends Error {
|
|
70
|
+
readonly status: number;
|
|
71
|
+
readonly code: string;
|
|
72
|
+
readonly description: string;
|
|
73
|
+
constructor(status: number, code: string, description: string);
|
|
46
74
|
}
|
|
47
75
|
/** A token refused: `status` is 401 (no token, or not a good one) or 403 (a scope missing). */
|
|
48
76
|
declare class McpAuthError extends Error {
|
|
@@ -89,6 +117,13 @@ interface McpAuth {
|
|
|
89
117
|
* what the MCP TypeScript SDK's transport hands its tools as `authInfo`.
|
|
90
118
|
*/
|
|
91
119
|
middleware(needs?: string[]): (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void;
|
|
120
|
+
/**
|
|
121
|
+
* Trades the agent's token this server received for one for another of the
|
|
122
|
+
* workspace's servers, to call it for the person (RFC 8693). Needs this
|
|
123
|
+
* server's `clientId` and `clientSecret`. Answers are kept until shortly
|
|
124
|
+
* before they expire. Throws `CauthyExchangeError` when Cauthy refuses.
|
|
125
|
+
*/
|
|
126
|
+
exchange(from: string | AgentIdentity, target: string, scopes?: string[]): Promise<ExchangedToken>;
|
|
92
127
|
}
|
|
93
128
|
interface NodeRequest {
|
|
94
129
|
method?: string;
|
|
@@ -111,4 +146,4 @@ declare function canonicalResource(raw: string): string;
|
|
|
111
146
|
/** Protects an MCP server with Cauthy. */
|
|
112
147
|
declare function mcpAuth(options?: McpAuthOptions): McpAuth;
|
|
113
148
|
|
|
114
|
-
export { type AgentIdentity, type McpAuth, McpAuthError, type McpAuthInfo, type McpAuthOptions, canonicalResource, mcpAuth };
|
|
149
|
+
export { type AgentIdentity, CauthyExchangeError, type ExchangedToken, type McpAuth, McpAuthError, type McpAuthInfo, type McpAuthOptions, canonicalResource, mcpAuth };
|
package/dist/mcp.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ interface AgentIdentity {
|
|
|
16
16
|
expiresAt: string;
|
|
17
17
|
/** The token as it arrived. */
|
|
18
18
|
token: string;
|
|
19
|
+
/**
|
|
20
|
+
* Set when the token came by token exchange: the server that called this
|
|
21
|
+
* one for the person (the `act` claim's subject).
|
|
22
|
+
*/
|
|
23
|
+
actor?: string;
|
|
19
24
|
}
|
|
20
25
|
interface McpAuthOptions {
|
|
21
26
|
/** This server's address, as registered in Cauthy. Default: `CAUTHY_MCP_RESOURCE`. */
|
|
@@ -43,6 +48,29 @@ interface McpAuthOptions {
|
|
|
43
48
|
};
|
|
44
49
|
/** The fetch to call Cauthy with. Default: the global one. */
|
|
45
50
|
fetch?: typeof fetch;
|
|
51
|
+
/**
|
|
52
|
+
* This server's client_id and secret at Cauthy, for token exchange (AI
|
|
53
|
+
* agents → the server → Server credentials). Defaults:
|
|
54
|
+
* `CAUTHY_MCP_CLIENT_ID`, `CAUTHY_MCP_CLIENT_SECRET`.
|
|
55
|
+
*/
|
|
56
|
+
clientId?: string;
|
|
57
|
+
clientSecret?: string;
|
|
58
|
+
}
|
|
59
|
+
/** What a token exchange answers. */
|
|
60
|
+
interface ExchangedToken {
|
|
61
|
+
/** The token for the other server, as the person. Send it as `Authorization: Bearer`. */
|
|
62
|
+
accessToken: string;
|
|
63
|
+
/** What it allows there. */
|
|
64
|
+
scopes: string[];
|
|
65
|
+
/** UTC, ISO 8601. Never later than the agent's own token. */
|
|
66
|
+
expiresAt: string;
|
|
67
|
+
}
|
|
68
|
+
/** A token exchange Cauthy refused, with the OAuth error it answered. */
|
|
69
|
+
declare class CauthyExchangeError extends Error {
|
|
70
|
+
readonly status: number;
|
|
71
|
+
readonly code: string;
|
|
72
|
+
readonly description: string;
|
|
73
|
+
constructor(status: number, code: string, description: string);
|
|
46
74
|
}
|
|
47
75
|
/** A token refused: `status` is 401 (no token, or not a good one) or 403 (a scope missing). */
|
|
48
76
|
declare class McpAuthError extends Error {
|
|
@@ -89,6 +117,13 @@ interface McpAuth {
|
|
|
89
117
|
* what the MCP TypeScript SDK's transport hands its tools as `authInfo`.
|
|
90
118
|
*/
|
|
91
119
|
middleware(needs?: string[]): (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void;
|
|
120
|
+
/**
|
|
121
|
+
* Trades the agent's token this server received for one for another of the
|
|
122
|
+
* workspace's servers, to call it for the person (RFC 8693). Needs this
|
|
123
|
+
* server's `clientId` and `clientSecret`. Answers are kept until shortly
|
|
124
|
+
* before they expire. Throws `CauthyExchangeError` when Cauthy refuses.
|
|
125
|
+
*/
|
|
126
|
+
exchange(from: string | AgentIdentity, target: string, scopes?: string[]): Promise<ExchangedToken>;
|
|
92
127
|
}
|
|
93
128
|
interface NodeRequest {
|
|
94
129
|
method?: string;
|
|
@@ -111,4 +146,4 @@ declare function canonicalResource(raw: string): string;
|
|
|
111
146
|
/** Protects an MCP server with Cauthy. */
|
|
112
147
|
declare function mcpAuth(options?: McpAuthOptions): McpAuth;
|
|
113
148
|
|
|
114
|
-
export { type AgentIdentity, type McpAuth, McpAuthError, type McpAuthInfo, type McpAuthOptions, canonicalResource, mcpAuth };
|
|
149
|
+
export { type AgentIdentity, CauthyExchangeError, type ExchangedToken, type McpAuth, McpAuthError, type McpAuthInfo, type McpAuthOptions, canonicalResource, mcpAuth };
|
package/dist/mcp.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/mcp.ts
|
|
21
21
|
var mcp_exports = {};
|
|
22
22
|
__export(mcp_exports, {
|
|
23
|
+
CauthyExchangeError: () => CauthyExchangeError,
|
|
23
24
|
McpAuthError: () => McpAuthError,
|
|
24
25
|
canonicalResource: () => canonicalResource,
|
|
25
26
|
mcpAuth: () => mcpAuth
|
|
@@ -40,6 +41,18 @@ function keysOf(issuer) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
// src/mcp.ts
|
|
44
|
+
var CauthyExchangeError = class extends Error {
|
|
45
|
+
constructor(status, code, description) {
|
|
46
|
+
super(`cauthy-auth: token exchange refused (${code}): ${description}`);
|
|
47
|
+
this.status = status;
|
|
48
|
+
this.code = code;
|
|
49
|
+
this.description = description;
|
|
50
|
+
this.name = "CauthyExchangeError";
|
|
51
|
+
}
|
|
52
|
+
status;
|
|
53
|
+
code;
|
|
54
|
+
description;
|
|
55
|
+
};
|
|
43
56
|
var McpAuthError = class extends Error {
|
|
44
57
|
constructor(status, code, description) {
|
|
45
58
|
super(`cauthy-auth: ${description}`);
|
|
@@ -74,6 +87,9 @@ function bearer(from) {
|
|
|
74
87
|
const header = (from instanceof Headers ? from : from.headers).get("authorization") ?? "";
|
|
75
88
|
return /^Bearer\s+/i.test(header) ? header.replace(/^Bearer\s+/i, "").trim() : "";
|
|
76
89
|
}
|
|
90
|
+
function isActor(v) {
|
|
91
|
+
return typeof v === "object" && v !== null && typeof v.sub === "string";
|
|
92
|
+
}
|
|
77
93
|
function quoted(v) {
|
|
78
94
|
return `"${v.replace(/["\\]/g, "")}"`;
|
|
79
95
|
}
|
|
@@ -134,10 +150,10 @@ function mcpAuth(options = {}) {
|
|
|
134
150
|
} catch (err) {
|
|
135
151
|
throw new McpAuthError(401, "invalid_token", err instanceof Error ? err.message : "the token is not valid");
|
|
136
152
|
}
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
153
|
+
const clientId2 = typeof claims.client_id === "string" ? claims.client_id : "";
|
|
154
|
+
if (clientId2 === "") throw new McpAuthError(401, "invalid_token", "not a token issued to an agent");
|
|
139
155
|
const scopes = typeof claims.scope === "string" ? claims.scope.split(" ").filter(Boolean) : [];
|
|
140
|
-
if (introspect && !await stillAllowed(token, introspect.cacheMs)) throw new McpAuthError(401, "invalid_token", "the
|
|
156
|
+
if (introspect && !await stillAllowed(token, introspect.cacheMs)) throw new McpAuthError(401, "invalid_token", "the token is no longer allowed: the agent was disconnected, or its access withdrawn");
|
|
141
157
|
const missing = [...required, ...needs].filter((s) => !scopes.includes(s));
|
|
142
158
|
if (missing.length > 0) throw new McpAuthError(403, "insufficient_scope", `this needs ${missing.join(" ")}`);
|
|
143
159
|
return {
|
|
@@ -146,10 +162,58 @@ function mcpAuth(options = {}) {
|
|
|
146
162
|
...typeof claims.email === "string" && claims.email !== "" ? { email: claims.email } : {},
|
|
147
163
|
roles: Array.isArray(claims.roles) ? claims.roles.filter((r) => typeof r === "string") : [],
|
|
148
164
|
scopes,
|
|
149
|
-
clientId,
|
|
165
|
+
clientId: clientId2,
|
|
150
166
|
expiresAt: new Date(claims.exp * 1e3).toISOString(),
|
|
151
|
-
token
|
|
167
|
+
token,
|
|
168
|
+
...isActor(claims.act) ? { actor: claims.act.sub } : {}
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
const clientId = options.clientId ?? env("CAUTHY_MCP_CLIENT_ID") ?? "";
|
|
172
|
+
const clientSecret = options.clientSecret ?? env("CAUTHY_MCP_CLIENT_SECRET") ?? "";
|
|
173
|
+
const exchanged = /* @__PURE__ */ new Map();
|
|
174
|
+
const exchange = async (from, target, scopes = []) => {
|
|
175
|
+
if (clientId === "" || clientSecret === "") {
|
|
176
|
+
throw new Error("cauthy-auth: token exchange needs clientId and clientSecret (CAUTHY_MCP_CLIENT_ID, CAUTHY_MCP_CLIENT_SECRET)");
|
|
177
|
+
}
|
|
178
|
+
const subject = typeof from === "string" ? bearer(from) : from.token;
|
|
179
|
+
const to = canonicalResource(target);
|
|
180
|
+
const key = `${subject}
|
|
181
|
+
${to}
|
|
182
|
+
${scopes.join(" ")}`;
|
|
183
|
+
const kept = exchanged.get(key);
|
|
184
|
+
if (kept && kept.until > Date.now()) return { accessToken: kept.accessToken, scopes: kept.scopes, expiresAt: kept.expiresAt };
|
|
185
|
+
const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/token`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
189
|
+
accept: "application/json",
|
|
190
|
+
authorization: `Basic ${btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`)}`
|
|
191
|
+
},
|
|
192
|
+
body: new URLSearchParams({
|
|
193
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
194
|
+
subject_token: subject,
|
|
195
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
|
|
196
|
+
resource: to,
|
|
197
|
+
...scopes.length > 0 ? { scope: scopes.join(" ") } : {}
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
const body = await res.json().catch(() => ({}));
|
|
201
|
+
if (!res.ok || typeof body.access_token !== "string") {
|
|
202
|
+
throw new CauthyExchangeError(
|
|
203
|
+
res.status,
|
|
204
|
+
typeof body.error === "string" ? body.error : "server_error",
|
|
205
|
+
typeof body.error_description === "string" ? body.error_description : `Cauthy answered ${res.status}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const expiresIn = typeof body.expires_in === "number" ? body.expires_in : 0;
|
|
209
|
+
const out = {
|
|
210
|
+
accessToken: body.access_token,
|
|
211
|
+
scopes: typeof body.scope === "string" ? body.scope.split(" ").filter(Boolean) : [],
|
|
212
|
+
expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString()
|
|
152
213
|
};
|
|
214
|
+
if (exchanged.size > 1e4) exchanged.clear();
|
|
215
|
+
exchanged.set(key, { ...out, until: Date.now() + Math.max(0, expiresIn - 30) * 1e3 });
|
|
216
|
+
return out;
|
|
153
217
|
};
|
|
154
218
|
const authenticate = (error) => {
|
|
155
219
|
const parts = [`resource_metadata=${quoted(metadataUrl)}`];
|
|
@@ -202,6 +266,7 @@ function mcpAuth(options = {}) {
|
|
|
202
266
|
return handler(request, agent);
|
|
203
267
|
};
|
|
204
268
|
},
|
|
269
|
+
exchange,
|
|
205
270
|
middleware(needs) {
|
|
206
271
|
return (req, res, next) => {
|
|
207
272
|
const pathname = (req.originalUrl ?? req.url ?? "/").split("?")[0] ?? "/";
|
|
@@ -235,6 +300,7 @@ function mcpAuth(options = {}) {
|
|
|
235
300
|
}
|
|
236
301
|
// Annotate the CommonJS export names for ESM import in node:
|
|
237
302
|
0 && (module.exports = {
|
|
303
|
+
CauthyExchangeError,
|
|
238
304
|
McpAuthError,
|
|
239
305
|
canonicalResource,
|
|
240
306
|
mcpAuth
|
package/dist/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp.ts","../src/verify.ts"],"sourcesContent":["// An MCP server (or any API) that AI agents use as one of your people.\n//\n// import { mcpAuth } from 'cauthy-auth/mcp'\n// const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:read'] })\n//\n// // Fetch-style (Hono, Next.js, Workers, Bun, Deno):\n// export default { fetch: auth.protect((request, agent) => handleMcp(request, agent)) }\n//\n// // Express or plain Node, with the MCP TypeScript SDK (its transport reads req.auth):\n// app.use(auth.middleware())\n//\n// How an agent gets in, as the MCP specification lays it out: it calls the\n// server, which answers 401 and points at its protected resource metadata\n// (RFC 9728); that names Cauthy as the authorization server; the agent\n// registers itself there, sends the person to sign in and allow it, and comes\n// back with a token issued for this server alone (its `aud`). This module is\n// the server's half: the metadata, the 401 that points at it, and checking\n// the token. The resource must be registered in Cauthy's dashboard (AI\n// agents), with the same address, for the workspace whose people use it.\n\nimport { jwtVerify, type JWTVerifyGetKey } from 'jose'\nimport { keysOf } from './verify'\n\n/** The person an agent acts for, and what they allowed it. */\nexport interface AgentIdentity {\n /** The person. */\n userId: string\n tenantId: string\n email?: string\n /** The person's roles in the workspace. */\n roles: string[]\n /** What the person allowed the agent to do here. */\n scopes: string[]\n /** The agent (its client_id at Cauthy). */\n clientId: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived. */\n token: string\n}\n\nexport interface McpAuthOptions {\n /** This server's address, as registered in Cauthy. Default: `CAUTHY_MCP_RESOURCE`. */\n resource?: string\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** Scopes every call needs. Default: none. */\n scopes?: string[]\n /** The scopes this server understands, to tell agents (`scopes_supported`). Default: `scopes`. */\n scopesSupported?: string[]\n /** A name for the server, shown by some agents. */\n resourceName?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: () => Date\n /**\n * Ask Cauthy, besides checking the token here, whether the person still\n * allows the agent (RFC 7662). Then an agent is cut off the moment it is\n * disconnected, not when its token runs out, at the price of a call to\n * Cauthy per token every `cacheSeconds` (default 30). Default: off.\n */\n introspect?: boolean | { cacheSeconds?: number }\n /** The fetch to call Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n}\n\n/** A token refused: `status` is 401 (no token, or not a good one) or 403 (a scope missing). */\nexport class McpAuthError extends Error {\n constructor(\n readonly status: 401 | 403,\n readonly code: 'invalid_request' | 'invalid_token' | 'insufficient_scope',\n readonly description: string,\n ) {\n super(`cauthy-auth: ${description}`)\n this.name = 'McpAuthError'\n }\n}\n\n/** What the MCP TypeScript SDK reads from `req.auth` (its AuthInfo). */\nexport interface McpAuthInfo {\n token: string\n clientId: string\n scopes: string[]\n /** Seconds since the epoch. */\n expiresAt: number\n resource: URL\n extra: { userId: string; tenantId: string; email?: string; roles: string[] }\n}\n\nexport interface McpAuth {\n /** This server's address, in the form Cauthy compares it in. */\n resource: string\n /** Where the protected resource metadata is served. */\n metadataUrl: string\n /** The protected resource metadata (RFC 9728). */\n metadata(): Record<string, unknown>\n /** The metadata's paths: `/.well-known/oauth-protected-resource` plus this server's path. */\n metadataPaths: string[]\n /** Verifies the `Authorization: Bearer` of a request (or a token). Throws `McpAuthError`. */\n verify(from: string | Request | Headers, needs?: string[]): Promise<AgentIdentity>\n /** The answer to a refused token: 401 or 403, with the `WWW-Authenticate` that points an agent at Cauthy. */\n challenge(error?: McpAuthError): Response\n /** The metadata, when the request asks for it; `null` otherwise. */\n handleMetadata(request: Request): Response | null\n /** Wraps a fetch-style handler: serves the metadata, refuses what has no good token, passes the rest with who it is. */\n protect(handler: (request: Request, agent: AgentIdentity) => Response | Promise<Response>, needs?: string[]): (request: Request) => Promise<Response>\n /**\n * Connect-style middleware (Express, Node's http): serves the metadata,\n * refuses what has no good token, and sets `req.agent` and `req.auth` —\n * what the MCP TypeScript SDK's transport hands its tools as `authInfo`.\n */\n middleware(needs?: string[]): (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void\n}\n\n// The slice of Node's (and Express's) request and response this needs.\ninterface NodeRequest {\n method?: string\n url?: string\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n auth?: McpAuthInfo\n agent?: AgentIdentity\n}\ninterface NodeResponse {\n statusCode: number\n setHeader(name: string, value: string): unknown\n end(body?: string): unknown\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\n/**\n * The address in the one form Cauthy keeps it: scheme and host in lower case,\n * no default port, no trailing slash, no query, no fragment.\n */\nexport function canonicalResource(raw: string): string {\n let u: URL\n try {\n u = new URL(raw.trim())\n } catch {\n throw new Error(`cauthy-auth: resource ${JSON.stringify(raw)} is not an absolute address`)\n }\n const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]'\n if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) {\n throw new Error('cauthy-auth: resource must be an https address')\n }\n if (u.search !== '' || u.hash !== '') throw new Error('cauthy-auth: resource may not have a query or a fragment')\n return `${u.protocol}//${u.host}${u.pathname.replace(/\\/+$/, '')}`\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\nfunction quoted(v: string): string {\n return `\"${v.replace(/[\"\\\\]/g, '')}\"`\n}\n\n/** Protects an MCP server with Cauthy. */\nexport function mcpAuth(options: McpAuthOptions = {}): McpAuth {\n const rawResource = options.resource ?? env('CAUTHY_MCP_RESOURCE') ?? ''\n if (rawResource === '') throw new Error('cauthy-auth: resource (CAUTHY_MCP_RESOURCE) is not set')\n const resource = canonicalResource(rawResource)\n const issuer = (options.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new Error('cauthy-auth: issuer (CAUTHY_ISSUER) is not set')\n const required = options.scopes ?? []\n const url = new URL(resource)\n const path = url.pathname === '/' ? '' : url.pathname\n const metadataPaths = [...new Set([`/.well-known/oauth-protected-resource${path}`, '/.well-known/oauth-protected-resource'])]\n const metadataUrl = `${url.origin}${metadataPaths[0]}`\n\n const metadata = () => ({\n resource,\n authorization_servers: [issuer],\n bearer_methods_supported: ['header'],\n scopes_supported: options.scopesSupported ?? required,\n ...(options.resourceName ? { resource_name: options.resourceName } : {}),\n })\n\n const introspect = options.introspect ? { cacheMs: ((typeof options.introspect === 'object' ? options.introspect.cacheSeconds : undefined) ?? 30) * 1000 } : null\n const answers = new Map<string, { active: boolean; until: number }>()\n const stillAllowed = async (token: string, cacheMs: number): Promise<boolean> => {\n const now = Date.now()\n const kept = answers.get(token)\n if (kept && kept.until > now) return kept.active\n let active = false\n try {\n const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/introspect`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },\n body: new URLSearchParams({ token }),\n })\n if (!res.ok) throw new Error(`introspection answered ${res.status}`)\n active = ((await res.json()) as { active?: unknown }).active === true\n } catch (err) {\n // Cauthy could not be asked: what it last said holds.\n if (kept) return kept.active\n throw new McpAuthError(401, 'invalid_token', `could not check the token with Cauthy: ${err instanceof Error ? err.message : 'unreachable'}`)\n }\n if (answers.size > 10_000) answers.clear()\n answers.set(token, { active, until: now + cacheMs })\n return active\n }\n\n const verify = async (from: string | Request | Headers, needs: string[] = []): Promise<AgentIdentity> => {\n const token = bearer(from)\n if (token === '') throw new McpAuthError(401, 'invalid_request', 'no access token')\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: resource,\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now?.(),\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new McpAuthError(401, 'invalid_token', err instanceof Error ? err.message : 'the token is not valid')\n }\n const clientId = typeof claims.client_id === 'string' ? claims.client_id : ''\n if (clientId === '') throw new McpAuthError(401, 'invalid_token', 'not a token issued to an agent')\n const scopes = typeof claims.scope === 'string' ? claims.scope.split(' ').filter(Boolean) : []\n if (introspect && !(await stillAllowed(token, introspect.cacheMs))) throw new McpAuthError(401, 'invalid_token', 'the person disconnected this agent')\n const missing = [...required, ...needs].filter((s) => !scopes.includes(s))\n if (missing.length > 0) throw new McpAuthError(403, 'insufficient_scope', `this needs ${missing.join(' ')}`)\n return {\n userId: String(claims.sub),\n tenantId: typeof claims.tid === 'string' ? claims.tid : '',\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n scopes,\n clientId,\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n }\n }\n\n const authenticate = (error?: McpAuthError): string => {\n const parts = [`resource_metadata=${quoted(metadataUrl)}`]\n if (error && error.code !== 'invalid_request') {\n parts.push(`error=${quoted(error.code)}`, `error_description=${quoted(error.description)}`)\n }\n // What to ask the person for: said when there is no token or it falls short.\n const scope = error?.code === 'invalid_token' ? [] : required\n if (scope.length > 0) parts.push(`scope=${quoted(scope.join(' '))}`)\n return `Bearer ${parts.join(', ')}`\n }\n\n const challenge = (error?: McpAuthError): Response => {\n const status = error?.status ?? 401\n return new Response(JSON.stringify({ error: error?.code ?? 'invalid_request', error_description: error?.description ?? 'an access token is required' }), {\n status,\n headers: { 'content-type': 'application/json', 'www-authenticate': authenticate(error) },\n })\n }\n\n const metadataResponse = () =>\n new Response(JSON.stringify(metadata()), {\n headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=3600', 'access-control-allow-origin': '*' },\n })\n\n const isMetadata = (pathname: string, method = 'GET') => method === 'GET' && metadataPaths.includes(pathname.replace(/\\/+$/, '') || '/')\n\n const handleMetadata = (request: Request): Response | null =>\n isMetadata(new URL(request.url).pathname, request.method) ? metadataResponse() : null\n\n const toAuthInfo = (a: AgentIdentity): McpAuthInfo => ({\n token: a.token,\n clientId: a.clientId,\n scopes: a.scopes,\n expiresAt: Math.floor(new Date(a.expiresAt).getTime() / 1000),\n resource: new URL(resource),\n extra: { userId: a.userId, tenantId: a.tenantId, ...(a.email ? { email: a.email } : {}), roles: a.roles },\n })\n\n return {\n resource,\n metadataUrl,\n metadataPaths,\n metadata,\n verify,\n challenge,\n handleMetadata,\n protect(handler, needs) {\n return async (request) => {\n const meta = handleMetadata(request)\n if (meta) return meta\n let agent: AgentIdentity\n try {\n agent = await verify(request, needs)\n } catch (err) {\n if (err instanceof McpAuthError) return challenge(err)\n throw err\n }\n return handler(request, agent)\n }\n },\n middleware(needs) {\n return (req, res, next) => {\n const pathname = (req.originalUrl ?? req.url ?? '/').split('?')[0] ?? '/'\n if (isMetadata(pathname, req.method)) {\n res.statusCode = 200\n res.setHeader('content-type', 'application/json')\n res.setHeader('cache-control', 'public, max-age=3600')\n res.setHeader('access-control-allow-origin', '*')\n res.end(JSON.stringify(metadata()))\n return\n }\n const raw = req.headers.authorization\n const header = Array.isArray(raw) ? (raw[0] ?? '') : (raw ?? '')\n verify(/^Bearer\\s+/i.test(header) ? header : '', needs).then(\n (agent) => {\n req.agent = agent\n req.auth = toAuthInfo(agent)\n next()\n },\n (err: unknown) => {\n if (!(err instanceof McpAuthError)) return next(err)\n res.statusCode = err.status\n res.setHeader('content-type', 'application/json')\n res.setHeader('www-authenticate', authenticate(err))\n res.end(JSON.stringify({ error: err.code, error_description: err.description }))\n },\n )\n }\n },\n }\n}\n","// A backend that is handed a Cauthy access token: is it real, and whose is it?\n//\n// const who = await verifyToken(request) // or the token itself\n// who.userId, who.tenantId, who.roles\n//\n// Cauthy signs its tokens with a key it publishes (/.well-known/jwks.json), so\n// checking one needs no secret and no call to Cauthy per request: the keys are\n// fetched once and cached. Throws `CauthyTokenError` for anything else: answer\n// it 401.\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyGetKey } from 'jose'\n\n/** Who a verified access token belongs to. */\nexport interface TokenIdentity {\n /** The person. For a system's own token, the client it identifies as. */\n userId: string\n tenantId: string\n email?: string\n roles: string[]\n /** Set when the token is a system's own and not a person's. */\n system?: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived, to pass on. */\n token: string\n}\n\nexport class CauthyTokenError extends Error {\n constructor(readonly reason: string) {\n super(`cauthy-auth: token refused: ${reason}`)\n this.name = 'CauthyTokenError'\n }\n}\n\nexport interface VerifyTokenOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The audience the tokens must carry. Default: `CAUTHY_AUDIENCE`; unchecked when neither is set. */\n audience?: string\n /** Refuse a token of any other workspace. Default: `CAUTHY_TENANT_ID`; unchecked when neither is set. */\n tenantId?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: Date\n}\n\nconst keys = new Map<string, JWTVerifyGetKey>()\n\n/** Cauthy's published keys, fetched once per issuer and cached. */\nexport function keysOf(issuer: string): JWTVerifyGetKey {\n let set = keys.get(issuer)\n if (set === undefined) {\n set = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 30_000 })\n keys.set(issuer, set)\n }\n return set\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\n/**\n * Verifies an access token (or the `Authorization: Bearer` of a request) and\n * answers who it belongs to. Checked: the signature, against Cauthy's\n * published keys; that Cauthy issued it; that it has not expired; and the\n * audience and the workspace, when given.\n */\nexport async function verifyToken(from: string | Request | Headers, options: VerifyTokenOptions = {}): Promise<TokenIdentity> {\n const issuer = (options.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new CauthyTokenError('issuer (CAUTHY_ISSUER) is not set')\n const token = bearer(from)\n if (token === '') throw new CauthyTokenError('no token')\n\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: options.audience ?? env('CAUTHY_AUDIENCE'),\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now,\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new CauthyTokenError(err instanceof Error ? err.message : 'bad signature')\n }\n\n const tenantId = typeof claims.tid === 'string' ? claims.tid : ''\n const wanted = options.tenantId ?? env('CAUTHY_TENANT_ID')\n if (wanted && tenantId !== wanted) throw new CauthyTokenError('issued for another workspace')\n\n return {\n userId: String(claims.sub),\n tenantId,\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n ...(typeof claims.sys === 'string' && claims.sys !== '' ? { system: claims.sys } : {}),\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n }\n}\n"],"mappings":"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","names":["import_jose"]}
|
|
1
|
+
{"version":3,"sources":["../src/mcp.ts","../src/verify.ts"],"sourcesContent":["// An MCP server (or any API) that AI agents use as one of your people.\n//\n// import { mcpAuth } from 'cauthy-auth/mcp'\n// const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:read'] })\n//\n// // Fetch-style (Hono, Next.js, Workers, Bun, Deno):\n// export default { fetch: auth.protect((request, agent) => handleMcp(request, agent)) }\n//\n// // Express or plain Node, with the MCP TypeScript SDK (its transport reads req.auth):\n// app.use(auth.middleware())\n//\n// How an agent gets in, as the MCP specification lays it out: it calls the\n// server, which answers 401 and points at its protected resource metadata\n// (RFC 9728); that names Cauthy as the authorization server; the agent\n// registers itself there, sends the person to sign in and allow it, and comes\n// back with a token issued for this server alone (its `aud`). This module is\n// the server's half: the metadata, the 401 that points at it, and checking\n// the token. The resource must be registered in Cauthy's dashboard (AI\n// agents), with the same address, for the workspace whose people use it.\n//\n// A server that calls another of the workspace's servers for the person\n// trades the agent's token for one for that server (token exchange, RFC 8693):\n//\n// const auth = mcpAuth({ resource, clientId, clientSecret }) // from the dashboard\n// const { accessToken } = await auth.exchange(agent, 'https://api.example.com', ['orders:read'])\n//\n// The workspace's admins must have delegated that call in the dashboard, and\n// the person allowed it when they connected the agent.\n\nimport { jwtVerify, type JWTVerifyGetKey } from 'jose'\nimport { keysOf } from './verify'\n\n/** The person an agent acts for, and what they allowed it. */\nexport interface AgentIdentity {\n /** The person. */\n userId: string\n tenantId: string\n email?: string\n /** The person's roles in the workspace. */\n roles: string[]\n /** What the person allowed the agent to do here. */\n scopes: string[]\n /** The agent (its client_id at Cauthy). */\n clientId: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived. */\n token: string\n /**\n * Set when the token came by token exchange: the server that called this\n * one for the person (the `act` claim's subject).\n */\n actor?: string\n}\n\nexport interface McpAuthOptions {\n /** This server's address, as registered in Cauthy. Default: `CAUTHY_MCP_RESOURCE`. */\n resource?: string\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** Scopes every call needs. Default: none. */\n scopes?: string[]\n /** The scopes this server understands, to tell agents (`scopes_supported`). Default: `scopes`. */\n scopesSupported?: string[]\n /** A name for the server, shown by some agents. */\n resourceName?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: () => Date\n /**\n * Ask Cauthy, besides checking the token here, whether the person still\n * allows the agent (RFC 7662). Then an agent is cut off the moment it is\n * disconnected, not when its token runs out, at the price of a call to\n * Cauthy per token every `cacheSeconds` (default 30). Default: off.\n */\n introspect?: boolean | { cacheSeconds?: number }\n /** The fetch to call Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n /**\n * This server's client_id and secret at Cauthy, for token exchange (AI\n * agents → the server → Server credentials). Defaults:\n * `CAUTHY_MCP_CLIENT_ID`, `CAUTHY_MCP_CLIENT_SECRET`.\n */\n clientId?: string\n clientSecret?: string\n}\n\n/** What a token exchange answers. */\nexport interface ExchangedToken {\n /** The token for the other server, as the person. Send it as `Authorization: Bearer`. */\n accessToken: string\n /** What it allows there. */\n scopes: string[]\n /** UTC, ISO 8601. Never later than the agent's own token. */\n expiresAt: string\n}\n\n/** A token exchange Cauthy refused, with the OAuth error it answered. */\nexport class CauthyExchangeError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n readonly description: string,\n ) {\n super(`cauthy-auth: token exchange refused (${code}): ${description}`)\n this.name = 'CauthyExchangeError'\n }\n}\n\n/** A token refused: `status` is 401 (no token, or not a good one) or 403 (a scope missing). */\nexport class McpAuthError extends Error {\n constructor(\n readonly status: 401 | 403,\n readonly code: 'invalid_request' | 'invalid_token' | 'insufficient_scope',\n readonly description: string,\n ) {\n super(`cauthy-auth: ${description}`)\n this.name = 'McpAuthError'\n }\n}\n\n/** What the MCP TypeScript SDK reads from `req.auth` (its AuthInfo). */\nexport interface McpAuthInfo {\n token: string\n clientId: string\n scopes: string[]\n /** Seconds since the epoch. */\n expiresAt: number\n resource: URL\n extra: { userId: string; tenantId: string; email?: string; roles: string[] }\n}\n\nexport interface McpAuth {\n /** This server's address, in the form Cauthy compares it in. */\n resource: string\n /** Where the protected resource metadata is served. */\n metadataUrl: string\n /** The protected resource metadata (RFC 9728). */\n metadata(): Record<string, unknown>\n /** The metadata's paths: `/.well-known/oauth-protected-resource` plus this server's path. */\n metadataPaths: string[]\n /** Verifies the `Authorization: Bearer` of a request (or a token). Throws `McpAuthError`. */\n verify(from: string | Request | Headers, needs?: string[]): Promise<AgentIdentity>\n /** The answer to a refused token: 401 or 403, with the `WWW-Authenticate` that points an agent at Cauthy. */\n challenge(error?: McpAuthError): Response\n /** The metadata, when the request asks for it; `null` otherwise. */\n handleMetadata(request: Request): Response | null\n /** Wraps a fetch-style handler: serves the metadata, refuses what has no good token, passes the rest with who it is. */\n protect(handler: (request: Request, agent: AgentIdentity) => Response | Promise<Response>, needs?: string[]): (request: Request) => Promise<Response>\n /**\n * Connect-style middleware (Express, Node's http): serves the metadata,\n * refuses what has no good token, and sets `req.agent` and `req.auth` —\n * what the MCP TypeScript SDK's transport hands its tools as `authInfo`.\n */\n middleware(needs?: string[]): (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void\n /**\n * Trades the agent's token this server received for one for another of the\n * workspace's servers, to call it for the person (RFC 8693). Needs this\n * server's `clientId` and `clientSecret`. Answers are kept until shortly\n * before they expire. Throws `CauthyExchangeError` when Cauthy refuses.\n */\n exchange(from: string | AgentIdentity, target: string, scopes?: string[]): Promise<ExchangedToken>\n}\n\n// The slice of Node's (and Express's) request and response this needs.\ninterface NodeRequest {\n method?: string\n url?: string\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n auth?: McpAuthInfo\n agent?: AgentIdentity\n}\ninterface NodeResponse {\n statusCode: number\n setHeader(name: string, value: string): unknown\n end(body?: string): unknown\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\n/**\n * The address in the one form Cauthy keeps it: scheme and host in lower case,\n * no default port, no trailing slash, no query, no fragment.\n */\nexport function canonicalResource(raw: string): string {\n let u: URL\n try {\n u = new URL(raw.trim())\n } catch {\n throw new Error(`cauthy-auth: resource ${JSON.stringify(raw)} is not an absolute address`)\n }\n const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]'\n if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) {\n throw new Error('cauthy-auth: resource must be an https address')\n }\n if (u.search !== '' || u.hash !== '') throw new Error('cauthy-auth: resource may not have a query or a fragment')\n return `${u.protocol}//${u.host}${u.pathname.replace(/\\/+$/, '')}`\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\nfunction isActor(v: unknown): v is { sub: string } {\n return typeof v === 'object' && v !== null && typeof (v as { sub?: unknown }).sub === 'string'\n}\n\nfunction quoted(v: string): string {\n return `\"${v.replace(/[\"\\\\]/g, '')}\"`\n}\n\n/** Protects an MCP server with Cauthy. */\nexport function mcpAuth(options: McpAuthOptions = {}): McpAuth {\n const rawResource = options.resource ?? env('CAUTHY_MCP_RESOURCE') ?? ''\n if (rawResource === '') throw new Error('cauthy-auth: resource (CAUTHY_MCP_RESOURCE) is not set')\n const resource = canonicalResource(rawResource)\n const issuer = (options.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new Error('cauthy-auth: issuer (CAUTHY_ISSUER) is not set')\n const required = options.scopes ?? []\n const url = new URL(resource)\n const path = url.pathname === '/' ? '' : url.pathname\n const metadataPaths = [...new Set([`/.well-known/oauth-protected-resource${path}`, '/.well-known/oauth-protected-resource'])]\n const metadataUrl = `${url.origin}${metadataPaths[0]}`\n\n const metadata = () => ({\n resource,\n authorization_servers: [issuer],\n bearer_methods_supported: ['header'],\n scopes_supported: options.scopesSupported ?? required,\n ...(options.resourceName ? { resource_name: options.resourceName } : {}),\n })\n\n const introspect = options.introspect ? { cacheMs: ((typeof options.introspect === 'object' ? options.introspect.cacheSeconds : undefined) ?? 30) * 1000 } : null\n const answers = new Map<string, { active: boolean; until: number }>()\n const stillAllowed = async (token: string, cacheMs: number): Promise<boolean> => {\n const now = Date.now()\n const kept = answers.get(token)\n if (kept && kept.until > now) return kept.active\n let active = false\n try {\n const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/introspect`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },\n body: new URLSearchParams({ token }),\n })\n if (!res.ok) throw new Error(`introspection answered ${res.status}`)\n active = ((await res.json()) as { active?: unknown }).active === true\n } catch (err) {\n // Cauthy could not be asked: what it last said holds.\n if (kept) return kept.active\n throw new McpAuthError(401, 'invalid_token', `could not check the token with Cauthy: ${err instanceof Error ? err.message : 'unreachable'}`)\n }\n if (answers.size > 10_000) answers.clear()\n answers.set(token, { active, until: now + cacheMs })\n return active\n }\n\n const verify = async (from: string | Request | Headers, needs: string[] = []): Promise<AgentIdentity> => {\n const token = bearer(from)\n if (token === '') throw new McpAuthError(401, 'invalid_request', 'no access token')\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: resource,\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now?.(),\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new McpAuthError(401, 'invalid_token', err instanceof Error ? err.message : 'the token is not valid')\n }\n const clientId = typeof claims.client_id === 'string' ? claims.client_id : ''\n if (clientId === '') throw new McpAuthError(401, 'invalid_token', 'not a token issued to an agent')\n const scopes = typeof claims.scope === 'string' ? claims.scope.split(' ').filter(Boolean) : []\n if (introspect && !(await stillAllowed(token, introspect.cacheMs))) throw new McpAuthError(401, 'invalid_token', 'the token is no longer allowed: the agent was disconnected, or its access withdrawn')\n const missing = [...required, ...needs].filter((s) => !scopes.includes(s))\n if (missing.length > 0) throw new McpAuthError(403, 'insufficient_scope', `this needs ${missing.join(' ')}`)\n return {\n userId: String(claims.sub),\n tenantId: typeof claims.tid === 'string' ? claims.tid : '',\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n scopes,\n clientId,\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n ...(isActor(claims.act) ? { actor: claims.act.sub } : {}),\n }\n }\n\n const clientId = options.clientId ?? env('CAUTHY_MCP_CLIENT_ID') ?? ''\n const clientSecret = options.clientSecret ?? env('CAUTHY_MCP_CLIENT_SECRET') ?? ''\n const exchanged = new Map<string, ExchangedToken & { until: number }>()\n const exchange = async (from: string | AgentIdentity, target: string, scopes: string[] = []): Promise<ExchangedToken> => {\n if (clientId === '' || clientSecret === '') {\n throw new Error('cauthy-auth: token exchange needs clientId and clientSecret (CAUTHY_MCP_CLIENT_ID, CAUTHY_MCP_CLIENT_SECRET)')\n }\n const subject = typeof from === 'string' ? bearer(from) : from.token\n const to = canonicalResource(target)\n const key = `${subject}\\n${to}\\n${scopes.join(' ')}`\n const kept = exchanged.get(key)\n if (kept && kept.until > Date.now()) return { accessToken: kept.accessToken, scopes: kept.scopes, expiresAt: kept.expiresAt }\n const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/token`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n accept: 'application/json',\n authorization: `Basic ${btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`)}`,\n },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subject,\n subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',\n resource: to,\n ...(scopes.length > 0 ? { scope: scopes.join(' ') } : {}),\n }),\n })\n const body = (await res.json().catch(() => ({}))) as Record<string, unknown>\n if (!res.ok || typeof body.access_token !== 'string') {\n throw new CauthyExchangeError(res.status, typeof body.error === 'string' ? body.error : 'server_error',\n typeof body.error_description === 'string' ? body.error_description : `Cauthy answered ${res.status}`)\n }\n const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : 0\n const out: ExchangedToken = {\n accessToken: body.access_token,\n scopes: typeof body.scope === 'string' ? body.scope.split(' ').filter(Boolean) : [],\n expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),\n }\n if (exchanged.size > 10_000) exchanged.clear()\n // Kept until 30 seconds before it runs out, so it is never sent stale.\n exchanged.set(key, { ...out, until: Date.now() + Math.max(0, expiresIn - 30) * 1000 })\n return out\n }\n\n const authenticate = (error?: McpAuthError): string => {\n const parts = [`resource_metadata=${quoted(metadataUrl)}`]\n if (error && error.code !== 'invalid_request') {\n parts.push(`error=${quoted(error.code)}`, `error_description=${quoted(error.description)}`)\n }\n // What to ask the person for: said when there is no token or it falls short.\n const scope = error?.code === 'invalid_token' ? [] : required\n if (scope.length > 0) parts.push(`scope=${quoted(scope.join(' '))}`)\n return `Bearer ${parts.join(', ')}`\n }\n\n const challenge = (error?: McpAuthError): Response => {\n const status = error?.status ?? 401\n return new Response(JSON.stringify({ error: error?.code ?? 'invalid_request', error_description: error?.description ?? 'an access token is required' }), {\n status,\n headers: { 'content-type': 'application/json', 'www-authenticate': authenticate(error) },\n })\n }\n\n const metadataResponse = () =>\n new Response(JSON.stringify(metadata()), {\n headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=3600', 'access-control-allow-origin': '*' },\n })\n\n const isMetadata = (pathname: string, method = 'GET') => method === 'GET' && metadataPaths.includes(pathname.replace(/\\/+$/, '') || '/')\n\n const handleMetadata = (request: Request): Response | null =>\n isMetadata(new URL(request.url).pathname, request.method) ? metadataResponse() : null\n\n const toAuthInfo = (a: AgentIdentity): McpAuthInfo => ({\n token: a.token,\n clientId: a.clientId,\n scopes: a.scopes,\n expiresAt: Math.floor(new Date(a.expiresAt).getTime() / 1000),\n resource: new URL(resource),\n extra: { userId: a.userId, tenantId: a.tenantId, ...(a.email ? { email: a.email } : {}), roles: a.roles },\n })\n\n return {\n resource,\n metadataUrl,\n metadataPaths,\n metadata,\n verify,\n challenge,\n handleMetadata,\n protect(handler, needs) {\n return async (request) => {\n const meta = handleMetadata(request)\n if (meta) return meta\n let agent: AgentIdentity\n try {\n agent = await verify(request, needs)\n } catch (err) {\n if (err instanceof McpAuthError) return challenge(err)\n throw err\n }\n return handler(request, agent)\n }\n },\n exchange,\n middleware(needs) {\n return (req, res, next) => {\n const pathname = (req.originalUrl ?? req.url ?? '/').split('?')[0] ?? '/'\n if (isMetadata(pathname, req.method)) {\n res.statusCode = 200\n res.setHeader('content-type', 'application/json')\n res.setHeader('cache-control', 'public, max-age=3600')\n res.setHeader('access-control-allow-origin', '*')\n res.end(JSON.stringify(metadata()))\n return\n }\n const raw = req.headers.authorization\n const header = Array.isArray(raw) ? (raw[0] ?? '') : (raw ?? '')\n verify(/^Bearer\\s+/i.test(header) ? header : '', needs).then(\n (agent) => {\n req.agent = agent\n req.auth = toAuthInfo(agent)\n next()\n },\n (err: unknown) => {\n if (!(err instanceof McpAuthError)) return next(err)\n res.statusCode = err.status\n res.setHeader('content-type', 'application/json')\n res.setHeader('www-authenticate', authenticate(err))\n res.end(JSON.stringify({ error: err.code, error_description: err.description }))\n },\n )\n }\n },\n }\n}\n","// A backend that is handed a Cauthy access token: is it real, and whose is it?\n//\n// const who = await verifyToken(request) // or the token itself\n// who.userId, who.tenantId, who.roles\n//\n// Cauthy signs its tokens with a key it publishes (/.well-known/jwks.json), so\n// checking one needs no secret and no call to Cauthy per request: the keys are\n// fetched once and cached. Throws `CauthyTokenError` for anything else: answer\n// it 401.\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyGetKey } from 'jose'\n\n/** Who a verified access token belongs to. */\nexport interface TokenIdentity {\n /** The person. For a system's own token, the client it identifies as. */\n userId: string\n tenantId: string\n email?: string\n roles: string[]\n /** Set when the token is a system's own and not a person's. */\n system?: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived, to pass on. */\n token: string\n}\n\nexport class CauthyTokenError extends Error {\n constructor(readonly reason: string) {\n super(`cauthy-auth: token refused: ${reason}`)\n this.name = 'CauthyTokenError'\n }\n}\n\nexport interface VerifyTokenOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The audience the tokens must carry. Default: `CAUTHY_AUDIENCE`; unchecked when neither is set. */\n audience?: string\n /** Refuse a token of any other workspace. Default: `CAUTHY_TENANT_ID`; unchecked when neither is set. */\n tenantId?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: Date\n}\n\nconst keys = new Map<string, JWTVerifyGetKey>()\n\n/** Cauthy's published keys, fetched once per issuer and cached. */\nexport function keysOf(issuer: string): JWTVerifyGetKey {\n let set = keys.get(issuer)\n if (set === undefined) {\n set = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 30_000 })\n keys.set(issuer, set)\n }\n return set\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\n/**\n * Verifies an access token (or the `Authorization: Bearer` of a request) and\n * answers who it belongs to. Checked: the signature, against Cauthy's\n * published keys; that Cauthy issued it; that it has not expired; and the\n * audience and the workspace, when given.\n */\nexport async function verifyToken(from: string | Request | Headers, options: VerifyTokenOptions = {}): Promise<TokenIdentity> {\n const issuer = (options.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new CauthyTokenError('issuer (CAUTHY_ISSUER) is not set')\n const token = bearer(from)\n if (token === '') throw new CauthyTokenError('no token')\n\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: options.audience ?? env('CAUTHY_AUDIENCE'),\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now,\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new CauthyTokenError(err instanceof Error ? err.message : 'bad signature')\n }\n\n const tenantId = typeof claims.tid === 'string' ? claims.tid : ''\n const wanted = options.tenantId ?? env('CAUTHY_TENANT_ID')\n if (wanted && tenantId !== wanted) throw new CauthyTokenError('issued for another workspace')\n\n return {\n userId: String(claims.sub),\n tenantId,\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n ...(typeof claims.sys === 'string' && claims.sys !== '' ? { system: claims.sys } : {}),\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n }\n}\n"],"mappings":"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","names":["import_jose","clientId"]}
|
package/dist/mcp.mjs
CHANGED
|
@@ -14,6 +14,18 @@ function keysOf(issuer) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
// src/mcp.ts
|
|
17
|
+
var CauthyExchangeError = class extends Error {
|
|
18
|
+
constructor(status, code, description) {
|
|
19
|
+
super(`cauthy-auth: token exchange refused (${code}): ${description}`);
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.description = description;
|
|
23
|
+
this.name = "CauthyExchangeError";
|
|
24
|
+
}
|
|
25
|
+
status;
|
|
26
|
+
code;
|
|
27
|
+
description;
|
|
28
|
+
};
|
|
17
29
|
var McpAuthError = class extends Error {
|
|
18
30
|
constructor(status, code, description) {
|
|
19
31
|
super(`cauthy-auth: ${description}`);
|
|
@@ -48,6 +60,9 @@ function bearer(from) {
|
|
|
48
60
|
const header = (from instanceof Headers ? from : from.headers).get("authorization") ?? "";
|
|
49
61
|
return /^Bearer\s+/i.test(header) ? header.replace(/^Bearer\s+/i, "").trim() : "";
|
|
50
62
|
}
|
|
63
|
+
function isActor(v) {
|
|
64
|
+
return typeof v === "object" && v !== null && typeof v.sub === "string";
|
|
65
|
+
}
|
|
51
66
|
function quoted(v) {
|
|
52
67
|
return `"${v.replace(/["\\]/g, "")}"`;
|
|
53
68
|
}
|
|
@@ -108,10 +123,10 @@ function mcpAuth(options = {}) {
|
|
|
108
123
|
} catch (err) {
|
|
109
124
|
throw new McpAuthError(401, "invalid_token", err instanceof Error ? err.message : "the token is not valid");
|
|
110
125
|
}
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
126
|
+
const clientId2 = typeof claims.client_id === "string" ? claims.client_id : "";
|
|
127
|
+
if (clientId2 === "") throw new McpAuthError(401, "invalid_token", "not a token issued to an agent");
|
|
113
128
|
const scopes = typeof claims.scope === "string" ? claims.scope.split(" ").filter(Boolean) : [];
|
|
114
|
-
if (introspect && !await stillAllowed(token, introspect.cacheMs)) throw new McpAuthError(401, "invalid_token", "the
|
|
129
|
+
if (introspect && !await stillAllowed(token, introspect.cacheMs)) throw new McpAuthError(401, "invalid_token", "the token is no longer allowed: the agent was disconnected, or its access withdrawn");
|
|
115
130
|
const missing = [...required, ...needs].filter((s) => !scopes.includes(s));
|
|
116
131
|
if (missing.length > 0) throw new McpAuthError(403, "insufficient_scope", `this needs ${missing.join(" ")}`);
|
|
117
132
|
return {
|
|
@@ -120,10 +135,58 @@ function mcpAuth(options = {}) {
|
|
|
120
135
|
...typeof claims.email === "string" && claims.email !== "" ? { email: claims.email } : {},
|
|
121
136
|
roles: Array.isArray(claims.roles) ? claims.roles.filter((r) => typeof r === "string") : [],
|
|
122
137
|
scopes,
|
|
123
|
-
clientId,
|
|
138
|
+
clientId: clientId2,
|
|
124
139
|
expiresAt: new Date(claims.exp * 1e3).toISOString(),
|
|
125
|
-
token
|
|
140
|
+
token,
|
|
141
|
+
...isActor(claims.act) ? { actor: claims.act.sub } : {}
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
const clientId = options.clientId ?? env("CAUTHY_MCP_CLIENT_ID") ?? "";
|
|
145
|
+
const clientSecret = options.clientSecret ?? env("CAUTHY_MCP_CLIENT_SECRET") ?? "";
|
|
146
|
+
const exchanged = /* @__PURE__ */ new Map();
|
|
147
|
+
const exchange = async (from, target, scopes = []) => {
|
|
148
|
+
if (clientId === "" || clientSecret === "") {
|
|
149
|
+
throw new Error("cauthy-auth: token exchange needs clientId and clientSecret (CAUTHY_MCP_CLIENT_ID, CAUTHY_MCP_CLIENT_SECRET)");
|
|
150
|
+
}
|
|
151
|
+
const subject = typeof from === "string" ? bearer(from) : from.token;
|
|
152
|
+
const to = canonicalResource(target);
|
|
153
|
+
const key = `${subject}
|
|
154
|
+
${to}
|
|
155
|
+
${scopes.join(" ")}`;
|
|
156
|
+
const kept = exchanged.get(key);
|
|
157
|
+
if (kept && kept.until > Date.now()) return { accessToken: kept.accessToken, scopes: kept.scopes, expiresAt: kept.expiresAt };
|
|
158
|
+
const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/token`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
162
|
+
accept: "application/json",
|
|
163
|
+
authorization: `Basic ${btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`)}`
|
|
164
|
+
},
|
|
165
|
+
body: new URLSearchParams({
|
|
166
|
+
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
167
|
+
subject_token: subject,
|
|
168
|
+
subject_token_type: "urn:ietf:params:oauth:token-type:access_token",
|
|
169
|
+
resource: to,
|
|
170
|
+
...scopes.length > 0 ? { scope: scopes.join(" ") } : {}
|
|
171
|
+
})
|
|
172
|
+
});
|
|
173
|
+
const body = await res.json().catch(() => ({}));
|
|
174
|
+
if (!res.ok || typeof body.access_token !== "string") {
|
|
175
|
+
throw new CauthyExchangeError(
|
|
176
|
+
res.status,
|
|
177
|
+
typeof body.error === "string" ? body.error : "server_error",
|
|
178
|
+
typeof body.error_description === "string" ? body.error_description : `Cauthy answered ${res.status}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const expiresIn = typeof body.expires_in === "number" ? body.expires_in : 0;
|
|
182
|
+
const out = {
|
|
183
|
+
accessToken: body.access_token,
|
|
184
|
+
scopes: typeof body.scope === "string" ? body.scope.split(" ").filter(Boolean) : [],
|
|
185
|
+
expiresAt: new Date(Date.now() + expiresIn * 1e3).toISOString()
|
|
126
186
|
};
|
|
187
|
+
if (exchanged.size > 1e4) exchanged.clear();
|
|
188
|
+
exchanged.set(key, { ...out, until: Date.now() + Math.max(0, expiresIn - 30) * 1e3 });
|
|
189
|
+
return out;
|
|
127
190
|
};
|
|
128
191
|
const authenticate = (error) => {
|
|
129
192
|
const parts = [`resource_metadata=${quoted(metadataUrl)}`];
|
|
@@ -176,6 +239,7 @@ function mcpAuth(options = {}) {
|
|
|
176
239
|
return handler(request, agent);
|
|
177
240
|
};
|
|
178
241
|
},
|
|
242
|
+
exchange,
|
|
179
243
|
middleware(needs) {
|
|
180
244
|
return (req, res, next) => {
|
|
181
245
|
const pathname = (req.originalUrl ?? req.url ?? "/").split("?")[0] ?? "/";
|
|
@@ -208,6 +272,7 @@ function mcpAuth(options = {}) {
|
|
|
208
272
|
};
|
|
209
273
|
}
|
|
210
274
|
export {
|
|
275
|
+
CauthyExchangeError,
|
|
211
276
|
McpAuthError,
|
|
212
277
|
canonicalResource,
|
|
213
278
|
mcpAuth
|
package/dist/mcp.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp.ts","../src/verify.ts"],"sourcesContent":["// An MCP server (or any API) that AI agents use as one of your people.\n//\n// import { mcpAuth } from 'cauthy-auth/mcp'\n// const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:read'] })\n//\n// // Fetch-style (Hono, Next.js, Workers, Bun, Deno):\n// export default { fetch: auth.protect((request, agent) => handleMcp(request, agent)) }\n//\n// // Express or plain Node, with the MCP TypeScript SDK (its transport reads req.auth):\n// app.use(auth.middleware())\n//\n// How an agent gets in, as the MCP specification lays it out: it calls the\n// server, which answers 401 and points at its protected resource metadata\n// (RFC 9728); that names Cauthy as the authorization server; the agent\n// registers itself there, sends the person to sign in and allow it, and comes\n// back with a token issued for this server alone (its `aud`). This module is\n// the server's half: the metadata, the 401 that points at it, and checking\n// the token. The resource must be registered in Cauthy's dashboard (AI\n// agents), with the same address, for the workspace whose people use it.\n\nimport { jwtVerify, type JWTVerifyGetKey } from 'jose'\nimport { keysOf } from './verify'\n\n/** The person an agent acts for, and what they allowed it. */\nexport interface AgentIdentity {\n /** The person. */\n userId: string\n tenantId: string\n email?: string\n /** The person's roles in the workspace. */\n roles: string[]\n /** What the person allowed the agent to do here. */\n scopes: string[]\n /** The agent (its client_id at Cauthy). */\n clientId: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived. */\n token: string\n}\n\nexport interface McpAuthOptions {\n /** This server's address, as registered in Cauthy. Default: `CAUTHY_MCP_RESOURCE`. */\n resource?: string\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** Scopes every call needs. Default: none. */\n scopes?: string[]\n /** The scopes this server understands, to tell agents (`scopes_supported`). Default: `scopes`. */\n scopesSupported?: string[]\n /** A name for the server, shown by some agents. */\n resourceName?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: () => Date\n /**\n * Ask Cauthy, besides checking the token here, whether the person still\n * allows the agent (RFC 7662). Then an agent is cut off the moment it is\n * disconnected, not when its token runs out, at the price of a call to\n * Cauthy per token every `cacheSeconds` (default 30). Default: off.\n */\n introspect?: boolean | { cacheSeconds?: number }\n /** The fetch to call Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n}\n\n/** A token refused: `status` is 401 (no token, or not a good one) or 403 (a scope missing). */\nexport class McpAuthError extends Error {\n constructor(\n readonly status: 401 | 403,\n readonly code: 'invalid_request' | 'invalid_token' | 'insufficient_scope',\n readonly description: string,\n ) {\n super(`cauthy-auth: ${description}`)\n this.name = 'McpAuthError'\n }\n}\n\n/** What the MCP TypeScript SDK reads from `req.auth` (its AuthInfo). */\nexport interface McpAuthInfo {\n token: string\n clientId: string\n scopes: string[]\n /** Seconds since the epoch. */\n expiresAt: number\n resource: URL\n extra: { userId: string; tenantId: string; email?: string; roles: string[] }\n}\n\nexport interface McpAuth {\n /** This server's address, in the form Cauthy compares it in. */\n resource: string\n /** Where the protected resource metadata is served. */\n metadataUrl: string\n /** The protected resource metadata (RFC 9728). */\n metadata(): Record<string, unknown>\n /** The metadata's paths: `/.well-known/oauth-protected-resource` plus this server's path. */\n metadataPaths: string[]\n /** Verifies the `Authorization: Bearer` of a request (or a token). Throws `McpAuthError`. */\n verify(from: string | Request | Headers, needs?: string[]): Promise<AgentIdentity>\n /** The answer to a refused token: 401 or 403, with the `WWW-Authenticate` that points an agent at Cauthy. */\n challenge(error?: McpAuthError): Response\n /** The metadata, when the request asks for it; `null` otherwise. */\n handleMetadata(request: Request): Response | null\n /** Wraps a fetch-style handler: serves the metadata, refuses what has no good token, passes the rest with who it is. */\n protect(handler: (request: Request, agent: AgentIdentity) => Response | Promise<Response>, needs?: string[]): (request: Request) => Promise<Response>\n /**\n * Connect-style middleware (Express, Node's http): serves the metadata,\n * refuses what has no good token, and sets `req.agent` and `req.auth` —\n * what the MCP TypeScript SDK's transport hands its tools as `authInfo`.\n */\n middleware(needs?: string[]): (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void\n}\n\n// The slice of Node's (and Express's) request and response this needs.\ninterface NodeRequest {\n method?: string\n url?: string\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n auth?: McpAuthInfo\n agent?: AgentIdentity\n}\ninterface NodeResponse {\n statusCode: number\n setHeader(name: string, value: string): unknown\n end(body?: string): unknown\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\n/**\n * The address in the one form Cauthy keeps it: scheme and host in lower case,\n * no default port, no trailing slash, no query, no fragment.\n */\nexport function canonicalResource(raw: string): string {\n let u: URL\n try {\n u = new URL(raw.trim())\n } catch {\n throw new Error(`cauthy-auth: resource ${JSON.stringify(raw)} is not an absolute address`)\n }\n const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]'\n if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) {\n throw new Error('cauthy-auth: resource must be an https address')\n }\n if (u.search !== '' || u.hash !== '') throw new Error('cauthy-auth: resource may not have a query or a fragment')\n return `${u.protocol}//${u.host}${u.pathname.replace(/\\/+$/, '')}`\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\nfunction quoted(v: string): string {\n return `\"${v.replace(/[\"\\\\]/g, '')}\"`\n}\n\n/** Protects an MCP server with Cauthy. */\nexport function mcpAuth(options: McpAuthOptions = {}): McpAuth {\n const rawResource = options.resource ?? env('CAUTHY_MCP_RESOURCE') ?? ''\n if (rawResource === '') throw new Error('cauthy-auth: resource (CAUTHY_MCP_RESOURCE) is not set')\n const resource = canonicalResource(rawResource)\n const issuer = (options.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new Error('cauthy-auth: issuer (CAUTHY_ISSUER) is not set')\n const required = options.scopes ?? []\n const url = new URL(resource)\n const path = url.pathname === '/' ? '' : url.pathname\n const metadataPaths = [...new Set([`/.well-known/oauth-protected-resource${path}`, '/.well-known/oauth-protected-resource'])]\n const metadataUrl = `${url.origin}${metadataPaths[0]}`\n\n const metadata = () => ({\n resource,\n authorization_servers: [issuer],\n bearer_methods_supported: ['header'],\n scopes_supported: options.scopesSupported ?? required,\n ...(options.resourceName ? { resource_name: options.resourceName } : {}),\n })\n\n const introspect = options.introspect ? { cacheMs: ((typeof options.introspect === 'object' ? options.introspect.cacheSeconds : undefined) ?? 30) * 1000 } : null\n const answers = new Map<string, { active: boolean; until: number }>()\n const stillAllowed = async (token: string, cacheMs: number): Promise<boolean> => {\n const now = Date.now()\n const kept = answers.get(token)\n if (kept && kept.until > now) return kept.active\n let active = false\n try {\n const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/introspect`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },\n body: new URLSearchParams({ token }),\n })\n if (!res.ok) throw new Error(`introspection answered ${res.status}`)\n active = ((await res.json()) as { active?: unknown }).active === true\n } catch (err) {\n // Cauthy could not be asked: what it last said holds.\n if (kept) return kept.active\n throw new McpAuthError(401, 'invalid_token', `could not check the token with Cauthy: ${err instanceof Error ? err.message : 'unreachable'}`)\n }\n if (answers.size > 10_000) answers.clear()\n answers.set(token, { active, until: now + cacheMs })\n return active\n }\n\n const verify = async (from: string | Request | Headers, needs: string[] = []): Promise<AgentIdentity> => {\n const token = bearer(from)\n if (token === '') throw new McpAuthError(401, 'invalid_request', 'no access token')\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: resource,\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now?.(),\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new McpAuthError(401, 'invalid_token', err instanceof Error ? err.message : 'the token is not valid')\n }\n const clientId = typeof claims.client_id === 'string' ? claims.client_id : ''\n if (clientId === '') throw new McpAuthError(401, 'invalid_token', 'not a token issued to an agent')\n const scopes = typeof claims.scope === 'string' ? claims.scope.split(' ').filter(Boolean) : []\n if (introspect && !(await stillAllowed(token, introspect.cacheMs))) throw new McpAuthError(401, 'invalid_token', 'the person disconnected this agent')\n const missing = [...required, ...needs].filter((s) => !scopes.includes(s))\n if (missing.length > 0) throw new McpAuthError(403, 'insufficient_scope', `this needs ${missing.join(' ')}`)\n return {\n userId: String(claims.sub),\n tenantId: typeof claims.tid === 'string' ? claims.tid : '',\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n scopes,\n clientId,\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n }\n }\n\n const authenticate = (error?: McpAuthError): string => {\n const parts = [`resource_metadata=${quoted(metadataUrl)}`]\n if (error && error.code !== 'invalid_request') {\n parts.push(`error=${quoted(error.code)}`, `error_description=${quoted(error.description)}`)\n }\n // What to ask the person for: said when there is no token or it falls short.\n const scope = error?.code === 'invalid_token' ? [] : required\n if (scope.length > 0) parts.push(`scope=${quoted(scope.join(' '))}`)\n return `Bearer ${parts.join(', ')}`\n }\n\n const challenge = (error?: McpAuthError): Response => {\n const status = error?.status ?? 401\n return new Response(JSON.stringify({ error: error?.code ?? 'invalid_request', error_description: error?.description ?? 'an access token is required' }), {\n status,\n headers: { 'content-type': 'application/json', 'www-authenticate': authenticate(error) },\n })\n }\n\n const metadataResponse = () =>\n new Response(JSON.stringify(metadata()), {\n headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=3600', 'access-control-allow-origin': '*' },\n })\n\n const isMetadata = (pathname: string, method = 'GET') => method === 'GET' && metadataPaths.includes(pathname.replace(/\\/+$/, '') || '/')\n\n const handleMetadata = (request: Request): Response | null =>\n isMetadata(new URL(request.url).pathname, request.method) ? metadataResponse() : null\n\n const toAuthInfo = (a: AgentIdentity): McpAuthInfo => ({\n token: a.token,\n clientId: a.clientId,\n scopes: a.scopes,\n expiresAt: Math.floor(new Date(a.expiresAt).getTime() / 1000),\n resource: new URL(resource),\n extra: { userId: a.userId, tenantId: a.tenantId, ...(a.email ? { email: a.email } : {}), roles: a.roles },\n })\n\n return {\n resource,\n metadataUrl,\n metadataPaths,\n metadata,\n verify,\n challenge,\n handleMetadata,\n protect(handler, needs) {\n return async (request) => {\n const meta = handleMetadata(request)\n if (meta) return meta\n let agent: AgentIdentity\n try {\n agent = await verify(request, needs)\n } catch (err) {\n if (err instanceof McpAuthError) return challenge(err)\n throw err\n }\n return handler(request, agent)\n }\n },\n middleware(needs) {\n return (req, res, next) => {\n const pathname = (req.originalUrl ?? req.url ?? '/').split('?')[0] ?? '/'\n if (isMetadata(pathname, req.method)) {\n res.statusCode = 200\n res.setHeader('content-type', 'application/json')\n res.setHeader('cache-control', 'public, max-age=3600')\n res.setHeader('access-control-allow-origin', '*')\n res.end(JSON.stringify(metadata()))\n return\n }\n const raw = req.headers.authorization\n const header = Array.isArray(raw) ? (raw[0] ?? '') : (raw ?? '')\n verify(/^Bearer\\s+/i.test(header) ? header : '', needs).then(\n (agent) => {\n req.agent = agent\n req.auth = toAuthInfo(agent)\n next()\n },\n (err: unknown) => {\n if (!(err instanceof McpAuthError)) return next(err)\n res.statusCode = err.status\n res.setHeader('content-type', 'application/json')\n res.setHeader('www-authenticate', authenticate(err))\n res.end(JSON.stringify({ error: err.code, error_description: err.description }))\n },\n )\n }\n },\n }\n}\n","// A backend that is handed a Cauthy access token: is it real, and whose is it?\n//\n// const who = await verifyToken(request) // or the token itself\n// who.userId, who.tenantId, who.roles\n//\n// Cauthy signs its tokens with a key it publishes (/.well-known/jwks.json), so\n// checking one needs no secret and no call to Cauthy per request: the keys are\n// fetched once and cached. Throws `CauthyTokenError` for anything else: answer\n// it 401.\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyGetKey } from 'jose'\n\n/** Who a verified access token belongs to. */\nexport interface TokenIdentity {\n /** The person. For a system's own token, the client it identifies as. */\n userId: string\n tenantId: string\n email?: string\n roles: string[]\n /** Set when the token is a system's own and not a person's. */\n system?: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived, to pass on. */\n token: string\n}\n\nexport class CauthyTokenError extends Error {\n constructor(readonly reason: string) {\n super(`cauthy-auth: token refused: ${reason}`)\n this.name = 'CauthyTokenError'\n }\n}\n\nexport interface VerifyTokenOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The audience the tokens must carry. Default: `CAUTHY_AUDIENCE`; unchecked when neither is set. */\n audience?: string\n /** Refuse a token of any other workspace. Default: `CAUTHY_TENANT_ID`; unchecked when neither is set. */\n tenantId?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: Date\n}\n\nconst keys = new Map<string, JWTVerifyGetKey>()\n\n/** Cauthy's published keys, fetched once per issuer and cached. */\nexport function keysOf(issuer: string): JWTVerifyGetKey {\n let set = keys.get(issuer)\n if (set === undefined) {\n set = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 30_000 })\n keys.set(issuer, set)\n }\n return set\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\n/**\n * Verifies an access token (or the `Authorization: Bearer` of a request) and\n * answers who it belongs to. Checked: the signature, against Cauthy's\n * published keys; that Cauthy issued it; that it has not expired; and the\n * audience and the workspace, when given.\n */\nexport async function verifyToken(from: string | Request | Headers, options: VerifyTokenOptions = {}): Promise<TokenIdentity> {\n const issuer = (options.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new CauthyTokenError('issuer (CAUTHY_ISSUER) is not set')\n const token = bearer(from)\n if (token === '') throw new CauthyTokenError('no token')\n\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: options.audience ?? env('CAUTHY_AUDIENCE'),\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now,\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new CauthyTokenError(err instanceof Error ? err.message : 'bad signature')\n }\n\n const tenantId = typeof claims.tid === 'string' ? claims.tid : ''\n const wanted = options.tenantId ?? env('CAUTHY_TENANT_ID')\n if (wanted && tenantId !== wanted) throw new CauthyTokenError('issued for another workspace')\n\n return {\n userId: String(claims.sub),\n tenantId,\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n ...(typeof claims.sys === 'string' && claims.sys !== '' ? { system: claims.sys } : {}),\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n }\n}\n"],"mappings":"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","names":["jwtVerify","jwtVerify"]}
|
|
1
|
+
{"version":3,"sources":["../src/mcp.ts","../src/verify.ts"],"sourcesContent":["// An MCP server (or any API) that AI agents use as one of your people.\n//\n// import { mcpAuth } from 'cauthy-auth/mcp'\n// const auth = mcpAuth({ resource: 'https://mcp.example.com/mcp', scopes: ['books:read'] })\n//\n// // Fetch-style (Hono, Next.js, Workers, Bun, Deno):\n// export default { fetch: auth.protect((request, agent) => handleMcp(request, agent)) }\n//\n// // Express or plain Node, with the MCP TypeScript SDK (its transport reads req.auth):\n// app.use(auth.middleware())\n//\n// How an agent gets in, as the MCP specification lays it out: it calls the\n// server, which answers 401 and points at its protected resource metadata\n// (RFC 9728); that names Cauthy as the authorization server; the agent\n// registers itself there, sends the person to sign in and allow it, and comes\n// back with a token issued for this server alone (its `aud`). This module is\n// the server's half: the metadata, the 401 that points at it, and checking\n// the token. The resource must be registered in Cauthy's dashboard (AI\n// agents), with the same address, for the workspace whose people use it.\n//\n// A server that calls another of the workspace's servers for the person\n// trades the agent's token for one for that server (token exchange, RFC 8693):\n//\n// const auth = mcpAuth({ resource, clientId, clientSecret }) // from the dashboard\n// const { accessToken } = await auth.exchange(agent, 'https://api.example.com', ['orders:read'])\n//\n// The workspace's admins must have delegated that call in the dashboard, and\n// the person allowed it when they connected the agent.\n\nimport { jwtVerify, type JWTVerifyGetKey } from 'jose'\nimport { keysOf } from './verify'\n\n/** The person an agent acts for, and what they allowed it. */\nexport interface AgentIdentity {\n /** The person. */\n userId: string\n tenantId: string\n email?: string\n /** The person's roles in the workspace. */\n roles: string[]\n /** What the person allowed the agent to do here. */\n scopes: string[]\n /** The agent (its client_id at Cauthy). */\n clientId: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived. */\n token: string\n /**\n * Set when the token came by token exchange: the server that called this\n * one for the person (the `act` claim's subject).\n */\n actor?: string\n}\n\nexport interface McpAuthOptions {\n /** This server's address, as registered in Cauthy. Default: `CAUTHY_MCP_RESOURCE`. */\n resource?: string\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** Scopes every call needs. Default: none. */\n scopes?: string[]\n /** The scopes this server understands, to tell agents (`scopes_supported`). Default: `scopes`. */\n scopesSupported?: string[]\n /** A name for the server, shown by some agents. */\n resourceName?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: () => Date\n /**\n * Ask Cauthy, besides checking the token here, whether the person still\n * allows the agent (RFC 7662). Then an agent is cut off the moment it is\n * disconnected, not when its token runs out, at the price of a call to\n * Cauthy per token every `cacheSeconds` (default 30). Default: off.\n */\n introspect?: boolean | { cacheSeconds?: number }\n /** The fetch to call Cauthy with. Default: the global one. */\n fetch?: typeof fetch\n /**\n * This server's client_id and secret at Cauthy, for token exchange (AI\n * agents → the server → Server credentials). Defaults:\n * `CAUTHY_MCP_CLIENT_ID`, `CAUTHY_MCP_CLIENT_SECRET`.\n */\n clientId?: string\n clientSecret?: string\n}\n\n/** What a token exchange answers. */\nexport interface ExchangedToken {\n /** The token for the other server, as the person. Send it as `Authorization: Bearer`. */\n accessToken: string\n /** What it allows there. */\n scopes: string[]\n /** UTC, ISO 8601. Never later than the agent's own token. */\n expiresAt: string\n}\n\n/** A token exchange Cauthy refused, with the OAuth error it answered. */\nexport class CauthyExchangeError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n readonly description: string,\n ) {\n super(`cauthy-auth: token exchange refused (${code}): ${description}`)\n this.name = 'CauthyExchangeError'\n }\n}\n\n/** A token refused: `status` is 401 (no token, or not a good one) or 403 (a scope missing). */\nexport class McpAuthError extends Error {\n constructor(\n readonly status: 401 | 403,\n readonly code: 'invalid_request' | 'invalid_token' | 'insufficient_scope',\n readonly description: string,\n ) {\n super(`cauthy-auth: ${description}`)\n this.name = 'McpAuthError'\n }\n}\n\n/** What the MCP TypeScript SDK reads from `req.auth` (its AuthInfo). */\nexport interface McpAuthInfo {\n token: string\n clientId: string\n scopes: string[]\n /** Seconds since the epoch. */\n expiresAt: number\n resource: URL\n extra: { userId: string; tenantId: string; email?: string; roles: string[] }\n}\n\nexport interface McpAuth {\n /** This server's address, in the form Cauthy compares it in. */\n resource: string\n /** Where the protected resource metadata is served. */\n metadataUrl: string\n /** The protected resource metadata (RFC 9728). */\n metadata(): Record<string, unknown>\n /** The metadata's paths: `/.well-known/oauth-protected-resource` plus this server's path. */\n metadataPaths: string[]\n /** Verifies the `Authorization: Bearer` of a request (or a token). Throws `McpAuthError`. */\n verify(from: string | Request | Headers, needs?: string[]): Promise<AgentIdentity>\n /** The answer to a refused token: 401 or 403, with the `WWW-Authenticate` that points an agent at Cauthy. */\n challenge(error?: McpAuthError): Response\n /** The metadata, when the request asks for it; `null` otherwise. */\n handleMetadata(request: Request): Response | null\n /** Wraps a fetch-style handler: serves the metadata, refuses what has no good token, passes the rest with who it is. */\n protect(handler: (request: Request, agent: AgentIdentity) => Response | Promise<Response>, needs?: string[]): (request: Request) => Promise<Response>\n /**\n * Connect-style middleware (Express, Node's http): serves the metadata,\n * refuses what has no good token, and sets `req.agent` and `req.auth` —\n * what the MCP TypeScript SDK's transport hands its tools as `authInfo`.\n */\n middleware(needs?: string[]): (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => void\n /**\n * Trades the agent's token this server received for one for another of the\n * workspace's servers, to call it for the person (RFC 8693). Needs this\n * server's `clientId` and `clientSecret`. Answers are kept until shortly\n * before they expire. Throws `CauthyExchangeError` when Cauthy refuses.\n */\n exchange(from: string | AgentIdentity, target: string, scopes?: string[]): Promise<ExchangedToken>\n}\n\n// The slice of Node's (and Express's) request and response this needs.\ninterface NodeRequest {\n method?: string\n url?: string\n originalUrl?: string\n headers: Record<string, string | string[] | undefined>\n auth?: McpAuthInfo\n agent?: AgentIdentity\n}\ninterface NodeResponse {\n statusCode: number\n setHeader(name: string, value: string): unknown\n end(body?: string): unknown\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\n/**\n * The address in the one form Cauthy keeps it: scheme and host in lower case,\n * no default port, no trailing slash, no query, no fragment.\n */\nexport function canonicalResource(raw: string): string {\n let u: URL\n try {\n u = new URL(raw.trim())\n } catch {\n throw new Error(`cauthy-auth: resource ${JSON.stringify(raw)} is not an absolute address`)\n }\n const local = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]'\n if (u.protocol !== 'https:' && !(u.protocol === 'http:' && local)) {\n throw new Error('cauthy-auth: resource must be an https address')\n }\n if (u.search !== '' || u.hash !== '') throw new Error('cauthy-auth: resource may not have a query or a fragment')\n return `${u.protocol}//${u.host}${u.pathname.replace(/\\/+$/, '')}`\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\nfunction isActor(v: unknown): v is { sub: string } {\n return typeof v === 'object' && v !== null && typeof (v as { sub?: unknown }).sub === 'string'\n}\n\nfunction quoted(v: string): string {\n return `\"${v.replace(/[\"\\\\]/g, '')}\"`\n}\n\n/** Protects an MCP server with Cauthy. */\nexport function mcpAuth(options: McpAuthOptions = {}): McpAuth {\n const rawResource = options.resource ?? env('CAUTHY_MCP_RESOURCE') ?? ''\n if (rawResource === '') throw new Error('cauthy-auth: resource (CAUTHY_MCP_RESOURCE) is not set')\n const resource = canonicalResource(rawResource)\n const issuer = (options.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new Error('cauthy-auth: issuer (CAUTHY_ISSUER) is not set')\n const required = options.scopes ?? []\n const url = new URL(resource)\n const path = url.pathname === '/' ? '' : url.pathname\n const metadataPaths = [...new Set([`/.well-known/oauth-protected-resource${path}`, '/.well-known/oauth-protected-resource'])]\n const metadataUrl = `${url.origin}${metadataPaths[0]}`\n\n const metadata = () => ({\n resource,\n authorization_servers: [issuer],\n bearer_methods_supported: ['header'],\n scopes_supported: options.scopesSupported ?? required,\n ...(options.resourceName ? { resource_name: options.resourceName } : {}),\n })\n\n const introspect = options.introspect ? { cacheMs: ((typeof options.introspect === 'object' ? options.introspect.cacheSeconds : undefined) ?? 30) * 1000 } : null\n const answers = new Map<string, { active: boolean; until: number }>()\n const stillAllowed = async (token: string, cacheMs: number): Promise<boolean> => {\n const now = Date.now()\n const kept = answers.get(token)\n if (kept && kept.until > now) return kept.active\n let active = false\n try {\n const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/introspect`, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', accept: 'application/json' },\n body: new URLSearchParams({ token }),\n })\n if (!res.ok) throw new Error(`introspection answered ${res.status}`)\n active = ((await res.json()) as { active?: unknown }).active === true\n } catch (err) {\n // Cauthy could not be asked: what it last said holds.\n if (kept) return kept.active\n throw new McpAuthError(401, 'invalid_token', `could not check the token with Cauthy: ${err instanceof Error ? err.message : 'unreachable'}`)\n }\n if (answers.size > 10_000) answers.clear()\n answers.set(token, { active, until: now + cacheMs })\n return active\n }\n\n const verify = async (from: string | Request | Headers, needs: string[] = []): Promise<AgentIdentity> => {\n const token = bearer(from)\n if (token === '') throw new McpAuthError(401, 'invalid_request', 'no access token')\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: resource,\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now?.(),\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new McpAuthError(401, 'invalid_token', err instanceof Error ? err.message : 'the token is not valid')\n }\n const clientId = typeof claims.client_id === 'string' ? claims.client_id : ''\n if (clientId === '') throw new McpAuthError(401, 'invalid_token', 'not a token issued to an agent')\n const scopes = typeof claims.scope === 'string' ? claims.scope.split(' ').filter(Boolean) : []\n if (introspect && !(await stillAllowed(token, introspect.cacheMs))) throw new McpAuthError(401, 'invalid_token', 'the token is no longer allowed: the agent was disconnected, or its access withdrawn')\n const missing = [...required, ...needs].filter((s) => !scopes.includes(s))\n if (missing.length > 0) throw new McpAuthError(403, 'insufficient_scope', `this needs ${missing.join(' ')}`)\n return {\n userId: String(claims.sub),\n tenantId: typeof claims.tid === 'string' ? claims.tid : '',\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n scopes,\n clientId,\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n ...(isActor(claims.act) ? { actor: claims.act.sub } : {}),\n }\n }\n\n const clientId = options.clientId ?? env('CAUTHY_MCP_CLIENT_ID') ?? ''\n const clientSecret = options.clientSecret ?? env('CAUTHY_MCP_CLIENT_SECRET') ?? ''\n const exchanged = new Map<string, ExchangedToken & { until: number }>()\n const exchange = async (from: string | AgentIdentity, target: string, scopes: string[] = []): Promise<ExchangedToken> => {\n if (clientId === '' || clientSecret === '') {\n throw new Error('cauthy-auth: token exchange needs clientId and clientSecret (CAUTHY_MCP_CLIENT_ID, CAUTHY_MCP_CLIENT_SECRET)')\n }\n const subject = typeof from === 'string' ? bearer(from) : from.token\n const to = canonicalResource(target)\n const key = `${subject}\\n${to}\\n${scopes.join(' ')}`\n const kept = exchanged.get(key)\n if (kept && kept.until > Date.now()) return { accessToken: kept.accessToken, scopes: kept.scopes, expiresAt: kept.expiresAt }\n const res = await (options.fetch ?? fetch)(`${issuer}/v1/oauth/token`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n accept: 'application/json',\n authorization: `Basic ${btoa(`${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`)}`,\n },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',\n subject_token: subject,\n subject_token_type: 'urn:ietf:params:oauth:token-type:access_token',\n resource: to,\n ...(scopes.length > 0 ? { scope: scopes.join(' ') } : {}),\n }),\n })\n const body = (await res.json().catch(() => ({}))) as Record<string, unknown>\n if (!res.ok || typeof body.access_token !== 'string') {\n throw new CauthyExchangeError(res.status, typeof body.error === 'string' ? body.error : 'server_error',\n typeof body.error_description === 'string' ? body.error_description : `Cauthy answered ${res.status}`)\n }\n const expiresIn = typeof body.expires_in === 'number' ? body.expires_in : 0\n const out: ExchangedToken = {\n accessToken: body.access_token,\n scopes: typeof body.scope === 'string' ? body.scope.split(' ').filter(Boolean) : [],\n expiresAt: new Date(Date.now() + expiresIn * 1000).toISOString(),\n }\n if (exchanged.size > 10_000) exchanged.clear()\n // Kept until 30 seconds before it runs out, so it is never sent stale.\n exchanged.set(key, { ...out, until: Date.now() + Math.max(0, expiresIn - 30) * 1000 })\n return out\n }\n\n const authenticate = (error?: McpAuthError): string => {\n const parts = [`resource_metadata=${quoted(metadataUrl)}`]\n if (error && error.code !== 'invalid_request') {\n parts.push(`error=${quoted(error.code)}`, `error_description=${quoted(error.description)}`)\n }\n // What to ask the person for: said when there is no token or it falls short.\n const scope = error?.code === 'invalid_token' ? [] : required\n if (scope.length > 0) parts.push(`scope=${quoted(scope.join(' '))}`)\n return `Bearer ${parts.join(', ')}`\n }\n\n const challenge = (error?: McpAuthError): Response => {\n const status = error?.status ?? 401\n return new Response(JSON.stringify({ error: error?.code ?? 'invalid_request', error_description: error?.description ?? 'an access token is required' }), {\n status,\n headers: { 'content-type': 'application/json', 'www-authenticate': authenticate(error) },\n })\n }\n\n const metadataResponse = () =>\n new Response(JSON.stringify(metadata()), {\n headers: { 'content-type': 'application/json', 'cache-control': 'public, max-age=3600', 'access-control-allow-origin': '*' },\n })\n\n const isMetadata = (pathname: string, method = 'GET') => method === 'GET' && metadataPaths.includes(pathname.replace(/\\/+$/, '') || '/')\n\n const handleMetadata = (request: Request): Response | null =>\n isMetadata(new URL(request.url).pathname, request.method) ? metadataResponse() : null\n\n const toAuthInfo = (a: AgentIdentity): McpAuthInfo => ({\n token: a.token,\n clientId: a.clientId,\n scopes: a.scopes,\n expiresAt: Math.floor(new Date(a.expiresAt).getTime() / 1000),\n resource: new URL(resource),\n extra: { userId: a.userId, tenantId: a.tenantId, ...(a.email ? { email: a.email } : {}), roles: a.roles },\n })\n\n return {\n resource,\n metadataUrl,\n metadataPaths,\n metadata,\n verify,\n challenge,\n handleMetadata,\n protect(handler, needs) {\n return async (request) => {\n const meta = handleMetadata(request)\n if (meta) return meta\n let agent: AgentIdentity\n try {\n agent = await verify(request, needs)\n } catch (err) {\n if (err instanceof McpAuthError) return challenge(err)\n throw err\n }\n return handler(request, agent)\n }\n },\n exchange,\n middleware(needs) {\n return (req, res, next) => {\n const pathname = (req.originalUrl ?? req.url ?? '/').split('?')[0] ?? '/'\n if (isMetadata(pathname, req.method)) {\n res.statusCode = 200\n res.setHeader('content-type', 'application/json')\n res.setHeader('cache-control', 'public, max-age=3600')\n res.setHeader('access-control-allow-origin', '*')\n res.end(JSON.stringify(metadata()))\n return\n }\n const raw = req.headers.authorization\n const header = Array.isArray(raw) ? (raw[0] ?? '') : (raw ?? '')\n verify(/^Bearer\\s+/i.test(header) ? header : '', needs).then(\n (agent) => {\n req.agent = agent\n req.auth = toAuthInfo(agent)\n next()\n },\n (err: unknown) => {\n if (!(err instanceof McpAuthError)) return next(err)\n res.statusCode = err.status\n res.setHeader('content-type', 'application/json')\n res.setHeader('www-authenticate', authenticate(err))\n res.end(JSON.stringify({ error: err.code, error_description: err.description }))\n },\n )\n }\n },\n }\n}\n","// A backend that is handed a Cauthy access token: is it real, and whose is it?\n//\n// const who = await verifyToken(request) // or the token itself\n// who.userId, who.tenantId, who.roles\n//\n// Cauthy signs its tokens with a key it publishes (/.well-known/jwks.json), so\n// checking one needs no secret and no call to Cauthy per request: the keys are\n// fetched once and cached. Throws `CauthyTokenError` for anything else: answer\n// it 401.\n\nimport { createRemoteJWKSet, jwtVerify, type JWTVerifyGetKey } from 'jose'\n\n/** Who a verified access token belongs to. */\nexport interface TokenIdentity {\n /** The person. For a system's own token, the client it identifies as. */\n userId: string\n tenantId: string\n email?: string\n roles: string[]\n /** Set when the token is a system's own and not a person's. */\n system?: string\n /** UTC, ISO 8601. */\n expiresAt: string\n /** The token as it arrived, to pass on. */\n token: string\n}\n\nexport class CauthyTokenError extends Error {\n constructor(readonly reason: string) {\n super(`cauthy-auth: token refused: ${reason}`)\n this.name = 'CauthyTokenError'\n }\n}\n\nexport interface VerifyTokenOptions {\n /** Cauthy's address. Default: `CAUTHY_ISSUER`. */\n issuer?: string\n /** The audience the tokens must carry. Default: `CAUTHY_AUDIENCE`; unchecked when neither is set. */\n audience?: string\n /** Refuse a token of any other workspace. Default: `CAUTHY_TENANT_ID`; unchecked when neither is set. */\n tenantId?: string\n /** Cauthy's keys. Default: fetched from the issuer and cached. */\n jwks?: JWTVerifyGetKey\n /** The moment to verify at. Default: now. */\n now?: Date\n}\n\nconst keys = new Map<string, JWTVerifyGetKey>()\n\n/** Cauthy's published keys, fetched once per issuer and cached. */\nexport function keysOf(issuer: string): JWTVerifyGetKey {\n let set = keys.get(issuer)\n if (set === undefined) {\n set = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`), { cooldownDuration: 30_000 })\n keys.set(issuer, set)\n }\n return set\n}\n\nfunction env(name: string): string | undefined {\n return typeof process !== 'undefined' ? process.env?.[name] : undefined\n}\n\nfunction bearer(from: string | Request | Headers): string {\n if (typeof from === 'string') return from.replace(/^Bearer\\s+/i, '').trim()\n const header = (from instanceof Headers ? from : from.headers).get('authorization') ?? ''\n return /^Bearer\\s+/i.test(header) ? header.replace(/^Bearer\\s+/i, '').trim() : ''\n}\n\n/**\n * Verifies an access token (or the `Authorization: Bearer` of a request) and\n * answers who it belongs to. Checked: the signature, against Cauthy's\n * published keys; that Cauthy issued it; that it has not expired; and the\n * audience and the workspace, when given.\n */\nexport async function verifyToken(from: string | Request | Headers, options: VerifyTokenOptions = {}): Promise<TokenIdentity> {\n const issuer = (options.issuer ?? env('CAUTHY_ISSUER') ?? '').replace(/\\/+$/, '')\n if (issuer === '') throw new CauthyTokenError('issuer (CAUTHY_ISSUER) is not set')\n const token = bearer(from)\n if (token === '') throw new CauthyTokenError('no token')\n\n let claims: Record<string, unknown>\n try {\n const verified = await jwtVerify(token, options.jwks ?? keysOf(issuer), {\n algorithms: ['RS256'],\n issuer,\n audience: options.audience ?? env('CAUTHY_AUDIENCE'),\n requiredClaims: ['exp', 'sub'],\n currentDate: options.now,\n })\n claims = verified.payload as Record<string, unknown>\n } catch (err) {\n throw new CauthyTokenError(err instanceof Error ? err.message : 'bad signature')\n }\n\n const tenantId = typeof claims.tid === 'string' ? claims.tid : ''\n const wanted = options.tenantId ?? env('CAUTHY_TENANT_ID')\n if (wanted && tenantId !== wanted) throw new CauthyTokenError('issued for another workspace')\n\n return {\n userId: String(claims.sub),\n tenantId,\n ...(typeof claims.email === 'string' && claims.email !== '' ? { email: claims.email } : {}),\n roles: Array.isArray(claims.roles) ? claims.roles.filter((r): r is string => typeof r === 'string') : [],\n ...(typeof claims.sys === 'string' && claims.sys !== '' ? { system: claims.sys } : {}),\n expiresAt: new Date((claims.exp as number) * 1000).toISOString(),\n token,\n }\n}\n"],"mappings":"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","names":["jwtVerify","jwtVerify","clientId"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cauthy-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Sign in with Cauthy: one route to mount, getSession on the server, useSession in the browser. No database, no screens to build.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "dist/index.js",
|