oauth-callback 2.1.1 → 2.2.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 +21 -8
- package/dist/auth/browser-auth.d.ts +1 -1
- package/dist/auth/browser-auth.d.ts.map +1 -1
- package/dist/index.js +5359 -234
- package/dist/mcp-types.d.ts +25 -15
- package/dist/mcp-types.d.ts.map +1 -1
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +105 -86
- package/dist/storage/file.d.ts +1 -1
- package/dist/storage/file.d.ts.map +1 -1
- package/dist/storage/memory.d.ts +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/auth/browser-auth.test.ts +175 -65
- package/src/auth/browser-auth.ts +160 -116
- package/src/mcp-types.ts +29 -16
- package/src/mcp.ts +1 -1
- package/src/storage/file.ts +4 -7
- package/src/storage/memory.ts +1 -5
package/dist/mcp-types.d.ts
CHANGED
|
@@ -18,34 +18,30 @@ export interface ClientInfo {
|
|
|
18
18
|
clientIdIssuedAt?: number;
|
|
19
19
|
clientSecretExpiresAt?: number;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Active OAuth flow state for crash recovery.
|
|
23
|
-
* Preserves PKCE verifier and state across process restarts.
|
|
24
|
-
*/
|
|
25
|
-
export interface OAuthSession {
|
|
26
|
-
codeVerifier?: string;
|
|
27
|
-
state?: string;
|
|
28
|
-
}
|
|
29
21
|
/**
|
|
30
22
|
* Minimal storage interface for OAuth tokens.
|
|
31
|
-
* @invariant Implementations must be thread-safe within process.
|
|
32
23
|
* @invariant Keys are scoped to avoid collisions between multiple OAuth flows.
|
|
33
24
|
*/
|
|
34
25
|
export interface TokenStore {
|
|
35
26
|
get(key: string): Promise<Tokens | null>;
|
|
36
27
|
set(key: string, tokens: Tokens): Promise<void>;
|
|
37
28
|
delete(key: string): Promise<void>;
|
|
38
|
-
clear(): Promise<void>;
|
|
39
29
|
}
|
|
30
|
+
/** Brand symbol for OAuthStore type detection. */
|
|
31
|
+
export declare const OAuthStoreBrand: unique symbol;
|
|
40
32
|
/**
|
|
41
|
-
*
|
|
42
|
-
* Enables recovery
|
|
33
|
+
* Extended storage with client registration and PKCE verifier persistence.
|
|
34
|
+
* Enables crash recovery mid-flow and reuse of dynamic registration.
|
|
35
|
+
* @invariant Implementations must include `[OAuthStoreBrand]: true` property.
|
|
43
36
|
*/
|
|
44
37
|
export interface OAuthStore extends TokenStore {
|
|
38
|
+
readonly [OAuthStoreBrand]: true;
|
|
45
39
|
getClient(key: string): Promise<ClientInfo | null>;
|
|
46
40
|
setClient(key: string, client: ClientInfo): Promise<void>;
|
|
47
|
-
|
|
48
|
-
|
|
41
|
+
deleteClient(key: string): Promise<void>;
|
|
42
|
+
getCodeVerifier(key: string): Promise<string | null>;
|
|
43
|
+
setCodeVerifier(key: string, verifier: string): Promise<void>;
|
|
44
|
+
deleteCodeVerifier(key: string): Promise<void>;
|
|
49
45
|
}
|
|
50
46
|
/**
|
|
51
47
|
* Configuration for browser-based OAuth flows with MCP servers.
|
|
@@ -53,8 +49,16 @@ export interface OAuthStore extends TokenStore {
|
|
|
53
49
|
* @see https://datatracker.ietf.org/doc/html/rfc8252
|
|
54
50
|
*/
|
|
55
51
|
export interface BrowserAuthOptions {
|
|
56
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Pre-registered OAuth client ID. Omit to use dynamic client registration.
|
|
54
|
+
* When provided, takes precedence over any DCR-obtained client.
|
|
55
|
+
*/
|
|
57
56
|
clientId?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Pre-registered client secret (for confidential clients).
|
|
59
|
+
* Determines auth method for token requests: `client_secret_post` if set, `none` otherwise.
|
|
60
|
+
* This is fixed at construction - DCR-obtained secrets don't change the auth method.
|
|
61
|
+
*/
|
|
58
62
|
clientSecret?: string;
|
|
59
63
|
scope?: string;
|
|
60
64
|
/** Local callback server config. Must match redirect_uri in client registration. */
|
|
@@ -72,5 +76,11 @@ export interface BrowserAuthOptions {
|
|
|
72
76
|
errorHtml?: string;
|
|
73
77
|
/** Request inspection callback for debugging OAuth flows. */
|
|
74
78
|
onRequest?: (req: Request) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Authorization server base URL (issuer) for token endpoint discovery.
|
|
81
|
+
* Pass the origin (e.g., `https://auth.example.com`), not `/token`.
|
|
82
|
+
* Defaults to the authorization URL origin. Discovery failures are non-fatal.
|
|
83
|
+
*/
|
|
84
|
+
authServerUrl?: string | URL;
|
|
75
85
|
}
|
|
76
86
|
//# sourceMappingURL=mcp-types.d.ts.map
|
package/dist/mcp-types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-types.d.ts","sourceRoot":"","sources":["../src/mcp-types.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"mcp-types.d.ts","sourceRoot":"","sources":["../src/mcp-types.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,kDAAkD;AAClD,eAAO,MAAM,eAAe,EAAE,OAAO,MAA6B,CAAC;AAEnE;;;;GAIG;AACH,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,QAAQ,CAAC,CAAC,eAAe,CAAC,EAAE,IAAI,CAAC;IAEjC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrD,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;4FACwF;IACxF,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAElC,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAEnC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CAC9B"}
|
package/dist/mcp.d.ts
CHANGED
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
export { browserAuth } from "./auth/browser-auth";
|
|
8
8
|
export { inMemoryStore } from "./storage/memory";
|
|
9
9
|
export { fileStore } from "./storage/file";
|
|
10
|
-
export
|
|
10
|
+
export { OAuthStoreBrand } from "./mcp-types";
|
|
11
|
+
export type { BrowserAuthOptions, Tokens, TokenStore, ClientInfo, OAuthStore, } from "./mcp-types";
|
|
11
12
|
//# sourceMappingURL=mcp.d.ts.map
|
package/dist/mcp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,YAAY,EACV,kBAAkB,EAClB,MAAM,EACN,UAAU,EACV,UAAU,EACV,
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAElD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EACV,kBAAkB,EAClB,MAAM,EACN,UAAU,EACV,UAAU,EACV,UAAU,GACX,MAAM,aAAa,CAAC"}
|
package/dist/mcp.js
CHANGED
|
@@ -74,11 +74,11 @@ var init_is_inside_container = __esm(() => {
|
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
// node_modules/is-wsl/index.js
|
|
77
|
-
import
|
|
77
|
+
import process2 from "node:process";
|
|
78
78
|
import os2 from "node:os";
|
|
79
79
|
import fs4 from "node:fs";
|
|
80
80
|
var isWsl = () => {
|
|
81
|
-
if (
|
|
81
|
+
if (process2.platform !== "linux") {
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
84
|
if (os2.release().toLowerCase().includes("microsoft")) {
|
|
@@ -95,15 +95,15 @@ var isWsl = () => {
|
|
|
95
95
|
}, is_wsl_default;
|
|
96
96
|
var init_is_wsl = __esm(() => {
|
|
97
97
|
init_is_inside_container();
|
|
98
|
-
is_wsl_default =
|
|
98
|
+
is_wsl_default = process2.env.__IS_WSL_TEST__ ? isWsl : isWsl();
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
// node_modules/powershell-utils/index.js
|
|
102
|
-
import
|
|
102
|
+
import process3 from "node:process";
|
|
103
103
|
import { Buffer } from "node:buffer";
|
|
104
104
|
import { promisify } from "node:util";
|
|
105
105
|
import childProcess from "node:child_process";
|
|
106
|
-
var execFile, powerShellPath = () => `${
|
|
106
|
+
var execFile, powerShellPath = () => `${process3.env.SYSTEMROOT || process3.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, executePowerShell = async (command, options = {}) => {
|
|
107
107
|
const {
|
|
108
108
|
powerShellPath: psPath,
|
|
109
109
|
...execFileOptions
|
|
@@ -233,10 +233,10 @@ function defineLazyProperty(object, propertyName, valueGetter) {
|
|
|
233
233
|
|
|
234
234
|
// node_modules/default-browser-id/index.js
|
|
235
235
|
import { promisify as promisify3 } from "node:util";
|
|
236
|
-
import
|
|
236
|
+
import process4 from "node:process";
|
|
237
237
|
import { execFile as execFile3 } from "node:child_process";
|
|
238
238
|
async function defaultBrowserId() {
|
|
239
|
-
if (
|
|
239
|
+
if (process4.platform !== "darwin") {
|
|
240
240
|
throw new Error("macOS only");
|
|
241
241
|
}
|
|
242
242
|
const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
|
|
@@ -249,11 +249,11 @@ var init_default_browser_id = __esm(() => {
|
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
// node_modules/run-applescript/index.js
|
|
252
|
-
import
|
|
252
|
+
import process5 from "node:process";
|
|
253
253
|
import { promisify as promisify4 } from "node:util";
|
|
254
254
|
import { execFile as execFile4, execFileSync } from "node:child_process";
|
|
255
255
|
async function runAppleScript(script, { humanReadableOutput = true } = {}) {
|
|
256
|
-
if (
|
|
256
|
+
if (process5.platform !== "darwin") {
|
|
257
257
|
throw new Error("macOS only");
|
|
258
258
|
}
|
|
259
259
|
const outputArguments = humanReadableOutput ? [] : ["-ss"];
|
|
@@ -323,21 +323,21 @@ var init_windows = __esm(() => {
|
|
|
323
323
|
|
|
324
324
|
// node_modules/default-browser/index.js
|
|
325
325
|
import { promisify as promisify6 } from "node:util";
|
|
326
|
-
import
|
|
326
|
+
import process6 from "node:process";
|
|
327
327
|
import { execFile as execFile6 } from "node:child_process";
|
|
328
328
|
async function defaultBrowser2() {
|
|
329
|
-
if (
|
|
329
|
+
if (process6.platform === "darwin") {
|
|
330
330
|
const id = await defaultBrowserId();
|
|
331
331
|
const name = await bundleName(id);
|
|
332
332
|
return { name, id };
|
|
333
333
|
}
|
|
334
|
-
if (
|
|
334
|
+
if (process6.platform === "linux") {
|
|
335
335
|
const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
336
336
|
const id = stdout.trim();
|
|
337
337
|
const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
|
|
338
338
|
return { name, id };
|
|
339
339
|
}
|
|
340
|
-
if (
|
|
340
|
+
if (process6.platform === "win32") {
|
|
341
341
|
return defaultBrowser();
|
|
342
342
|
}
|
|
343
343
|
throw new Error("Only macOS, Linux, and Windows are supported");
|
|
@@ -352,10 +352,10 @@ var init_default_browser = __esm(() => {
|
|
|
352
352
|
});
|
|
353
353
|
|
|
354
354
|
// node_modules/is-in-ssh/index.js
|
|
355
|
-
import
|
|
355
|
+
import process7 from "node:process";
|
|
356
356
|
var isInSsh, is_in_ssh_default;
|
|
357
357
|
var init_is_in_ssh = __esm(() => {
|
|
358
|
-
isInSsh = Boolean(
|
|
358
|
+
isInSsh = Boolean(process7.env.SSH_CONNECTION || process7.env.SSH_CLIENT || process7.env.SSH_TTY);
|
|
359
359
|
is_in_ssh_default = isInSsh;
|
|
360
360
|
});
|
|
361
361
|
|
|
@@ -366,7 +366,7 @@ __export(exports_open, {
|
|
|
366
366
|
default: () => open_default,
|
|
367
367
|
apps: () => apps
|
|
368
368
|
});
|
|
369
|
-
import
|
|
369
|
+
import process8 from "node:process";
|
|
370
370
|
import path2 from "node:path";
|
|
371
371
|
import { fileURLToPath } from "node:url";
|
|
372
372
|
import childProcess3 from "node:child_process";
|
|
@@ -537,7 +537,7 @@ var fallbackAttemptSymbol, __dirname2, localXdgOpenPath, platform, arch, tryEach
|
|
|
537
537
|
await fs6.access(localXdgOpenPath, fsConstants2.X_OK);
|
|
538
538
|
exeLocalXdgOpen = true;
|
|
539
539
|
} catch {}
|
|
540
|
-
const useSystemXdgOpen =
|
|
540
|
+
const useSystemXdgOpen = process8.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
|
|
541
541
|
command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
|
|
542
542
|
}
|
|
543
543
|
if (appArguments.length > 0) {
|
|
@@ -624,7 +624,7 @@ var init_open = __esm(() => {
|
|
|
624
624
|
fallbackAttemptSymbol = Symbol("fallbackAttempt");
|
|
625
625
|
__dirname2 = import.meta.url ? path2.dirname(fileURLToPath(import.meta.url)) : "";
|
|
626
626
|
localXdgOpenPath = path2.join(__dirname2, "xdg-open");
|
|
627
|
-
({ platform, arch } =
|
|
627
|
+
({ platform, arch } = process8);
|
|
628
628
|
apps = {
|
|
629
629
|
browser: "browser",
|
|
630
630
|
browserPrivate: "browserPrivate"
|
|
@@ -672,6 +672,9 @@ var init_open = __esm(() => {
|
|
|
672
672
|
// src/auth/browser-auth.ts
|
|
673
673
|
import { randomBytes } from "node:crypto";
|
|
674
674
|
|
|
675
|
+
// src/mcp-types.ts
|
|
676
|
+
var OAuthStoreBrand = Symbol("OAuthStore");
|
|
677
|
+
|
|
675
678
|
// src/utils/token.ts
|
|
676
679
|
function calculateExpiry(expiresIn) {
|
|
677
680
|
if (!expiresIn)
|
|
@@ -691,9 +694,6 @@ function inMemoryStore() {
|
|
|
691
694
|
},
|
|
692
695
|
async delete(key) {
|
|
693
696
|
store.delete(key);
|
|
694
|
-
},
|
|
695
|
-
async clear() {
|
|
696
|
-
store.clear();
|
|
697
697
|
}
|
|
698
698
|
};
|
|
699
699
|
}
|
|
@@ -982,7 +982,9 @@ function fileStore(filepath) {
|
|
|
982
982
|
}
|
|
983
983
|
async function writeStore(data) {
|
|
984
984
|
await ensureDir();
|
|
985
|
-
|
|
985
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
986
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
987
|
+
await fs.rename(tmp, file);
|
|
986
988
|
}
|
|
987
989
|
return {
|
|
988
990
|
async get(key) {
|
|
@@ -998,9 +1000,6 @@ function fileStore(filepath) {
|
|
|
998
1000
|
const store = await readStore();
|
|
999
1001
|
delete store[key];
|
|
1000
1002
|
await writeStore(store);
|
|
1001
|
-
},
|
|
1002
|
-
async clear() {
|
|
1003
|
-
await writeStore({});
|
|
1004
1003
|
}
|
|
1005
1004
|
};
|
|
1006
1005
|
}
|
|
@@ -1049,6 +1048,10 @@ async function getAuthCode(input) {
|
|
|
1049
1048
|
}
|
|
1050
1049
|
|
|
1051
1050
|
// src/auth/browser-auth.ts
|
|
1051
|
+
import {
|
|
1052
|
+
exchangeAuthorization,
|
|
1053
|
+
discoverAuthorizationServerMetadata
|
|
1054
|
+
} from "@modelcontextprotocol/sdk/client/auth.js";
|
|
1052
1055
|
function browserAuth(options = {}) {
|
|
1053
1056
|
return new BrowserOAuthProvider(options);
|
|
1054
1057
|
}
|
|
@@ -1060,6 +1063,7 @@ class BrowserOAuthProvider {
|
|
|
1060
1063
|
_hostname;
|
|
1061
1064
|
_callbackPath;
|
|
1062
1065
|
_authTimeout;
|
|
1066
|
+
_redirectUrl;
|
|
1063
1067
|
_launch;
|
|
1064
1068
|
_clientId;
|
|
1065
1069
|
_clientSecret;
|
|
@@ -1067,12 +1071,11 @@ class BrowserOAuthProvider {
|
|
|
1067
1071
|
_successHtml;
|
|
1068
1072
|
_errorHtml;
|
|
1069
1073
|
_onRequest;
|
|
1074
|
+
_authServerUrl;
|
|
1070
1075
|
_clientInfo;
|
|
1071
1076
|
_tokens;
|
|
1077
|
+
_expiresAt;
|
|
1072
1078
|
_codeVerifier;
|
|
1073
|
-
_pendingAuthCode;
|
|
1074
|
-
_pendingAuthState;
|
|
1075
|
-
_isExchangingCode = false;
|
|
1076
1079
|
_tokensLoaded = false;
|
|
1077
1080
|
_loadingTokens;
|
|
1078
1081
|
_authInProgress;
|
|
@@ -1083,6 +1086,7 @@ class BrowserOAuthProvider {
|
|
|
1083
1086
|
this._hostname = options.hostname ?? "localhost";
|
|
1084
1087
|
this._callbackPath = options.callbackPath ?? "/callback";
|
|
1085
1088
|
this._authTimeout = options.authTimeout ?? 300000;
|
|
1089
|
+
this._redirectUrl = `http://${this._hostname}:${this._port}${this._callbackPath}`;
|
|
1086
1090
|
this._launch = options.launch;
|
|
1087
1091
|
this._clientId = options.clientId;
|
|
1088
1092
|
this._clientSecret = options.clientSecret;
|
|
@@ -1090,6 +1094,7 @@ class BrowserOAuthProvider {
|
|
|
1090
1094
|
this._successHtml = options.successHtml;
|
|
1091
1095
|
this._errorHtml = options.errorHtml;
|
|
1092
1096
|
this._onRequest = options.onRequest;
|
|
1097
|
+
this._authServerUrl = options.authServerUrl ? new URL(options.authServerUrl) : undefined;
|
|
1093
1098
|
}
|
|
1094
1099
|
async _ensureTokensLoaded() {
|
|
1095
1100
|
if (this._tokensLoaded)
|
|
@@ -1103,28 +1108,30 @@ class BrowserOAuthProvider {
|
|
|
1103
1108
|
try {
|
|
1104
1109
|
const stored = await this._store.get(this._storeKey);
|
|
1105
1110
|
if (stored) {
|
|
1111
|
+
this._expiresAt = stored.expiresAt;
|
|
1106
1112
|
this._tokens = {
|
|
1107
1113
|
access_token: stored.accessToken,
|
|
1108
1114
|
token_type: "Bearer",
|
|
1109
1115
|
refresh_token: stored.refreshToken,
|
|
1110
|
-
expires_in: stored.expiresAt ? Math.floor((stored.expiresAt - Date.now()) / 1000) : undefined,
|
|
1111
1116
|
scope: stored.scope
|
|
1112
1117
|
};
|
|
1113
1118
|
}
|
|
1114
1119
|
if (this._isOAuthStore(this._store)) {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1120
|
+
if (!this._clientId) {
|
|
1121
|
+
const clientInfo = await this._store.getClient(this._storeKey);
|
|
1122
|
+
if (clientInfo?.clientId) {
|
|
1123
|
+
this._clientInfo = {
|
|
1124
|
+
client_id: clientInfo.clientId,
|
|
1125
|
+
client_secret: clientInfo.clientSecret,
|
|
1126
|
+
client_id_issued_at: clientInfo.clientIdIssuedAt,
|
|
1127
|
+
client_secret_expires_at: clientInfo.clientSecretExpiresAt,
|
|
1128
|
+
redirect_uris: [this.redirectUrl]
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1124
1131
|
}
|
|
1125
|
-
const
|
|
1126
|
-
if (
|
|
1127
|
-
this._codeVerifier =
|
|
1132
|
+
const verifier = await this._store.getCodeVerifier(this._storeKey);
|
|
1133
|
+
if (verifier) {
|
|
1134
|
+
this._codeVerifier = verifier;
|
|
1128
1135
|
}
|
|
1129
1136
|
}
|
|
1130
1137
|
this._tokensLoaded = true;
|
|
@@ -1133,17 +1140,17 @@ class BrowserOAuthProvider {
|
|
|
1133
1140
|
}
|
|
1134
1141
|
}
|
|
1135
1142
|
_isOAuthStore(store) {
|
|
1136
|
-
return
|
|
1143
|
+
return OAuthStoreBrand in store;
|
|
1137
1144
|
}
|
|
1138
1145
|
get redirectUrl() {
|
|
1139
|
-
return
|
|
1146
|
+
return this._redirectUrl;
|
|
1140
1147
|
}
|
|
1141
1148
|
get clientMetadata() {
|
|
1142
1149
|
return {
|
|
1143
1150
|
client_name: "OAuth Callback Handler",
|
|
1144
1151
|
client_uri: "https://github.com/kriasoft/oauth-callback",
|
|
1145
1152
|
redirect_uris: [this.redirectUrl],
|
|
1146
|
-
grant_types: ["authorization_code"
|
|
1153
|
+
grant_types: ["authorization_code"],
|
|
1147
1154
|
response_types: ["code"],
|
|
1148
1155
|
scope: this._scope,
|
|
1149
1156
|
token_endpoint_auth_method: this._clientSecret ? "client_secret_post" : "none"
|
|
@@ -1185,19 +1192,19 @@ class BrowserOAuthProvider {
|
|
|
1185
1192
|
if (!this._tokens) {
|
|
1186
1193
|
return;
|
|
1187
1194
|
}
|
|
1188
|
-
|
|
1189
|
-
if (stored?.expiresAt && Date.now() >= stored.expiresAt - 60000) {
|
|
1195
|
+
if (this._expiresAt && Date.now() >= this._expiresAt - 60000) {
|
|
1190
1196
|
return;
|
|
1191
1197
|
}
|
|
1192
1198
|
return this._tokens;
|
|
1193
1199
|
}
|
|
1194
1200
|
async saveTokens(tokens) {
|
|
1195
1201
|
this._tokens = tokens;
|
|
1202
|
+
this._expiresAt = tokens.expires_in ? calculateExpiry(tokens.expires_in) : undefined;
|
|
1196
1203
|
this._tokensLoaded = true;
|
|
1197
1204
|
const storedTokens = {
|
|
1198
1205
|
accessToken: tokens.access_token,
|
|
1199
1206
|
refreshToken: tokens.refresh_token,
|
|
1200
|
-
expiresAt:
|
|
1207
|
+
expiresAt: this._expiresAt,
|
|
1201
1208
|
scope: tokens.scope
|
|
1202
1209
|
};
|
|
1203
1210
|
await this._store.set(this._storeKey, storedTokens);
|
|
@@ -1207,14 +1214,14 @@ class BrowserOAuthProvider {
|
|
|
1207
1214
|
await this._authInProgress;
|
|
1208
1215
|
return;
|
|
1209
1216
|
}
|
|
1210
|
-
this._authInProgress = this.
|
|
1217
|
+
this._authInProgress = this._completeAuthorizationFlow(authorizationUrl);
|
|
1211
1218
|
try {
|
|
1212
1219
|
await this._authInProgress;
|
|
1213
1220
|
} finally {
|
|
1214
1221
|
this._authInProgress = undefined;
|
|
1215
1222
|
}
|
|
1216
1223
|
}
|
|
1217
|
-
async
|
|
1224
|
+
async _completeAuthorizationFlow(authorizationUrl) {
|
|
1218
1225
|
const baseOptions = {
|
|
1219
1226
|
port: this._port,
|
|
1220
1227
|
hostname: this._hostname,
|
|
@@ -1229,23 +1236,49 @@ class BrowserOAuthProvider {
|
|
|
1229
1236
|
authorizationUrl: authorizationUrl.href,
|
|
1230
1237
|
launch: this._launch
|
|
1231
1238
|
} : baseOptions);
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1239
|
+
if (!result.code) {
|
|
1240
|
+
throw new Error("No authorization code received");
|
|
1241
|
+
}
|
|
1242
|
+
const expectedState = authorizationUrl.searchParams.get("state");
|
|
1243
|
+
if (expectedState && result.state !== expectedState) {
|
|
1244
|
+
throw new Error("OAuth state mismatch - possible CSRF attack");
|
|
1245
|
+
}
|
|
1246
|
+
await this._exchangeCodeForTokens(authorizationUrl, result.code);
|
|
1247
|
+
}
|
|
1248
|
+
async _exchangeCodeForTokens(authorizationUrl, code) {
|
|
1249
|
+
const authServerUrl = this._authServerUrl ?? new URL("/", authorizationUrl.origin);
|
|
1250
|
+
const metadata = await discoverAuthorizationServerMetadata(authServerUrl).catch(() => {
|
|
1251
|
+
return;
|
|
1252
|
+
});
|
|
1253
|
+
const clientInfo = await this.clientInformation();
|
|
1254
|
+
if (!clientInfo) {
|
|
1255
|
+
throw new Error("Client information required for token exchange. " + "Provide clientId in options or ensure DCR succeeded.");
|
|
1256
|
+
}
|
|
1257
|
+
if (!this._codeVerifier) {
|
|
1258
|
+
throw new Error("Code verifier required for token exchange");
|
|
1259
|
+
}
|
|
1260
|
+
let tokens;
|
|
1261
|
+
try {
|
|
1262
|
+
tokens = await exchangeAuthorization(authServerUrl, {
|
|
1263
|
+
metadata,
|
|
1264
|
+
clientInformation: clientInfo,
|
|
1265
|
+
authorizationCode: code,
|
|
1266
|
+
codeVerifier: this._codeVerifier,
|
|
1267
|
+
redirectUri: this.redirectUrl
|
|
1268
|
+
});
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
if (!this._authServerUrl && !metadata) {
|
|
1271
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1272
|
+
throw new Error(`Token exchange failed: ${msg}. ` + `If the token endpoint differs from ${authorizationUrl.origin}, set authServerUrl explicitly.`);
|
|
1238
1273
|
}
|
|
1239
|
-
|
|
1274
|
+
throw error;
|
|
1275
|
+
}
|
|
1276
|
+
await this.saveTokens(tokens);
|
|
1240
1277
|
}
|
|
1241
1278
|
async saveCodeVerifier(codeVerifier) {
|
|
1242
1279
|
this._codeVerifier = codeVerifier;
|
|
1243
1280
|
if (this._isOAuthStore(this._store)) {
|
|
1244
|
-
|
|
1245
|
-
codeVerifier,
|
|
1246
|
-
state: this._pendingAuthState
|
|
1247
|
-
};
|
|
1248
|
-
await this._store.setSession(this._storeKey, session);
|
|
1281
|
+
await this._store.setCodeVerifier(this._storeKey, codeVerifier);
|
|
1249
1282
|
}
|
|
1250
1283
|
}
|
|
1251
1284
|
async codeVerifier() {
|
|
@@ -1255,36 +1288,34 @@ class BrowserOAuthProvider {
|
|
|
1255
1288
|
return this._codeVerifier;
|
|
1256
1289
|
}
|
|
1257
1290
|
async invalidateCredentials(scope) {
|
|
1258
|
-
if (scope === "all" && this._isExchangingCode) {
|
|
1259
|
-
this._tokens = undefined;
|
|
1260
|
-
await this._store.delete(this._storeKey);
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
if (this._isExchangingCode && (scope === "client" || scope === "all")) {
|
|
1264
|
-
this._isExchangingCode = false;
|
|
1265
|
-
}
|
|
1266
1291
|
switch (scope) {
|
|
1267
1292
|
case "all":
|
|
1268
1293
|
this._clientInfo = undefined;
|
|
1269
1294
|
this._tokens = undefined;
|
|
1295
|
+
this._expiresAt = undefined;
|
|
1270
1296
|
this._codeVerifier = undefined;
|
|
1271
1297
|
this._tokensLoaded = false;
|
|
1272
|
-
await this._store.
|
|
1298
|
+
await this._store.delete(this._storeKey);
|
|
1299
|
+
if (this._isOAuthStore(this._store)) {
|
|
1300
|
+
await this._store.deleteClient(this._storeKey);
|
|
1301
|
+
await this._store.deleteCodeVerifier(this._storeKey);
|
|
1302
|
+
}
|
|
1273
1303
|
break;
|
|
1274
1304
|
case "client":
|
|
1275
1305
|
this._clientInfo = undefined;
|
|
1276
1306
|
if (this._isOAuthStore(this._store)) {
|
|
1277
|
-
await this._store.
|
|
1307
|
+
await this._store.deleteClient(this._storeKey);
|
|
1278
1308
|
}
|
|
1279
1309
|
break;
|
|
1280
1310
|
case "tokens":
|
|
1281
1311
|
this._tokens = undefined;
|
|
1312
|
+
this._expiresAt = undefined;
|
|
1282
1313
|
await this._store.delete(this._storeKey);
|
|
1283
1314
|
break;
|
|
1284
1315
|
case "verifier":
|
|
1285
1316
|
this._codeVerifier = undefined;
|
|
1286
1317
|
if (this._isOAuthStore(this._store)) {
|
|
1287
|
-
await this._store.
|
|
1318
|
+
await this._store.deleteCodeVerifier(this._storeKey);
|
|
1288
1319
|
}
|
|
1289
1320
|
break;
|
|
1290
1321
|
}
|
|
@@ -1292,22 +1323,10 @@ class BrowserOAuthProvider {
|
|
|
1292
1323
|
async validateResourceURL(_serverUrl, _resource) {
|
|
1293
1324
|
return;
|
|
1294
1325
|
}
|
|
1295
|
-
getPendingAuthCode() {
|
|
1296
|
-
if (this._pendingAuthCode) {
|
|
1297
|
-
const result = {
|
|
1298
|
-
code: this._pendingAuthCode,
|
|
1299
|
-
state: this._pendingAuthState
|
|
1300
|
-
};
|
|
1301
|
-
this._isExchangingCode = true;
|
|
1302
|
-
this._pendingAuthCode = undefined;
|
|
1303
|
-
this._pendingAuthState = undefined;
|
|
1304
|
-
return result;
|
|
1305
|
-
}
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
1326
|
}
|
|
1309
1327
|
export {
|
|
1310
1328
|
inMemoryStore,
|
|
1311
1329
|
fileStore,
|
|
1312
|
-
browserAuth
|
|
1330
|
+
browserAuth,
|
|
1331
|
+
OAuthStoreBrand
|
|
1313
1332
|
};
|
package/dist/storage/file.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { TokenStore } from "../mcp-types";
|
|
2
2
|
/**
|
|
3
3
|
* Persistent file-based token storage.
|
|
4
|
+
* Not safe for concurrent access across multiple processes.
|
|
4
5
|
* Default: ~/.mcp/tokens.json
|
|
5
|
-
* WARNING: Not safe for concurrent access across processes.
|
|
6
6
|
*/
|
|
7
7
|
export declare function fileStore(filepath?: string): TokenStore;
|
|
8
8
|
//# sourceMappingURL=file.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/storage/file.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,cAAc,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/storage/file.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,cAAc,CAAC;AAEvD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAyCvD"}
|
package/dist/storage/memory.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TokenStore } from "../mcp-types";
|
|
2
2
|
/**
|
|
3
3
|
* Ephemeral in-memory token storage.
|
|
4
|
-
* Tokens lost on process restart.
|
|
4
|
+
* Tokens lost on process restart.
|
|
5
5
|
*/
|
|
6
6
|
export declare function inMemoryStore(): TokenStore;
|
|
7
7
|
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/storage/memory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,cAAc,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/storage/memory.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAU,MAAM,cAAc,CAAC;AAEvD;;;GAGG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAgB1C"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oauth-callback",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Lightweight OAuth 2.0 callback handler for Node.js, Deno, and Bun with built-in browser flow and MCP SDK integration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"oauth",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
},
|
|
105
105
|
"scripts": {
|
|
106
106
|
"build:templates": "bun run templates/build.ts",
|
|
107
|
-
"build": "bun run build:templates && bun build ./src/index.ts --outdir=./dist --target=node && bun build ./src/mcp.ts --outdir=./dist --target=node && tsc --declaration --emitDeclarationOnly --outDir ./dist",
|
|
107
|
+
"build": "bun run build:templates && bun build ./src/index.ts --outdir=./dist --target=node && bun build ./src/mcp.ts --outdir=./dist --target=node --external=@modelcontextprotocol/sdk && tsc --declaration --emitDeclarationOnly --outDir ./dist",
|
|
108
108
|
"clean": "rm -rf dist",
|
|
109
109
|
"test": "bun test",
|
|
110
110
|
"test:login": "bun run scripts/test-login.ts",
|