@grackle-ai/auth 0.72.3
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 +26 -0
- package/dist/api-key.d.ts +12 -0
- package/dist/api-key.d.ts.map +1 -0
- package/dist/api-key.js +65 -0
- package/dist/api-key.js.map +1 -0
- package/dist/auth-context.d.ts +20 -0
- package/dist/auth-context.d.ts.map +1 -0
- package/dist/auth-context.js +2 -0
- package/dist/auth-context.js.map +1 -0
- package/dist/auth-logger.d.ts +12 -0
- package/dist/auth-logger.d.ts.map +1 -0
- package/dist/auth-logger.js +24 -0
- package/dist/auth-logger.js.map +1 -0
- package/dist/auth-middleware.d.ts +16 -0
- package/dist/auth-middleware.d.ts.map +1 -0
- package/dist/auth-middleware.js +93 -0
- package/dist/auth-middleware.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth-token.d.ts +38 -0
- package/dist/oauth-token.d.ts.map +1 -0
- package/dist/oauth-token.js +101 -0
- package/dist/oauth-token.js.map +1 -0
- package/dist/oauth.d.ts +102 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +225 -0
- package/dist/oauth.js.map +1 -0
- package/dist/pairing.d.ts +22 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +136 -0
- package/dist/pairing.js.map +1 -0
- package/dist/scoped-token.d.ts +55 -0
- package/dist/scoped-token.d.ts.map +1 -0
- package/dist/scoped-token.js +131 -0
- package/dist/scoped-token.js.map +1 -0
- package/dist/security-headers.d.ts +17 -0
- package/dist/security-headers.d.ts.map +1 -0
- package/dist/security-headers.js +31 -0
- package/dist/security-headers.js.map +1 -0
- package/dist/session.d.ts +36 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +143 -0
- package/dist/session.js.map +1 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @grackle-ai/auth
|
|
2
|
+
|
|
3
|
+
Authentication and authorization primitives for [Grackle](https://github.com/nick-pape/grackle).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **API key management** — Generate, persist, and verify 256-bit API keys with constant-time comparison
|
|
8
|
+
- **Browser sessions** — HMAC-signed cookie sessions with automatic expiry and cleanup
|
|
9
|
+
- **Pairing codes** — Time-limited 6-character codes with IP-based rate limiting for device pairing
|
|
10
|
+
- **OAuth 2.1** — Dynamic client registration, PKCE (S256), authorization codes, and refresh token rotation
|
|
11
|
+
- **HMAC-signed tokens** — OAuth access tokens and scoped task tokens for MCP authentication
|
|
12
|
+
- **MCP request auth** — Middleware that authenticates API key, OAuth, and scoped token bearers
|
|
13
|
+
- **Security headers** — CSP and defense-in-depth headers for HTTP responses
|
|
14
|
+
|
|
15
|
+
## Logger Configuration
|
|
16
|
+
|
|
17
|
+
Auth modules use a pluggable logger. Call `setAuthLogger()` at startup to inject your application logger:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { setAuthLogger } from "@grackle-ai/auth";
|
|
21
|
+
import { logger } from "./my-logger.js";
|
|
22
|
+
|
|
23
|
+
setAuthLogger(logger);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If not configured, a default console-based logger is used.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load or create the API key. On first run, a random 256-bit key is
|
|
3
|
+
* generated and written to `<homePath>/api-key` with 0600 permissions.
|
|
4
|
+
*
|
|
5
|
+
* @param homePath - The Grackle home directory (e.g., `~/.grackle`).
|
|
6
|
+
*/
|
|
7
|
+
export declare function loadOrCreateApiKey(homePath: string): string;
|
|
8
|
+
/** Verify a bearer token matches the API key. */
|
|
9
|
+
export declare function verifyApiKey(token: string): boolean;
|
|
10
|
+
/** Reset cached key (for testing). */
|
|
11
|
+
export declare function _resetCachedKeyForTesting(): void;
|
|
12
|
+
//# sourceMappingURL=api-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.d.ts","sourceRoot":"","sources":["../src/api-key.ts"],"names":[],"mappings":"AAqCA;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ3D;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAcnD;AAED,sCAAsC;AACtC,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD"}
|
package/dist/api-key.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { API_KEY_FILENAME } from "@grackle-ai/common";
|
|
5
|
+
import { getAuthLogger } from "./auth-logger.js";
|
|
6
|
+
const API_KEY_BYTE_LENGTH = 32;
|
|
7
|
+
let cachedKey = undefined;
|
|
8
|
+
/** Attempt to read an existing API key from disk. Returns undefined if none exists. */
|
|
9
|
+
function tryLoadApiKey(keyPath) {
|
|
10
|
+
if (existsSync(keyPath)) {
|
|
11
|
+
const content = readFileSync(keyPath, "utf8").trim();
|
|
12
|
+
if (content.length > 0) {
|
|
13
|
+
return content;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Generate a new random API key, write it to disk with 0600 permissions, and return it. */
|
|
19
|
+
function createApiKey(homePath, keyPath) {
|
|
20
|
+
const key = randomBytes(API_KEY_BYTE_LENGTH).toString("hex");
|
|
21
|
+
mkdirSync(homePath, { recursive: true });
|
|
22
|
+
writeFileSync(keyPath, key + "\n", { mode: 0o600 });
|
|
23
|
+
// Ensure permissions on Windows (best-effort)
|
|
24
|
+
try {
|
|
25
|
+
chmodSync(keyPath, 0o600);
|
|
26
|
+
}
|
|
27
|
+
catch { /* Windows may not support this */ }
|
|
28
|
+
getAuthLogger().info({ keyPath }, "Generated new API key");
|
|
29
|
+
return key;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Load or create the API key. On first run, a random 256-bit key is
|
|
33
|
+
* generated and written to `<homePath>/api-key` with 0600 permissions.
|
|
34
|
+
*
|
|
35
|
+
* @param homePath - The Grackle home directory (e.g., `~/.grackle`).
|
|
36
|
+
*/
|
|
37
|
+
export function loadOrCreateApiKey(homePath) {
|
|
38
|
+
if (cachedKey) {
|
|
39
|
+
return cachedKey;
|
|
40
|
+
}
|
|
41
|
+
const keyPath = join(homePath, API_KEY_FILENAME);
|
|
42
|
+
cachedKey = tryLoadApiKey(keyPath) ?? createApiKey(homePath, keyPath);
|
|
43
|
+
return cachedKey;
|
|
44
|
+
}
|
|
45
|
+
/** Verify a bearer token matches the API key. */
|
|
46
|
+
export function verifyApiKey(token) {
|
|
47
|
+
if (!cachedKey) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Constant-time comparison to prevent timing attacks
|
|
51
|
+
if (token.length !== cachedKey.length) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
let result = 0;
|
|
55
|
+
for (let i = 0; i < cachedKey.length; i++) {
|
|
56
|
+
// eslint-disable-next-line no-bitwise
|
|
57
|
+
result |= token.charCodeAt(i) ^ cachedKey.charCodeAt(i);
|
|
58
|
+
}
|
|
59
|
+
return result === 0;
|
|
60
|
+
}
|
|
61
|
+
/** Reset cached key (for testing). */
|
|
62
|
+
export function _resetCachedKeyForTesting() {
|
|
63
|
+
cachedKey = undefined;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=api-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key.js","sourceRoot":"","sources":["../src/api-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,mBAAmB,GAAW,EAAE,CAAC;AAEvC,IAAI,SAAS,GAAuB,SAAS,CAAC;AAE9C,uFAAuF;AACvF,SAAS,aAAa,CAAC,OAAe;IACpC,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;QACrD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,4FAA4F;AAC5F,SAAS,YAAY,CAAC,QAAgB,EAAE,OAAe;IACrD,MAAM,GAAG,GAAG,WAAW,CAAC,mBAAmB,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE7D,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,aAAa,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAEpD,8CAA8C;IAC9C,IAAI,CAAC;QACH,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5B,CAAC;IAAC,MAAM,CAAC,CAAC,kCAAkC,CAAC,CAAC;IAE9C,aAAa,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,uBAAuB,CAAC,CAAC;IAC3D,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC;IACjD,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC,IAAI,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACtE,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,KAAK,CAAC;IACf,CAAC;IACD,qDAAqD;IACrD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,MAAM,EAAE,CAAC;QACtC,OAAO,KAAK,CAAC;IACf,CAAC;IACD,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,sCAAsC;QACtC,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,MAAM,KAAK,CAAC,CAAC;AACtB,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,yBAAyB;IACvC,SAAS,GAAG,SAAS,CAAC;AACxB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication context resolved from an incoming MCP request.
|
|
3
|
+
*
|
|
4
|
+
* - `"api-key"`: Full-access authentication via the global API key.
|
|
5
|
+
* - `"scoped"`: Session-scoped token identifying a specific task/session/persona.
|
|
6
|
+
* - `"oauth"`: OAuth-authorized client — full tool access (user explicitly approved).
|
|
7
|
+
*/
|
|
8
|
+
export type AuthContext = {
|
|
9
|
+
type: "api-key";
|
|
10
|
+
} | {
|
|
11
|
+
type: "scoped";
|
|
12
|
+
taskId: string;
|
|
13
|
+
workspaceId?: string;
|
|
14
|
+
personaId: string;
|
|
15
|
+
taskSessionId: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "oauth";
|
|
18
|
+
clientId: string;
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=auth-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-context.d.ts","sourceRoot":"","sources":["../src/auth-context.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GACnB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,GAClG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-context.js","sourceRoot":"","sources":["../src/auth-context.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Logger interface for auth modules. */
|
|
2
|
+
export interface AuthLogger {
|
|
3
|
+
info(obj: object, msg: string, ...args: unknown[]): void;
|
|
4
|
+
warn(objOrMsg: object | string, msg?: string, ...args: unknown[]): void;
|
|
5
|
+
}
|
|
6
|
+
/** Default console-based logger. */
|
|
7
|
+
export declare const defaultAuthLogger: AuthLogger;
|
|
8
|
+
/** Set the logger used by all auth modules. Call once at startup. */
|
|
9
|
+
export declare function setAuthLogger(l: AuthLogger): void;
|
|
10
|
+
/** Get the current auth logger. */
|
|
11
|
+
export declare function getAuthLogger(): AuthLogger;
|
|
12
|
+
//# sourceMappingURL=auth-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-logger.d.ts","sourceRoot":"","sources":["../src/auth-logger.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACzE;AAED,oCAAoC;AACpC,eAAO,MAAM,iBAAiB,EAAE,UAW/B,CAAC;AAIF,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,CAEjD;AAED,mCAAmC;AACnC,wBAAgB,aAAa,IAAI,UAAU,CAE1C"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Default console-based logger. */
|
|
2
|
+
export const defaultAuthLogger = {
|
|
3
|
+
info(_obj, msg, ...args) {
|
|
4
|
+
console.log("[auth]", msg, ...args);
|
|
5
|
+
},
|
|
6
|
+
warn(objOrMsg, msg, ...args) {
|
|
7
|
+
if (typeof objOrMsg === "string") {
|
|
8
|
+
console.warn("[auth]", objOrMsg, msg, ...args);
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
console.warn("[auth]", msg, ...args);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
let logger = defaultAuthLogger;
|
|
16
|
+
/** Set the logger used by all auth modules. Call once at startup. */
|
|
17
|
+
export function setAuthLogger(l) {
|
|
18
|
+
logger = l;
|
|
19
|
+
}
|
|
20
|
+
/** Get the current auth logger. */
|
|
21
|
+
export function getAuthLogger() {
|
|
22
|
+
return logger;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=auth-logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-logger.js","sourceRoot":"","sources":["../src/auth-logger.ts"],"names":[],"mappings":"AAMA,oCAAoC;AACpC,MAAM,CAAC,MAAM,iBAAiB,GAAe;IAC3C,IAAI,CAAC,IAAY,EAAE,GAAW,EAAE,GAAG,IAAe;QAChD,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IACtC,CAAC;IACD,IAAI,CAAC,QAAyB,EAAE,GAAY,EAAE,GAAG,IAAe;QAC9D,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACjC,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QACjD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;CACF,CAAC;AAEF,IAAI,MAAM,GAAe,iBAAiB,CAAC;AAE3C,qEAAqE;AACrE,MAAM,UAAU,aAAa,CAAC,CAAa;IACzC,MAAM,GAAG,CAAC,CAAC;AACb,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,aAAa;IAC3B,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type http from "node:http";
|
|
2
|
+
import type { AuthContext } from "./auth-context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Authenticate an incoming MCP HTTP request.
|
|
5
|
+
*
|
|
6
|
+
* Supports three authentication modes:
|
|
7
|
+
* 1. **API key**: A 64-character hex Bearer token compared constant-time against the server API key.
|
|
8
|
+
* 2. **OAuth token**: An HMAC-signed token with `typ === "oauth"`, audience-validated against the request.
|
|
9
|
+
* 3. **Scoped token**: An HMAC-signed token (contains a `.`) verified against the API key as signing secret.
|
|
10
|
+
*
|
|
11
|
+
* @param req - The incoming HTTP request.
|
|
12
|
+
* @param apiKey - The server's API key (used for both direct comparison and as the HMAC signing secret).
|
|
13
|
+
* @returns An {@link AuthContext} if authentication succeeds, or `undefined` for a 401.
|
|
14
|
+
*/
|
|
15
|
+
export declare function authenticateMcpRequest(req: http.IncomingMessage, apiKey: string): AuthContext | undefined;
|
|
16
|
+
//# sourceMappingURL=auth-middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-middleware.d.ts","sourceRoot":"","sources":["../src/auth-middleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAuBrD;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CA8DzG"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { verifyOAuthAccessToken } from "./oauth-token.js";
|
|
3
|
+
import { isRevokedTask, verifyScopedToken } from "./scoped-token.js";
|
|
4
|
+
/** Expected length of API key tokens (64 hex characters). */
|
|
5
|
+
const API_KEY_LENGTH = 64;
|
|
6
|
+
/**
|
|
7
|
+
* Normalize loopback hostnames so that `localhost` and `127.0.0.1` compare equal.
|
|
8
|
+
* Parses the URL and replaces `localhost` with `127.0.0.1`, returning the origin.
|
|
9
|
+
*/
|
|
10
|
+
function normalizeLoopback(url) {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = new URL(url);
|
|
13
|
+
if (parsed.hostname === "localhost") {
|
|
14
|
+
parsed.hostname = "127.0.0.1";
|
|
15
|
+
}
|
|
16
|
+
return parsed.origin;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Authenticate an incoming MCP HTTP request.
|
|
24
|
+
*
|
|
25
|
+
* Supports three authentication modes:
|
|
26
|
+
* 1. **API key**: A 64-character hex Bearer token compared constant-time against the server API key.
|
|
27
|
+
* 2. **OAuth token**: An HMAC-signed token with `typ === "oauth"`, audience-validated against the request.
|
|
28
|
+
* 3. **Scoped token**: An HMAC-signed token (contains a `.`) verified against the API key as signing secret.
|
|
29
|
+
*
|
|
30
|
+
* @param req - The incoming HTTP request.
|
|
31
|
+
* @param apiKey - The server's API key (used for both direct comparison and as the HMAC signing secret).
|
|
32
|
+
* @returns An {@link AuthContext} if authentication succeeds, or `undefined` for a 401.
|
|
33
|
+
*/
|
|
34
|
+
export function authenticateMcpRequest(req, apiKey) {
|
|
35
|
+
const authHeader = req.headers.authorization || "";
|
|
36
|
+
const match = /^Bearer\s+(.+)$/i.exec(authHeader);
|
|
37
|
+
if (!match) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const token = match[1];
|
|
41
|
+
if (token.length === 0) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
// Path 1: API key authentication (fixed-length hex token)
|
|
45
|
+
if (token.length === API_KEY_LENGTH && apiKey.length === API_KEY_LENGTH) {
|
|
46
|
+
const a = Buffer.from(token);
|
|
47
|
+
const b = Buffer.from(apiKey);
|
|
48
|
+
if (a.length === b.length && timingSafeEqual(a, b)) {
|
|
49
|
+
return { type: "api-key" };
|
|
50
|
+
}
|
|
51
|
+
// Fall through — a 64-char token that doesn't match the API key is invalid
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
// Path 2: Token with dot separator — try OAuth first, then scoped
|
|
55
|
+
if (token.includes(".")) {
|
|
56
|
+
// Try OAuth access token (distinguished by typ === "oauth")
|
|
57
|
+
const oauthClaims = verifyOAuthAccessToken(token, apiKey);
|
|
58
|
+
if (oauthClaims) {
|
|
59
|
+
// Validate audience if present — when non-empty, must match this server's resource URL.
|
|
60
|
+
// Empty aud is accepted because the client may omit the resource indicator (RFC 8707).
|
|
61
|
+
// Use the socket's local port (server-controlled) rather than the Host header (client-controlled)
|
|
62
|
+
// to prevent token replay via Host spoofing.
|
|
63
|
+
// Normalize trailing slashes and treat "localhost" as equivalent to "127.0.0.1" since
|
|
64
|
+
// MCP clients may connect via either hostname.
|
|
65
|
+
if (oauthClaims.aud) {
|
|
66
|
+
const localPort = req.socket.localPort;
|
|
67
|
+
const expectedAudience = localPort ? `http://127.0.0.1:${localPort}` : undefined;
|
|
68
|
+
const normalizedAud = normalizeLoopback(oauthClaims.aud.replace(/\/+$/, ""));
|
|
69
|
+
if (!expectedAudience || normalizedAud !== expectedAudience) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { type: "oauth", clientId: oauthClaims.sub };
|
|
74
|
+
}
|
|
75
|
+
// Fall through to scoped token
|
|
76
|
+
const claims = verifyScopedToken(token, apiKey);
|
|
77
|
+
if (!claims) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (isRevokedTask(claims.sub)) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
type: "scoped",
|
|
85
|
+
taskId: claims.sub,
|
|
86
|
+
workspaceId: claims.pid || undefined,
|
|
87
|
+
personaId: claims.per,
|
|
88
|
+
taskSessionId: claims.sid,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=auth-middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-middleware.js","sourceRoot":"","sources":["../src/auth-middleware.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type { AuthLogger } from "./auth-logger.js";
|
|
2
|
+
export { defaultAuthLogger, setAuthLogger } from "./auth-logger.js";
|
|
3
|
+
export { loadOrCreateApiKey, verifyApiKey } from "./api-key.js";
|
|
4
|
+
export { SESSION_COOKIE_NAME, createSession, validateSessionCookie, parseCookies, clearSessions, startSessionCleanup, stopSessionCleanup, } from "./session.js";
|
|
5
|
+
export { generatePairingCode, redeemPairingCode, clearPairing, startPairingCleanup, stopPairingCleanup, } from "./pairing.js";
|
|
6
|
+
export type { ClientRecord, AuthCodeRecord, RefreshTokenRecord } from "./oauth.js";
|
|
7
|
+
export { registerClient, getClient, createAuthorizationCode, consumeAuthorizationCode, createRefreshToken, consumeRefreshToken, computeCodeChallenge, verifyCodeChallenge, clearOAuthState, startOAuthCleanup, stopOAuthCleanup, } from "./oauth.js";
|
|
8
|
+
export type { OAuthTokenClaims } from "./oauth-token.js";
|
|
9
|
+
export { OAUTH_ACCESS_TOKEN_TTL_MS, OAUTH_REFRESH_TOKEN_TTL_MS, createOAuthAccessToken, verifyOAuthAccessToken, } from "./oauth-token.js";
|
|
10
|
+
export type { ScopedTokenClaims } from "./scoped-token.js";
|
|
11
|
+
export { createScopedToken, verifyScopedToken, revokeTask, isRevokedTask, pruneRevocations, clearRevocations, } from "./scoped-token.js";
|
|
12
|
+
export type { AuthContext } from "./auth-context.js";
|
|
13
|
+
export { authenticateMcpRequest } from "./auth-middleware.js";
|
|
14
|
+
export { WEB_CONTENT_SECURITY_POLICY, setSecurityHeaders } from "./security-headers.js";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAGpE,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAGhE,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAGtB,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AACnF,OAAO,EACL,cAAc,EACd,SAAS,EACT,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAGpB,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EACL,yBAAyB,EACzB,0BAA0B,EAC1B,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAG1B,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAG3B,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG9D,OAAO,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { defaultAuthLogger, setAuthLogger } from "./auth-logger.js";
|
|
2
|
+
// ─── API Key ────────────────────────────────────────────────
|
|
3
|
+
export { loadOrCreateApiKey, verifyApiKey } from "./api-key.js";
|
|
4
|
+
// ─── Sessions ───────────────────────────────────────────────
|
|
5
|
+
export { SESSION_COOKIE_NAME, createSession, validateSessionCookie, parseCookies, clearSessions, startSessionCleanup, stopSessionCleanup, } from "./session.js";
|
|
6
|
+
// ─── Pairing ────────────────────────────────────────────────
|
|
7
|
+
export { generatePairingCode, redeemPairingCode, clearPairing, startPairingCleanup, stopPairingCleanup, } from "./pairing.js";
|
|
8
|
+
export { registerClient, getClient, createAuthorizationCode, consumeAuthorizationCode, createRefreshToken, consumeRefreshToken, computeCodeChallenge, verifyCodeChallenge, clearOAuthState, startOAuthCleanup, stopOAuthCleanup, } from "./oauth.js";
|
|
9
|
+
export { OAUTH_ACCESS_TOKEN_TTL_MS, OAUTH_REFRESH_TOKEN_TTL_MS, createOAuthAccessToken, verifyOAuthAccessToken, } from "./oauth-token.js";
|
|
10
|
+
export { createScopedToken, verifyScopedToken, revokeTask, isRevokedTask, pruneRevocations, clearRevocations, } from "./scoped-token.js";
|
|
11
|
+
// ─── Auth Middleware ────────────────────────────────────────
|
|
12
|
+
export { authenticateMcpRequest } from "./auth-middleware.js";
|
|
13
|
+
// ─── Security Headers ──────────────────────────────────────
|
|
14
|
+
export { WEB_CONTENT_SECURITY_POLICY, setSecurityHeaders } from "./security-headers.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEpE,+DAA+D;AAC/D,OAAO,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEhE,+DAA+D;AAC/D,OAAO,EACL,mBAAmB,EACnB,aAAa,EACb,qBAAqB,EACrB,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAEtB,+DAA+D;AAC/D,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,cAAc,CAAC;AAItB,OAAO,EACL,cAAc,EACd,SAAS,EACT,uBAAuB,EACvB,wBAAwB,EACxB,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAIpB,OAAO,EACL,yBAAyB,EACzB,0BAA0B,EAC1B,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAK3B,+DAA+D;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,8DAA8D;AAC9D,OAAO,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Default access token time-to-live: 1 hour in milliseconds. */
|
|
2
|
+
export declare const OAUTH_ACCESS_TOKEN_TTL_MS: number;
|
|
3
|
+
/** Default refresh token time-to-live: 30 days in milliseconds. */
|
|
4
|
+
export declare const OAUTH_REFRESH_TOKEN_TTL_MS: number;
|
|
5
|
+
/** Claims embedded in an OAuth access token payload. */
|
|
6
|
+
export interface OAuthTokenClaims {
|
|
7
|
+
/** Token type discriminator — always "oauth" for OAuth access tokens. */
|
|
8
|
+
typ: "oauth";
|
|
9
|
+
/** Subject — the OAuth client ID that was authorized. */
|
|
10
|
+
sub: string;
|
|
11
|
+
/** Audience — the resource URL (MCP server URL) this token was issued for. */
|
|
12
|
+
aud: string;
|
|
13
|
+
/** Issued-at time (epoch seconds). */
|
|
14
|
+
iat: number;
|
|
15
|
+
/** Expiry time (epoch seconds). */
|
|
16
|
+
exp: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create an OAuth access token with the given client ID and resource, signed with the provided secret.
|
|
20
|
+
*
|
|
21
|
+
* @param clientId - The OAuth client ID (subject).
|
|
22
|
+
* @param resource - The resource URL (audience) this token is scoped to.
|
|
23
|
+
* @param signingSecret - Secret used to HMAC-sign the token (typically the API key).
|
|
24
|
+
* @param ttlMs - Token time-to-live in milliseconds (default: 1 hour).
|
|
25
|
+
* @returns The signed opaque token string.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createOAuthAccessToken(clientId: string, resource: string, signingSecret: string, ttlMs?: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Verify an OAuth access token's signature and expiry.
|
|
30
|
+
*
|
|
31
|
+
* Uses constant-time comparison for the HMAC signature.
|
|
32
|
+
*
|
|
33
|
+
* @param token - The token string to verify.
|
|
34
|
+
* @param signingSecret - The secret used to verify the HMAC signature.
|
|
35
|
+
* @returns The decoded claims if valid, or `undefined` if verification fails.
|
|
36
|
+
*/
|
|
37
|
+
export declare function verifyOAuthAccessToken(token: string, signingSecret: string): OAuthTokenClaims | undefined;
|
|
38
|
+
//# sourceMappingURL=oauth-token.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-token.d.ts","sourceRoot":"","sources":["../src/oauth-token.ts"],"names":[],"mappings":"AAEA,iEAAiE;AACjE,eAAO,MAAM,yBAAyB,EAAE,MAAuB,CAAC;AAEhE,mEAAmE;AACnE,eAAO,MAAM,0BAA0B,EAAE,MAAiC,CAAC;AAE3E,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,GAAG,EAAE,OAAO,CAAC;IACb,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,sCAAsC;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;CACb;AAiBD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAkC,GACxC,MAAM,CAaR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CA4DzG"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
/** Default access token time-to-live: 1 hour in milliseconds. */
|
|
3
|
+
export const OAUTH_ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000;
|
|
4
|
+
/** Default refresh token time-to-live: 30 days in milliseconds. */
|
|
5
|
+
export const OAUTH_REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
6
|
+
/** Encode a buffer as base64url (no padding). */
|
|
7
|
+
function toBase64Url(buf) {
|
|
8
|
+
return buf.toString("base64url");
|
|
9
|
+
}
|
|
10
|
+
/** Decode a base64url string to a Buffer. */
|
|
11
|
+
function fromBase64Url(str) {
|
|
12
|
+
return Buffer.from(str, "base64url");
|
|
13
|
+
}
|
|
14
|
+
/** Compute HMAC-SHA256 signature over a payload string. */
|
|
15
|
+
function sign(payload, secret) {
|
|
16
|
+
return createHmac("sha256", secret).update(payload).digest();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create an OAuth access token with the given client ID and resource, signed with the provided secret.
|
|
20
|
+
*
|
|
21
|
+
* @param clientId - The OAuth client ID (subject).
|
|
22
|
+
* @param resource - The resource URL (audience) this token is scoped to.
|
|
23
|
+
* @param signingSecret - Secret used to HMAC-sign the token (typically the API key).
|
|
24
|
+
* @param ttlMs - Token time-to-live in milliseconds (default: 1 hour).
|
|
25
|
+
* @returns The signed opaque token string.
|
|
26
|
+
*/
|
|
27
|
+
export function createOAuthAccessToken(clientId, resource, signingSecret, ttlMs = OAUTH_ACCESS_TOKEN_TTL_MS) {
|
|
28
|
+
const now = Math.floor(Date.now() / 1000);
|
|
29
|
+
const payload = {
|
|
30
|
+
typ: "oauth",
|
|
31
|
+
sub: clientId,
|
|
32
|
+
aud: resource,
|
|
33
|
+
iat: now,
|
|
34
|
+
exp: now + Math.floor(ttlMs / 1000),
|
|
35
|
+
};
|
|
36
|
+
const payloadStr = JSON.stringify(payload);
|
|
37
|
+
const payloadEncoded = toBase64Url(Buffer.from(payloadStr, "utf8"));
|
|
38
|
+
const signature = toBase64Url(sign(payloadEncoded, signingSecret));
|
|
39
|
+
return `${payloadEncoded}.${signature}`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Verify an OAuth access token's signature and expiry.
|
|
43
|
+
*
|
|
44
|
+
* Uses constant-time comparison for the HMAC signature.
|
|
45
|
+
*
|
|
46
|
+
* @param token - The token string to verify.
|
|
47
|
+
* @param signingSecret - The secret used to verify the HMAC signature.
|
|
48
|
+
* @returns The decoded claims if valid, or `undefined` if verification fails.
|
|
49
|
+
*/
|
|
50
|
+
export function verifyOAuthAccessToken(token, signingSecret) {
|
|
51
|
+
const dotIndex = token.indexOf(".");
|
|
52
|
+
if (dotIndex === -1 || dotIndex === 0 || dotIndex === token.length - 1) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
// Reject tokens with multiple dots
|
|
56
|
+
if (token.indexOf(".", dotIndex + 1) !== -1) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const payloadEncoded = token.slice(0, dotIndex);
|
|
60
|
+
const signatureEncoded = token.slice(dotIndex + 1);
|
|
61
|
+
// Verify signature using constant-time comparison
|
|
62
|
+
const expectedSignature = sign(payloadEncoded, signingSecret);
|
|
63
|
+
let actualSignature;
|
|
64
|
+
try {
|
|
65
|
+
actualSignature = fromBase64Url(signatureEncoded);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (expectedSignature.length !== actualSignature.length) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
if (!timingSafeEqual(expectedSignature, actualSignature)) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
// Decode and parse payload — parse as Record first for runtime validation
|
|
77
|
+
let raw;
|
|
78
|
+
try {
|
|
79
|
+
const payloadStr = fromBase64Url(payloadEncoded).toString("utf8");
|
|
80
|
+
raw = JSON.parse(payloadStr);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
// Validate claim types to prevent bypass via crafted payloads
|
|
86
|
+
if (raw.typ !== "oauth" ||
|
|
87
|
+
typeof raw.sub !== "string" ||
|
|
88
|
+
typeof raw.aud !== "string" ||
|
|
89
|
+
!Number.isFinite(raw.iat) ||
|
|
90
|
+
!Number.isFinite(raw.exp)) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const claims = raw;
|
|
94
|
+
// Check expiry (exp must be strictly greater than both iat and now)
|
|
95
|
+
const now = Math.floor(Date.now() / 1000);
|
|
96
|
+
if (claims.exp <= now || claims.exp <= claims.iat) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return claims;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=oauth-token.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-token.js","sourceRoot":"","sources":["../src/oauth-token.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Registered OAuth client record. */
|
|
2
|
+
export interface ClientRecord {
|
|
3
|
+
clientId: string;
|
|
4
|
+
redirectUris: string[];
|
|
5
|
+
clientName: string;
|
|
6
|
+
createdAt: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Register a new OAuth client with dynamic client registration.
|
|
10
|
+
*
|
|
11
|
+
* @param redirectUris - List of allowed redirect URIs for this client.
|
|
12
|
+
* @param clientName - Human-readable name for the client (optional).
|
|
13
|
+
* @returns The newly registered client record.
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerClient(redirectUris: string[], clientName?: string): ClientRecord | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Look up a registered client by ID.
|
|
18
|
+
*
|
|
19
|
+
* @param clientId - The client ID to look up.
|
|
20
|
+
* @returns The client record, or undefined if not found.
|
|
21
|
+
*/
|
|
22
|
+
export declare function getClient(clientId: string): ClientRecord | undefined;
|
|
23
|
+
/** Authorization code record bound to PKCE parameters. */
|
|
24
|
+
export interface AuthCodeRecord {
|
|
25
|
+
code: string;
|
|
26
|
+
clientId: string;
|
|
27
|
+
redirectUri: string;
|
|
28
|
+
codeChallenge: string;
|
|
29
|
+
resource: string;
|
|
30
|
+
createdAt: number;
|
|
31
|
+
expiresAt: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Create a single-use authorization code bound to the given parameters.
|
|
35
|
+
*
|
|
36
|
+
* @param clientId - The client that requested authorization.
|
|
37
|
+
* @param redirectUri - The redirect URI for this authorization.
|
|
38
|
+
* @param codeChallenge - PKCE S256 code challenge.
|
|
39
|
+
* @param resource - The resource URL the client wants to access.
|
|
40
|
+
* @returns The generated authorization code string.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createAuthorizationCode(clientId: string, redirectUri: string, codeChallenge: string, resource: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Compute the S256 code challenge from a code verifier.
|
|
45
|
+
*
|
|
46
|
+
* @param codeVerifier - The PKCE code verifier string.
|
|
47
|
+
* @returns The base64url-encoded SHA-256 hash.
|
|
48
|
+
*/
|
|
49
|
+
export declare function computeCodeChallenge(codeVerifier: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Verify that a code verifier matches a code challenge using S256.
|
|
52
|
+
*
|
|
53
|
+
* @param codeVerifier - The PKCE code verifier from the token request.
|
|
54
|
+
* @param codeChallenge - The code challenge stored at authorization time.
|
|
55
|
+
* @returns True if the verifier matches the challenge.
|
|
56
|
+
*/
|
|
57
|
+
export declare function verifyCodeChallenge(codeVerifier: string, codeChallenge: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Consume an authorization code, validating all parameters.
|
|
60
|
+
*
|
|
61
|
+
* The code is deleted regardless of whether validation succeeds (single-use).
|
|
62
|
+
*
|
|
63
|
+
* @param code - The authorization code to consume.
|
|
64
|
+
* @param clientId - The client ID making the token request.
|
|
65
|
+
* @param redirectUri - The redirect URI from the token request.
|
|
66
|
+
* @param codeVerifier - The PKCE code verifier.
|
|
67
|
+
* @param resource - The resource URL from the token request.
|
|
68
|
+
* @returns The auth code record if valid, or undefined.
|
|
69
|
+
*/
|
|
70
|
+
export declare function consumeAuthorizationCode(code: string, clientId: string, redirectUri: string, codeVerifier: string, resource: string): AuthCodeRecord | undefined;
|
|
71
|
+
/** Refresh token record with client and resource binding. */
|
|
72
|
+
export interface RefreshTokenRecord {
|
|
73
|
+
token: string;
|
|
74
|
+
clientId: string;
|
|
75
|
+
resource: string;
|
|
76
|
+
createdAt: number;
|
|
77
|
+
expiresAt: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a new refresh token for the given client and resource.
|
|
81
|
+
*
|
|
82
|
+
* @param clientId - The client this refresh token is issued to.
|
|
83
|
+
* @param resource - The resource URL this refresh token is scoped to.
|
|
84
|
+
* @returns The generated refresh token string.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createRefreshToken(clientId: string, resource: string): string;
|
|
87
|
+
/**
|
|
88
|
+
* Consume a refresh token with rotation — the old token is invalidated and
|
|
89
|
+
* a new one must be issued in its place.
|
|
90
|
+
*
|
|
91
|
+
* @param token - The refresh token to consume.
|
|
92
|
+
* @param clientId - The client ID making the refresh request.
|
|
93
|
+
* @returns The refresh token record if valid, or undefined.
|
|
94
|
+
*/
|
|
95
|
+
export declare function consumeRefreshToken(token: string, clientId: string): RefreshTokenRecord | undefined;
|
|
96
|
+
/** Start the periodic OAuth state cleanup timer. Call once on server startup. */
|
|
97
|
+
export declare function startOAuthCleanup(): void;
|
|
98
|
+
/** Stop the periodic OAuth state cleanup timer. */
|
|
99
|
+
export declare function stopOAuthCleanup(): void;
|
|
100
|
+
/** Clear all OAuth state. Intended for testing only. */
|
|
101
|
+
export declare function clearOAuthState(): void;
|
|
102
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAuBA,sCAAsC;AACtC,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAKD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAwBpG;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEpE;AAID,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAKD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,GACf,MAAM,CAeR;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAGxF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GACf,cAAc,GAAG,SAAS,CA+B5B;AAID,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAKD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAW7E;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAmBnG;AAMD,iFAAiF;AACjF,wBAAgB,iBAAiB,IAAI,IAAI,CAuBxC;AAED,mDAAmD;AACnD,wBAAgB,gBAAgB,IAAI,IAAI,CAKvC;AAED,wDAAwD;AACxD,wBAAgB,eAAe,IAAI,IAAI,CAItC"}
|