@automattic/mcp-wordpress-remote 0.5.2 → 0.5.4
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/Docs/authentication.md +45 -0
- package/README.md +5 -5
- package/dist/lib.d.ts +18 -12
- package/dist/lib.js +567 -412
- package/dist/proxy.js +496 -466
- package/package.json +7 -3
package/dist/lib.js
CHANGED
|
@@ -6498,7 +6498,7 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) {
|
|
|
6498
6498
|
const LAST_CHUNK = Buffer3.from("0\r\n\r\n");
|
|
6499
6499
|
let isChunkedTransfer = false;
|
|
6500
6500
|
let properLastChunkReceived = false;
|
|
6501
|
-
let
|
|
6501
|
+
let trailingBytes = Buffer3.alloc(0);
|
|
6502
6502
|
request.on("response", (response) => {
|
|
6503
6503
|
const { headers } = response;
|
|
6504
6504
|
isChunkedTransfer = headers["transfer-encoding"] === "chunked" && !headers["content-length"];
|
|
@@ -6512,11 +6512,15 @@ function fixResponseChunkedTransferBadEnding(request, errorCallback) {
|
|
|
6512
6512
|
}
|
|
6513
6513
|
};
|
|
6514
6514
|
const onData = (buf) => {
|
|
6515
|
-
|
|
6516
|
-
if (
|
|
6517
|
-
|
|
6515
|
+
if (buf.length === 0) return;
|
|
6516
|
+
if (buf.length >= LAST_CHUNK.length) {
|
|
6517
|
+
trailingBytes = Buffer3.from(buf.subarray(-LAST_CHUNK.length));
|
|
6518
|
+
} else {
|
|
6519
|
+
trailingBytes = Buffer3.from(
|
|
6520
|
+
Buffer3.concat([trailingBytes, buf]).subarray(-LAST_CHUNK.length)
|
|
6521
|
+
);
|
|
6518
6522
|
}
|
|
6519
|
-
|
|
6523
|
+
properLastChunkReceived = trailingBytes.equals(LAST_CHUNK);
|
|
6520
6524
|
};
|
|
6521
6525
|
socket.prependListener("close", onSocketClose);
|
|
6522
6526
|
socket.on("data", onData);
|
|
@@ -75554,8 +75558,152 @@ var OAUTH_ERRORS = {
|
|
|
75554
75558
|
};
|
|
75555
75559
|
|
|
75556
75560
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js
|
|
75561
|
+
function isClientAuthMethod(method) {
|
|
75562
|
+
return ["client_secret_basic", "client_secret_post", "none"].includes(method);
|
|
75563
|
+
}
|
|
75564
|
+
function selectClientAuthMethod(clientInformation, supportedMethods) {
|
|
75565
|
+
const hasClientSecret = clientInformation.client_secret !== void 0;
|
|
75566
|
+
if ("token_endpoint_auth_method" in clientInformation && clientInformation.token_endpoint_auth_method && isClientAuthMethod(clientInformation.token_endpoint_auth_method) && (supportedMethods.length === 0 || supportedMethods.includes(clientInformation.token_endpoint_auth_method))) {
|
|
75567
|
+
return clientInformation.token_endpoint_auth_method;
|
|
75568
|
+
}
|
|
75569
|
+
if (supportedMethods.length === 0) {
|
|
75570
|
+
return hasClientSecret ? "client_secret_basic" : "none";
|
|
75571
|
+
}
|
|
75572
|
+
if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
|
|
75573
|
+
return "client_secret_basic";
|
|
75574
|
+
}
|
|
75575
|
+
if (hasClientSecret && supportedMethods.includes("client_secret_post")) {
|
|
75576
|
+
return "client_secret_post";
|
|
75577
|
+
}
|
|
75578
|
+
if (supportedMethods.includes("none")) {
|
|
75579
|
+
return "none";
|
|
75580
|
+
}
|
|
75581
|
+
return hasClientSecret ? "client_secret_post" : "none";
|
|
75582
|
+
}
|
|
75583
|
+
function applyClientAuthentication(method, clientInformation, headers, params) {
|
|
75584
|
+
const { client_id, client_secret } = clientInformation;
|
|
75585
|
+
switch (method) {
|
|
75586
|
+
case "client_secret_basic":
|
|
75587
|
+
applyBasicAuth(client_id, client_secret, headers);
|
|
75588
|
+
return;
|
|
75589
|
+
case "client_secret_post":
|
|
75590
|
+
applyPostAuth(client_id, client_secret, params);
|
|
75591
|
+
return;
|
|
75592
|
+
case "none":
|
|
75593
|
+
applyPublicAuth(client_id, params);
|
|
75594
|
+
return;
|
|
75595
|
+
default:
|
|
75596
|
+
throw new Error(`Unsupported client authentication method: ${method}`);
|
|
75597
|
+
}
|
|
75598
|
+
}
|
|
75599
|
+
function applyBasicAuth(clientId, clientSecret, headers) {
|
|
75600
|
+
if (!clientSecret) {
|
|
75601
|
+
throw new Error("client_secret_basic authentication requires a client_secret");
|
|
75602
|
+
}
|
|
75603
|
+
const credentials = btoa(`${clientId}:${clientSecret}`);
|
|
75604
|
+
headers.set("Authorization", `Basic ${credentials}`);
|
|
75605
|
+
}
|
|
75606
|
+
function applyPostAuth(clientId, clientSecret, params) {
|
|
75607
|
+
params.set("client_id", clientId);
|
|
75608
|
+
if (clientSecret) {
|
|
75609
|
+
params.set("client_secret", clientSecret);
|
|
75610
|
+
}
|
|
75611
|
+
}
|
|
75612
|
+
function applyPublicAuth(clientId, params) {
|
|
75613
|
+
params.set("client_id", clientId);
|
|
75614
|
+
}
|
|
75615
|
+
async function parseErrorResponse(input) {
|
|
75616
|
+
const statusCode = input instanceof Response ? input.status : void 0;
|
|
75617
|
+
const body = input instanceof Response ? await input.text() : input;
|
|
75618
|
+
try {
|
|
75619
|
+
const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
|
|
75620
|
+
const { error: error51, error_description, error_uri } = result;
|
|
75621
|
+
const errorClass = OAUTH_ERRORS[error51] || ServerError;
|
|
75622
|
+
return new errorClass(error_description || "", error_uri);
|
|
75623
|
+
} catch (error51) {
|
|
75624
|
+
const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ""}Invalid OAuth error response: ${error51}. Raw body: ${body}`;
|
|
75625
|
+
return new ServerError(errorMessage);
|
|
75626
|
+
}
|
|
75627
|
+
}
|
|
75557
75628
|
var TokenResponseSchema = OAuthTokensSchema.omit({ issuer: true });
|
|
75558
75629
|
var RegistrationResponseSchema = OAuthClientInformationFullSchema.omit({ issuer: true });
|
|
75630
|
+
function buildDiscoveryUrls(authorizationServerUrl) {
|
|
75631
|
+
const url2 = typeof authorizationServerUrl === "string" ? new URL(authorizationServerUrl) : authorizationServerUrl;
|
|
75632
|
+
const hasPath = url2.pathname !== "/";
|
|
75633
|
+
const urlsToTry = [];
|
|
75634
|
+
if (!hasPath) {
|
|
75635
|
+
urlsToTry.push({
|
|
75636
|
+
url: new URL("/.well-known/oauth-authorization-server", url2.origin),
|
|
75637
|
+
type: "oauth"
|
|
75638
|
+
});
|
|
75639
|
+
urlsToTry.push({
|
|
75640
|
+
url: new URL(`/.well-known/openid-configuration`, url2.origin),
|
|
75641
|
+
type: "oidc"
|
|
75642
|
+
});
|
|
75643
|
+
return urlsToTry;
|
|
75644
|
+
}
|
|
75645
|
+
let pathname = url2.pathname;
|
|
75646
|
+
if (pathname.endsWith("/")) {
|
|
75647
|
+
pathname = pathname.slice(0, -1);
|
|
75648
|
+
}
|
|
75649
|
+
urlsToTry.push({
|
|
75650
|
+
url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url2.origin),
|
|
75651
|
+
type: "oauth"
|
|
75652
|
+
});
|
|
75653
|
+
urlsToTry.push({
|
|
75654
|
+
url: new URL(`/.well-known/openid-configuration${pathname}`, url2.origin),
|
|
75655
|
+
type: "oidc"
|
|
75656
|
+
});
|
|
75657
|
+
urlsToTry.push({
|
|
75658
|
+
url: new URL(`${pathname}/.well-known/openid-configuration`, url2.origin),
|
|
75659
|
+
type: "oidc"
|
|
75660
|
+
});
|
|
75661
|
+
return urlsToTry;
|
|
75662
|
+
}
|
|
75663
|
+
function resourceIndicatorToString(resource) {
|
|
75664
|
+
return typeof resource === "string" ? resource : resource.href;
|
|
75665
|
+
}
|
|
75666
|
+
async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication, resource, fetchFn }) {
|
|
75667
|
+
const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
|
|
75668
|
+
const headers = new Headers({
|
|
75669
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
75670
|
+
Accept: "application/json"
|
|
75671
|
+
});
|
|
75672
|
+
if (resource) {
|
|
75673
|
+
tokenRequestParams.set("resource", resourceIndicatorToString(resource));
|
|
75674
|
+
}
|
|
75675
|
+
if (addClientAuthentication) {
|
|
75676
|
+
await addClientAuthentication(headers, tokenRequestParams, tokenUrl, metadata);
|
|
75677
|
+
} else if (clientInformation) {
|
|
75678
|
+
const supportedMethods = metadata?.token_endpoint_auth_methods_supported ?? [];
|
|
75679
|
+
const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
|
|
75680
|
+
applyClientAuthentication(authMethod, clientInformation, headers, tokenRequestParams);
|
|
75681
|
+
}
|
|
75682
|
+
const response = await (fetchFn ?? fetch)(tokenUrl, {
|
|
75683
|
+
method: "POST",
|
|
75684
|
+
headers,
|
|
75685
|
+
body: tokenRequestParams
|
|
75686
|
+
});
|
|
75687
|
+
if (!response.ok) {
|
|
75688
|
+
throw await parseErrorResponse(response);
|
|
75689
|
+
}
|
|
75690
|
+
return TokenResponseSchema.parse(await response.json());
|
|
75691
|
+
}
|
|
75692
|
+
async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
|
|
75693
|
+
const tokenRequestParams = new URLSearchParams({
|
|
75694
|
+
grant_type: "refresh_token",
|
|
75695
|
+
refresh_token: refreshToken
|
|
75696
|
+
});
|
|
75697
|
+
const tokens = await executeTokenRequest(authorizationServerUrl, {
|
|
75698
|
+
metadata,
|
|
75699
|
+
tokenRequestParams,
|
|
75700
|
+
clientInformation,
|
|
75701
|
+
addClientAuthentication,
|
|
75702
|
+
resource,
|
|
75703
|
+
fetchFn
|
|
75704
|
+
});
|
|
75705
|
+
return { refresh_token: refreshToken, ...tokens };
|
|
75706
|
+
}
|
|
75559
75707
|
|
|
75560
75708
|
// src/lib/utils.ts
|
|
75561
75709
|
import * as fs2 from "node:fs";
|
|
@@ -75668,7 +75816,19 @@ async function writeTokens(serverUrlHash, tokens) {
|
|
|
75668
75816
|
...tokens,
|
|
75669
75817
|
obtained_at: Date.now()
|
|
75670
75818
|
};
|
|
75671
|
-
await
|
|
75819
|
+
await ensureConfigDir();
|
|
75820
|
+
const file2 = getConfigFilePath(serverUrlHash, "tokens.json");
|
|
75821
|
+
const temporary = `${file2}.${crypto2.randomUUID()}.tmp`;
|
|
75822
|
+
try {
|
|
75823
|
+
await fs.writeFile(temporary, JSON.stringify(tokensWithTimestamp, null, 2), {
|
|
75824
|
+
encoding: "utf-8",
|
|
75825
|
+
mode: 384,
|
|
75826
|
+
flag: "wx"
|
|
75827
|
+
});
|
|
75828
|
+
await fs.rename(temporary, file2);
|
|
75829
|
+
} finally {
|
|
75830
|
+
await fs.rm(temporary, { force: true });
|
|
75831
|
+
}
|
|
75672
75832
|
logger.info(`Stored tokens for server hash: ${serverUrlHash}`, "AUTH");
|
|
75673
75833
|
} catch (error51) {
|
|
75674
75834
|
logger.error(`Error writing tokens for ${serverUrlHash}`, "AUTH", error51);
|
|
@@ -75701,7 +75861,7 @@ function isTokenValid(tokens) {
|
|
|
75701
75861
|
if (!tokens?.access_token) {
|
|
75702
75862
|
return { isValid: false, error: "No access token" };
|
|
75703
75863
|
}
|
|
75704
|
-
if (
|
|
75864
|
+
if (tokens.expires_in === void 0 || tokens.obtained_at === void 0) {
|
|
75705
75865
|
return { isValid: true };
|
|
75706
75866
|
}
|
|
75707
75867
|
const now = Date.now();
|
|
@@ -75740,12 +75900,12 @@ var OAuthClientInfoSchema = external_exports.object({
|
|
|
75740
75900
|
client_id: external_exports.string().optional(),
|
|
75741
75901
|
client_secret: external_exports.string().optional()
|
|
75742
75902
|
});
|
|
75743
|
-
async function findAvailablePort(preferredPort) {
|
|
75903
|
+
async function findAvailablePort(preferredPort, host = "127.0.0.1") {
|
|
75744
75904
|
return new Promise((resolve, reject) => {
|
|
75745
75905
|
const server = net.createServer();
|
|
75746
75906
|
server.on("error", (err) => {
|
|
75747
75907
|
if (err.code === "EADDRINUSE") {
|
|
75748
|
-
server.listen(0);
|
|
75908
|
+
server.listen(0, host);
|
|
75749
75909
|
} else {
|
|
75750
75910
|
reject(err);
|
|
75751
75911
|
}
|
|
@@ -75756,7 +75916,7 @@ async function findAvailablePort(preferredPort) {
|
|
|
75756
75916
|
resolve(port);
|
|
75757
75917
|
});
|
|
75758
75918
|
});
|
|
75759
|
-
server.listen(preferredPort || 0);
|
|
75919
|
+
server.listen(preferredPort || 0, host);
|
|
75760
75920
|
});
|
|
75761
75921
|
}
|
|
75762
75922
|
function calculateDefaultPort(serverUrlHash) {
|
|
@@ -75784,24 +75944,17 @@ async function findExistingClientPort(serverUrlHash) {
|
|
|
75784
75944
|
function getServerUrlHash(serverUrl) {
|
|
75785
75945
|
return crypto3.createHash("md5").update(serverUrl).digest("hex");
|
|
75786
75946
|
}
|
|
75787
|
-
async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
|
|
75947
|
+
async function selectCallbackPort(serverUrl, specifiedPort, _unused, host = "127.0.0.1") {
|
|
75788
75948
|
const serverUrlHash = getServerUrlHash(serverUrl);
|
|
75789
75949
|
if (specifiedPort) {
|
|
75790
75950
|
return specifiedPort;
|
|
75791
75951
|
}
|
|
75792
|
-
const
|
|
75793
|
-
|
|
75794
|
-
findExistingClientPort(serverUrlHash),
|
|
75795
|
-
findAvailablePort(defaultPort)
|
|
75796
|
-
]);
|
|
75797
|
-
if (existingClientPort) {
|
|
75798
|
-
return existingClientPort;
|
|
75799
|
-
}
|
|
75800
|
-
return availablePort;
|
|
75952
|
+
const existingClientPort = await findExistingClientPort(serverUrlHash);
|
|
75953
|
+
return findAvailablePort(existingClientPort ?? calculateDefaultPort(serverUrlHash), host);
|
|
75801
75954
|
}
|
|
75802
75955
|
|
|
75803
75956
|
// src/lib/config.ts
|
|
75804
|
-
var MCP_WORDPRESS_REMOTE_VERSION = "0.5.
|
|
75957
|
+
var MCP_WORDPRESS_REMOTE_VERSION = "0.5.4";
|
|
75805
75958
|
function parsePositiveIntEnv(value, defaultValue) {
|
|
75806
75959
|
if (!value) {
|
|
75807
75960
|
return defaultValue;
|
|
@@ -76002,8 +76155,8 @@ async function getOAuthCallbackPort() {
|
|
|
76002
76155
|
return selectCallbackPort(
|
|
76003
76156
|
CONFIG.WP_API_URL,
|
|
76004
76157
|
CONFIG.OAUTH_CALLBACK_PORT,
|
|
76005
|
-
false
|
|
76006
|
-
|
|
76158
|
+
false,
|
|
76159
|
+
CONFIG.OAUTH_HOST
|
|
76007
76160
|
);
|
|
76008
76161
|
}
|
|
76009
76162
|
function formatConfigError(error51) {
|
|
@@ -76923,10 +77076,48 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
|
76923
77076
|
var open_default = open;
|
|
76924
77077
|
|
|
76925
77078
|
// src/lib/mcp-oauth-provider.ts
|
|
77079
|
+
import lockfile from "proper-lockfile";
|
|
77080
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
76926
77081
|
import { EventEmitter } from "node:events";
|
|
76927
77082
|
|
|
76928
77083
|
// src/lib/mcp-oauth-utils.ts
|
|
76929
77084
|
import crypto4 from "crypto";
|
|
77085
|
+
|
|
77086
|
+
// src/lib/oauth-http.ts
|
|
77087
|
+
function validateOAuthUrl(value) {
|
|
77088
|
+
let url2;
|
|
77089
|
+
try {
|
|
77090
|
+
url2 = new URL(value);
|
|
77091
|
+
} catch {
|
|
77092
|
+
throw new OAuthError2("Invalid OAuth URL", "INVALID_OAUTH_URL");
|
|
77093
|
+
}
|
|
77094
|
+
const loopback = url2.hostname === "localhost" || url2.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url2.hostname);
|
|
77095
|
+
if (url2.protocol !== "https:" && !(url2.protocol === "http:" && loopback) || url2.username || url2.password || url2.hash) {
|
|
77096
|
+
throw new OAuthError2(
|
|
77097
|
+
"OAuth URLs require HTTPS (HTTP is allowed only on loopback)",
|
|
77098
|
+
"INSECURE_OAUTH_URL"
|
|
77099
|
+
);
|
|
77100
|
+
}
|
|
77101
|
+
return url2;
|
|
77102
|
+
}
|
|
77103
|
+
async function oauthFetch(value, init = {}) {
|
|
77104
|
+
const url2 = validateOAuthUrl(value);
|
|
77105
|
+
const headers = new Headers(
|
|
77106
|
+
url2.origin === new URL(CONFIG.WP_API_URL).origin ? getCustomHeaders() : {}
|
|
77107
|
+
);
|
|
77108
|
+
new Headers(init.headers).forEach((value2, key) => headers.set(key, value2));
|
|
77109
|
+
const response = await proxyFetch(url2.toString(), {
|
|
77110
|
+
...init,
|
|
77111
|
+
headers,
|
|
77112
|
+
redirect: "error",
|
|
77113
|
+
signal: init.signal ?? AbortSignal.timeout(CONFIG.OAUTH_TIMEOUT)
|
|
77114
|
+
});
|
|
77115
|
+
if (response.status >= 300 && response.status < 400)
|
|
77116
|
+
throw new OAuthError2("OAuth endpoint redirects are not accepted", "OAUTH_REDIRECT");
|
|
77117
|
+
return response;
|
|
77118
|
+
}
|
|
77119
|
+
|
|
77120
|
+
// src/lib/mcp-oauth-utils.ts
|
|
76930
77121
|
function generatePKCE() {
|
|
76931
77122
|
const codeVerifier = crypto4.randomBytes(64).toString("base64url").substring(0, 128);
|
|
76932
77123
|
const codeChallenge = crypto4.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
@@ -76954,118 +77145,76 @@ function generateCanonicalResourceURI(serverUrl) {
|
|
|
76954
77145
|
}
|
|
76955
77146
|
}
|
|
76956
77147
|
async function discoverAuthorizationServerMetadata(authorizationServerUrl) {
|
|
76957
|
-
|
|
76958
|
-
|
|
76959
|
-
|
|
76960
|
-
const
|
|
76961
|
-
|
|
76962
|
-
|
|
76963
|
-
Accept: "application/json",
|
|
76964
|
-
...customHeaders
|
|
76965
|
-
}
|
|
76966
|
-
});
|
|
76967
|
-
if (!response.ok) {
|
|
77148
|
+
const issuer = validateOAuthUrl(authorizationServerUrl);
|
|
77149
|
+
if (issuer.search) throw new OAuthError2("Issuer must not contain a query", "INVALID_METADATA");
|
|
77150
|
+
for (const { url: url2 } of buildDiscoveryUrls(issuer)) {
|
|
77151
|
+
const response = await oauthFetch(url2.toString(), { headers: { Accept: "application/json" } });
|
|
77152
|
+
if ([404, 405].includes(response.status)) continue;
|
|
77153
|
+
if (!response.ok)
|
|
76968
77154
|
throw new OAuthError2(
|
|
76969
|
-
`
|
|
77155
|
+
`Metadata request failed (${response.status})`,
|
|
76970
77156
|
"METADATA_DISCOVERY_FAILED"
|
|
76971
77157
|
);
|
|
76972
|
-
|
|
76973
|
-
|
|
76974
|
-
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
77158
|
+
const parsed = OAuthMetadataSchema.safeParse(await response.json());
|
|
77159
|
+
if (!parsed.success || parsed.data.issuer !== authorizationServerUrl) {
|
|
76975
77160
|
throw new OAuthError2(
|
|
76976
|
-
"Authorization
|
|
77161
|
+
"Authorization metadata has an invalid or mismatched issuer",
|
|
76977
77162
|
"INVALID_METADATA"
|
|
76978
77163
|
);
|
|
76979
77164
|
}
|
|
76980
|
-
|
|
77165
|
+
const metadata = parsed.data;
|
|
77166
|
+
validateOAuthUrl(metadata.issuer);
|
|
77167
|
+
validateOAuthUrl(metadata.authorization_endpoint);
|
|
77168
|
+
validateOAuthUrl(metadata.token_endpoint);
|
|
77169
|
+
if (metadata.registration_endpoint) validateOAuthUrl(metadata.registration_endpoint);
|
|
76981
77170
|
return metadata;
|
|
76982
|
-
} catch (error51) {
|
|
76983
|
-
if (error51 instanceof OAuthError2) {
|
|
76984
|
-
throw error51;
|
|
76985
|
-
}
|
|
76986
|
-
throw new OAuthError2(
|
|
76987
|
-
`Error discovering authorization server metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
|
|
76988
|
-
"METADATA_DISCOVERY_ERROR"
|
|
76989
|
-
);
|
|
76990
77171
|
}
|
|
77172
|
+
throw new OAuthError2("No authorization server metadata found", "METADATA_DISCOVERY_FAILED");
|
|
76991
77173
|
}
|
|
76992
|
-
async function discoverProtectedResourceMetadata(resourceUrl) {
|
|
76993
|
-
|
|
76994
|
-
|
|
76995
|
-
|
|
76996
|
-
|
|
76997
|
-
|
|
76998
|
-
|
|
76999
|
-
|
|
77000
|
-
|
|
77001
|
-
|
|
77002
|
-
}
|
|
77003
|
-
|
|
77004
|
-
|
|
77174
|
+
async function discoverProtectedResourceMetadata(resourceUrl, resourceMetadataUrl) {
|
|
77175
|
+
const resource = validateOAuthUrl(resourceUrl);
|
|
77176
|
+
const candidates = resourceMetadataUrl ? [{ url: resourceMetadataUrl, expected: resourceUrl }] : [
|
|
77177
|
+
...resource.pathname !== "/" ? [
|
|
77178
|
+
{
|
|
77179
|
+
url: `${resource.origin}/.well-known/oauth-protected-resource${resource.pathname}`,
|
|
77180
|
+
expected: resourceUrl
|
|
77181
|
+
}
|
|
77182
|
+
] : [],
|
|
77183
|
+
{
|
|
77184
|
+
url: `${resource.origin}/.well-known/oauth-protected-resource`,
|
|
77185
|
+
expected: resource.pathname === "/" ? resourceUrl : resource.origin
|
|
77186
|
+
}
|
|
77187
|
+
];
|
|
77188
|
+
for (const candidate of candidates) {
|
|
77189
|
+
const response = await oauthFetch(candidate.url, { headers: { Accept: "application/json" } });
|
|
77190
|
+
if (!resourceMetadataUrl && [404, 405].includes(response.status)) continue;
|
|
77191
|
+
if (!response.ok)
|
|
77005
77192
|
throw new OAuthError2(
|
|
77006
|
-
`
|
|
77193
|
+
`Resource metadata request failed (${response.status})`,
|
|
77007
77194
|
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
77008
77195
|
);
|
|
77009
|
-
|
|
77010
|
-
|
|
77011
|
-
if (!metadata.authorization_servers || metadata.authorization_servers.length === 0) {
|
|
77196
|
+
const parsed = OAuthProtectedResourceMetadataSchema.safeParse(await response.json());
|
|
77197
|
+
if (!parsed.success || parsed.data.resource !== candidate.expected || !parsed.data.authorization_servers?.length) {
|
|
77012
77198
|
throw new OAuthError2(
|
|
77013
|
-
"Protected resource metadata
|
|
77199
|
+
"Protected resource metadata has an invalid or mismatched resource",
|
|
77014
77200
|
"INVALID_RESOURCE_METADATA"
|
|
77015
77201
|
);
|
|
77016
77202
|
}
|
|
77017
|
-
|
|
77018
|
-
return
|
|
77019
|
-
} catch (error51) {
|
|
77020
|
-
if (error51 instanceof OAuthError2) {
|
|
77021
|
-
throw error51;
|
|
77022
|
-
}
|
|
77023
|
-
throw new OAuthError2(
|
|
77024
|
-
`Error discovering protected resource metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
|
|
77025
|
-
"RESOURCE_METADATA_DISCOVERY_ERROR"
|
|
77026
|
-
);
|
|
77203
|
+
for (const issuer of parsed.data.authorization_servers) validateOAuthUrl(issuer);
|
|
77204
|
+
return parsed.data;
|
|
77027
77205
|
}
|
|
77028
|
-
|
|
77029
|
-
|
|
77030
|
-
|
|
77031
|
-
|
|
77032
|
-
const result = { scheme };
|
|
77033
|
-
const paramString = parts.slice(1).join(" ");
|
|
77034
|
-
const paramMatches = paramString.matchAll(/(\w+)="([^"]+)"/g);
|
|
77035
|
-
for (const match of paramMatches) {
|
|
77036
|
-
const [, key, value] = match;
|
|
77037
|
-
switch (key) {
|
|
77038
|
-
case "realm":
|
|
77039
|
-
result.realm = value;
|
|
77040
|
-
break;
|
|
77041
|
-
case "scope":
|
|
77042
|
-
result.scope = value;
|
|
77043
|
-
break;
|
|
77044
|
-
case "error":
|
|
77045
|
-
result.error = value;
|
|
77046
|
-
break;
|
|
77047
|
-
case "error_description":
|
|
77048
|
-
result.error_description = value;
|
|
77049
|
-
break;
|
|
77050
|
-
case "error_uri":
|
|
77051
|
-
result.error_uri = value;
|
|
77052
|
-
break;
|
|
77053
|
-
case "resource_metadata_url":
|
|
77054
|
-
result.resource_metadata_url = value;
|
|
77055
|
-
break;
|
|
77056
|
-
}
|
|
77057
|
-
}
|
|
77058
|
-
return result;
|
|
77206
|
+
throw new OAuthError2(
|
|
77207
|
+
"No protected resource metadata found",
|
|
77208
|
+
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
77209
|
+
);
|
|
77059
77210
|
}
|
|
77060
77211
|
async function registerDynamicClient(registrationEndpoint, registrationRequest) {
|
|
77061
77212
|
try {
|
|
77062
77213
|
logger.oauth(`Attempting dynamic client registration: ${registrationEndpoint}`);
|
|
77063
|
-
const
|
|
77064
|
-
const response = await proxyFetch(registrationEndpoint, {
|
|
77214
|
+
const response = await oauthFetch(registrationEndpoint, {
|
|
77065
77215
|
method: "POST",
|
|
77066
77216
|
headers: {
|
|
77067
|
-
"Content-Type": "application/json"
|
|
77068
|
-
...customHeaders
|
|
77217
|
+
"Content-Type": "application/json"
|
|
77069
77218
|
},
|
|
77070
77219
|
body: JSON.stringify(registrationRequest)
|
|
77071
77220
|
});
|
|
@@ -77077,12 +77226,26 @@ async function registerDynamicClient(registrationEndpoint, registrationRequest)
|
|
|
77077
77226
|
);
|
|
77078
77227
|
}
|
|
77079
77228
|
const registrationResponse = await response.json();
|
|
77080
|
-
if (!registrationResponse.client_id) {
|
|
77229
|
+
if (typeof registrationResponse.client_id !== "string" || !registrationResponse.client_id) {
|
|
77081
77230
|
throw new OAuthError2(
|
|
77082
77231
|
"Dynamic client registration response missing client_id",
|
|
77083
77232
|
"INVALID_REGISTRATION_RESPONSE"
|
|
77084
77233
|
);
|
|
77085
77234
|
}
|
|
77235
|
+
if (registrationResponse.token_endpoint_auth_method && registrationResponse.token_endpoint_auth_method !== registrationRequest.token_endpoint_auth_method) {
|
|
77236
|
+
throw new OAuthError2(
|
|
77237
|
+
"Authorization server changed the requested client authentication method",
|
|
77238
|
+
"INVALID_REGISTRATION_RESPONSE"
|
|
77239
|
+
);
|
|
77240
|
+
}
|
|
77241
|
+
if (registrationResponse.redirect_uris && (!Array.isArray(registrationResponse.redirect_uris) || !registrationRequest.redirect_uris.every(
|
|
77242
|
+
(uri) => registrationResponse.redirect_uris.includes(uri)
|
|
77243
|
+
))) {
|
|
77244
|
+
throw new OAuthError2(
|
|
77245
|
+
"Authorization server changed the registered callback URI",
|
|
77246
|
+
"INVALID_REGISTRATION_RESPONSE"
|
|
77247
|
+
);
|
|
77248
|
+
}
|
|
77086
77249
|
logger.oauth("Dynamic client registration successful");
|
|
77087
77250
|
logger.debug("Client ID obtained", "OAUTH", {
|
|
77088
77251
|
client_id: registrationResponse.client_id,
|
|
@@ -77112,12 +77275,10 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
|
|
|
77112
77275
|
if (resource) {
|
|
77113
77276
|
params.set("resource", resource);
|
|
77114
77277
|
}
|
|
77115
|
-
const
|
|
77116
|
-
const response = await proxyFetch(tokenEndpoint, {
|
|
77278
|
+
const response = await oauthFetch(tokenEndpoint, {
|
|
77117
77279
|
method: "POST",
|
|
77118
77280
|
headers: {
|
|
77119
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
77120
|
-
...customHeaders
|
|
77281
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
77121
77282
|
},
|
|
77122
77283
|
body: params.toString()
|
|
77123
77284
|
});
|
|
@@ -77128,7 +77289,9 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
|
|
|
77128
77289
|
"TOKEN_EXCHANGE_FAILED"
|
|
77129
77290
|
);
|
|
77130
77291
|
}
|
|
77131
|
-
const tokenResponse = await response.json();
|
|
77292
|
+
const tokenResponse = OAuthTokensSchema.parse(await response.json());
|
|
77293
|
+
if (tokenResponse.token_type.toLowerCase() !== "bearer")
|
|
77294
|
+
throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
|
|
77132
77295
|
if (!tokenResponse.access_token) {
|
|
77133
77296
|
throw new OAuthError2("Token response missing access_token", "INVALID_TOKEN_RESPONSE");
|
|
77134
77297
|
}
|
|
@@ -77157,15 +77320,18 @@ function buildAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, sco
|
|
|
77157
77320
|
// OAuth 2.1 uses authorization code flow
|
|
77158
77321
|
client_id: clientId,
|
|
77159
77322
|
redirect_uri: redirectUri,
|
|
77160
|
-
scope: scopes.join(" "),
|
|
77161
77323
|
state,
|
|
77162
77324
|
code_challenge: codeChallenge,
|
|
77163
77325
|
code_challenge_method: "S256"
|
|
77164
77326
|
});
|
|
77327
|
+
if (scopes.length) params.set("scope", scopes.join(" "));
|
|
77165
77328
|
if (resource) {
|
|
77166
77329
|
params.set("resource", resource);
|
|
77167
77330
|
}
|
|
77168
|
-
|
|
77331
|
+
validateOAuthUrl(authorizationEndpoint);
|
|
77332
|
+
const url2 = new URL(authorizationEndpoint);
|
|
77333
|
+
params.forEach((value, key) => url2.searchParams.set(key, value));
|
|
77334
|
+
return url2.toString();
|
|
77169
77335
|
}
|
|
77170
77336
|
function generateSecureState() {
|
|
77171
77337
|
return crypto4.randomBytes(32).toString("base64url");
|
|
@@ -77246,110 +77412,26 @@ var AUTHORIZATION_CODE_HTML = `
|
|
|
77246
77412
|
</div>
|
|
77247
77413
|
|
|
77248
77414
|
<script>
|
|
77249
|
-
|
|
77250
|
-
|
|
77251
|
-
|
|
77252
|
-
|
|
77253
|
-
|
|
77254
|
-
|
|
77255
|
-
|
|
77256
|
-
|
|
77257
|
-
|
|
77258
|
-
|
|
77259
|
-
|
|
77260
|
-
|
|
77261
|
-
|
|
77262
|
-
|
|
77263
|
-
|
|
77264
|
-
|
|
77265
|
-
|
|
77266
|
-
|
|
77267
|
-
|
|
77268
|
-
|
|
77269
|
-
// Check for authorization code first (OAuth 2.1 flow)
|
|
77270
|
-
if (urlParams.has('code')) {
|
|
77271
|
-
const authData = {
|
|
77272
|
-
code: urlParams.get('code'),
|
|
77273
|
-
state: urlParams.get('state'),
|
|
77274
|
-
// Additional parameters for validation
|
|
77275
|
-
iss: urlParams.get('iss'),
|
|
77276
|
-
};
|
|
77277
|
-
|
|
77278
|
-
// Send authorization code to server for token exchange
|
|
77279
|
-
fetch('/oauth/callback', {
|
|
77280
|
-
method: 'POST',
|
|
77281
|
-
headers: {
|
|
77282
|
-
'Content-Type': 'application/json'
|
|
77283
|
-
},
|
|
77284
|
-
body: JSON.stringify(authData)
|
|
77285
|
-
})
|
|
77286
|
-
.then(response => {
|
|
77287
|
-
if (response.ok) {
|
|
77288
|
-
displayStatus('\u2705 Authorization successful!', 'success');
|
|
77289
|
-
displayDetails('You can safely close this window.');
|
|
77290
|
-
} else {
|
|
77291
|
-
return response.json().then(err => {
|
|
77292
|
-
throw new Error(err.error || 'Failed to exchange authorization code');
|
|
77293
|
-
});
|
|
77294
|
-
}
|
|
77295
|
-
})
|
|
77296
|
-
.catch(error => {
|
|
77297
|
-
displayStatus('\u274C Error completing authorization', 'error');
|
|
77298
|
-
displayDetails(error.message + '. Please try again.');
|
|
77299
|
-
});
|
|
77300
|
-
}
|
|
77301
|
-
// Check for access token in URL fragment (implicit flow)
|
|
77302
|
-
else if (hashParams.has('access_token')) {
|
|
77303
|
-
const tokens = {
|
|
77304
|
-
access_token: hashParams.get('access_token'),
|
|
77305
|
-
token_type: hashParams.get('token_type') || 'Bearer',
|
|
77306
|
-
expires_in: hashParams.get('expires_in') ? parseInt(hashParams.get('expires_in')) : 3600,
|
|
77307
|
-
scope: hashParams.get('scope'),
|
|
77308
|
-
state: hashParams.get('state'),
|
|
77309
|
-
obtained_at: Date.now()
|
|
77310
|
-
};
|
|
77311
|
-
|
|
77312
|
-
// Send tokens to server for storage
|
|
77313
|
-
fetch('/oauth/tokens', {
|
|
77314
|
-
method: 'POST',
|
|
77315
|
-
headers: {
|
|
77316
|
-
'Content-Type': 'application/json'
|
|
77317
|
-
},
|
|
77318
|
-
body: JSON.stringify(tokens)
|
|
77319
|
-
})
|
|
77320
|
-
.then(response => {
|
|
77321
|
-
if (response.ok) {
|
|
77322
|
-
displayStatus('\u2705 Authorization successful!', 'success');
|
|
77323
|
-
displayDetails('You can safely close this window.');
|
|
77324
|
-
} else {
|
|
77325
|
-
return response.json().then(err => {
|
|
77326
|
-
throw new Error(err.error || 'Failed to store access token');
|
|
77327
|
-
});
|
|
77328
|
-
}
|
|
77329
|
-
})
|
|
77330
|
-
.catch(error => {
|
|
77331
|
-
displayStatus('\u274C Error completing authorization', 'error');
|
|
77332
|
-
displayDetails(error.message + '. Please try again.');
|
|
77333
|
-
});
|
|
77334
|
-
}
|
|
77335
|
-
// Check for errors in query parameters
|
|
77336
|
-
else if (urlParams.has('error')) {
|
|
77337
|
-
const error = urlParams.get('error');
|
|
77338
|
-
const errorDescription = urlParams.get('error_description');
|
|
77339
|
-
displayStatus('\u274C Authorization failed', 'error');
|
|
77340
|
-
displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
|
|
77341
|
-
}
|
|
77342
|
-
// Check for errors in URL fragment (implicit flow errors)
|
|
77343
|
-
else if (hashParams.has('error')) {
|
|
77344
|
-
const error = hashParams.get('error');
|
|
77345
|
-
const errorDescription = hashParams.get('error_description');
|
|
77346
|
-
displayStatus('\u274C Authorization failed', 'error');
|
|
77347
|
-
displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
|
|
77348
|
-
}
|
|
77349
|
-
// No authorization code or access token found
|
|
77350
|
-
else {
|
|
77351
|
-
displayStatus('\u274C No authorization code or access token received', 'error');
|
|
77352
|
-
displayDetails('Please try the authorization process again.');
|
|
77415
|
+
const query = new URLSearchParams(window.location.search);
|
|
77416
|
+
const fragment = new URLSearchParams(window.location.hash.substring(1));
|
|
77417
|
+
const status = document.getElementById('status');
|
|
77418
|
+
const details = document.getElementById('details');
|
|
77419
|
+
const data = Object.fromEntries(query.has('code') || query.has('error') ? query : fragment);
|
|
77420
|
+
const endpoint = data.access_token ? '/oauth/tokens' : '/oauth/callback';
|
|
77421
|
+
if (data.code || data.error || data.access_token) {
|
|
77422
|
+
fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
|
|
77423
|
+
.then(async response => {
|
|
77424
|
+
const result = await response.json();
|
|
77425
|
+
if (!response.ok) throw new Error(result.error || 'Authorization response was rejected');
|
|
77426
|
+
status.textContent = data.access_token ? 'Authorization successful' : 'Authorization code received';
|
|
77427
|
+
status.className = 'success';
|
|
77428
|
+
details.textContent = data.access_token ? 'You can close this window.' : 'Return to your app to check the connection. Token exchange may still fail.';
|
|
77429
|
+
})
|
|
77430
|
+
.catch(error => { status.textContent = 'Authorization failed'; status.className = 'error'; details.textContent = error.message; })
|
|
77431
|
+
.finally(() => { document.getElementById('spinner').style.display = 'none'; });
|
|
77432
|
+
} else {
|
|
77433
|
+
status.textContent = 'No authorization response received';
|
|
77434
|
+
document.getElementById('spinner').style.display = 'none';
|
|
77353
77435
|
}
|
|
77354
77436
|
</script>
|
|
77355
77437
|
</body>
|
|
@@ -77364,6 +77446,17 @@ var OAuthCallbackServer = class {
|
|
|
77364
77446
|
this.setupRoutes();
|
|
77365
77447
|
}
|
|
77366
77448
|
setupRoutes() {
|
|
77449
|
+
this.app.use((req, res, next) => {
|
|
77450
|
+
const host = this.options.host === "::1" ? "[::1]" : this.options.host;
|
|
77451
|
+
const origin = req.headers.origin;
|
|
77452
|
+
const port = this.server?.address();
|
|
77453
|
+
const expectedOrigin = `http://${host}:${port && typeof port !== "string" ? port.port : this.options.port}`;
|
|
77454
|
+
if (req.hostname !== host || origin && origin !== expectedOrigin) {
|
|
77455
|
+
res.status(403).json({ error: "Untrusted callback origin" });
|
|
77456
|
+
return;
|
|
77457
|
+
}
|
|
77458
|
+
next();
|
|
77459
|
+
});
|
|
77367
77460
|
this.app.use(import_express.default.json());
|
|
77368
77461
|
this.app.get("/oauth/callback", (req, res) => {
|
|
77369
77462
|
logger.oauth("OAuth 2.1 authorization callback page requested");
|
|
@@ -77371,46 +77464,47 @@ var OAuthCallbackServer = class {
|
|
|
77371
77464
|
});
|
|
77372
77465
|
this.app.post("/oauth/callback", async (req, res) => {
|
|
77373
77466
|
try {
|
|
77374
|
-
const { code, state, iss } = req.body;
|
|
77375
|
-
|
|
77376
|
-
|
|
77377
|
-
|
|
77378
|
-
|
|
77379
|
-
|
|
77380
|
-
|
|
77381
|
-
state: state || "none",
|
|
77382
|
-
issuer: iss || "none"
|
|
77383
|
-
});
|
|
77467
|
+
const { code, state, iss, error: error51 } = req.body;
|
|
77468
|
+
this.validateResponse(state, iss);
|
|
77469
|
+
if (error51) throw new OAuthError2("Authorization denied or failed", "AUTH_DENIED");
|
|
77470
|
+
if (this.options.flow === "implicit")
|
|
77471
|
+
throw new OAuthError2("Authorization-code callback is disabled", "WRONG_FLOW");
|
|
77472
|
+
if (typeof code !== "string" || !code)
|
|
77473
|
+
throw new OAuthError2("No authorization code received", "INVALID_CODE");
|
|
77384
77474
|
this.events.emit("oauth-code-received", { code, state, iss });
|
|
77385
|
-
res.json({
|
|
77475
|
+
res.json({
|
|
77476
|
+
success: true,
|
|
77477
|
+
message: "Authorization code received; check connection status in the app"
|
|
77478
|
+
});
|
|
77386
77479
|
} catch (error51) {
|
|
77387
|
-
|
|
77388
|
-
|
|
77389
|
-
|
|
77390
|
-
res.status(400).json({ error: errorMessage });
|
|
77480
|
+
const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid authorization response");
|
|
77481
|
+
if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
|
|
77482
|
+
res.status(400).json({ error: failure.message });
|
|
77391
77483
|
}
|
|
77392
77484
|
});
|
|
77393
77485
|
this.app.post("/oauth/tokens", async (req, res) => {
|
|
77486
|
+
if (this.options.flow !== "implicit") {
|
|
77487
|
+
res.status(404).json({ error: "Implicit token callback is disabled" });
|
|
77488
|
+
return;
|
|
77489
|
+
}
|
|
77394
77490
|
try {
|
|
77395
|
-
|
|
77396
|
-
if (!
|
|
77397
|
-
throw new OAuthError2("
|
|
77398
|
-
|
|
77399
|
-
|
|
77400
|
-
|
|
77401
|
-
|
|
77402
|
-
|
|
77403
|
-
|
|
77404
|
-
|
|
77405
|
-
});
|
|
77491
|
+
this.validateResponse(req.body.state, req.body.iss);
|
|
77492
|
+
if (typeof req.body.access_token !== "string" || !req.body.access_token || req.body.token_type && req.body.token_type.toLowerCase() !== "bearer")
|
|
77493
|
+
throw new OAuthError2("Invalid implicit token response");
|
|
77494
|
+
const tokens = {
|
|
77495
|
+
access_token: req.body.access_token,
|
|
77496
|
+
token_type: "Bearer",
|
|
77497
|
+
expires_in: req.body.expires_in === void 0 ? void 0 : Number(req.body.expires_in),
|
|
77498
|
+
scope: req.body.scope,
|
|
77499
|
+
obtained_at: Date.now()
|
|
77500
|
+
};
|
|
77406
77501
|
await writeTokens(this.options.serverUrlHash, tokens);
|
|
77407
77502
|
this.events.emit("oauth-success", tokens);
|
|
77408
|
-
res.json({ success: true
|
|
77503
|
+
res.json({ success: true });
|
|
77409
77504
|
} catch (error51) {
|
|
77410
|
-
|
|
77411
|
-
|
|
77412
|
-
|
|
77413
|
-
res.status(400).json({ error: errorMessage });
|
|
77505
|
+
const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid implicit response");
|
|
77506
|
+
if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
|
|
77507
|
+
res.status(400).json({ error: failure.message });
|
|
77414
77508
|
}
|
|
77415
77509
|
});
|
|
77416
77510
|
this.app.get("/oauth/health", (req, res) => {
|
|
@@ -77427,10 +77521,17 @@ var OAuthCallbackServer = class {
|
|
|
77427
77521
|
}
|
|
77428
77522
|
);
|
|
77429
77523
|
}
|
|
77524
|
+
validateResponse(state, issuer) {
|
|
77525
|
+
if (!this.options.state || state !== this.options.state)
|
|
77526
|
+
throw new OAuthError2("Invalid callback state", "INVALID_STATE");
|
|
77527
|
+
if ((issuer !== void 0 || this.options.requireIssuer) && issuer !== this.options.issuer)
|
|
77528
|
+
throw new OAuthError2("Invalid callback issuer", "INVALID_ISSUER");
|
|
77529
|
+
}
|
|
77430
77530
|
async start() {
|
|
77431
77531
|
return new Promise((resolve, reject) => {
|
|
77432
77532
|
try {
|
|
77433
77533
|
this.server = this.app.listen(this.options.port, this.options.host, (error51) => {
|
|
77534
|
+
clearTimeout(this.startupTimer);
|
|
77434
77535
|
if (error51) {
|
|
77435
77536
|
reject(error51);
|
|
77436
77537
|
return;
|
|
@@ -77441,11 +77542,12 @@ var OAuthCallbackServer = class {
|
|
|
77441
77542
|
resolve();
|
|
77442
77543
|
});
|
|
77443
77544
|
this.server.on("error", (error51) => {
|
|
77545
|
+
clearTimeout(this.startupTimer);
|
|
77444
77546
|
logger.error("OAuth callback server error", "OAUTH", error51);
|
|
77445
77547
|
reject(error51);
|
|
77446
77548
|
});
|
|
77447
77549
|
if (this.options.timeout) {
|
|
77448
|
-
setTimeout(() => {
|
|
77550
|
+
this.startupTimer = setTimeout(() => {
|
|
77449
77551
|
if (!this.server?.listening) {
|
|
77450
77552
|
reject(new OAuthError2("OAuth callback server startup timeout", "TIMEOUT"));
|
|
77451
77553
|
}
|
|
@@ -77457,6 +77559,7 @@ var OAuthCallbackServer = class {
|
|
|
77457
77559
|
});
|
|
77458
77560
|
}
|
|
77459
77561
|
async stop() {
|
|
77562
|
+
clearTimeout(this.startupTimer);
|
|
77460
77563
|
if (!this.server) return;
|
|
77461
77564
|
const server = this.server;
|
|
77462
77565
|
this.server = null;
|
|
@@ -77488,7 +77591,9 @@ var MCPOAuthProvider = class {
|
|
|
77488
77591
|
this.authServerMetadata = null;
|
|
77489
77592
|
this.resourceMetadata = null;
|
|
77490
77593
|
const serverUrl = options.serverUrl || CONFIG.WP_API_URL;
|
|
77491
|
-
const defaultScopes = getDefaultOAuthScopes();
|
|
77594
|
+
const defaultScopes = CONFIG.OAUTH_SCOPES ? getDefaultOAuthScopes() : [];
|
|
77595
|
+
this.explicitScopes = options.scopes ?? defaultScopes;
|
|
77596
|
+
this.configuredClientId = options.clientId || CONFIG.WP_OAUTH_CLIENT_ID || void 0;
|
|
77492
77597
|
this.config = {
|
|
77493
77598
|
serverUrl,
|
|
77494
77599
|
resource: generateCanonicalResourceURI(serverUrl),
|
|
@@ -77547,187 +77652,217 @@ var MCPOAuthProvider = class {
|
|
|
77547
77652
|
/**
|
|
77548
77653
|
* Discover OAuth 2.1 server endpoints using MCP-compliant discovery
|
|
77549
77654
|
*/
|
|
77550
|
-
async discoverOAuthEndpoints() {
|
|
77551
|
-
|
|
77552
|
-
|
|
77553
|
-
|
|
77554
|
-
|
|
77555
|
-
|
|
77556
|
-
|
|
77557
|
-
|
|
77558
|
-
|
|
77559
|
-
|
|
77560
|
-
|
|
77561
|
-
|
|
77562
|
-
|
|
77563
|
-
|
|
77564
|
-
|
|
77565
|
-
|
|
77566
|
-
|
|
77567
|
-
|
|
77568
|
-
|
|
77569
|
-
this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
|
|
77570
|
-
} else {
|
|
77571
|
-
throw new OAuthError2("Could not determine authorization server URL", "DISCOVERY_FAILED");
|
|
77572
|
-
}
|
|
77573
|
-
} catch (error51) {
|
|
77574
|
-
logger.error("OAuth endpoint discovery failed", "OAUTH", error51);
|
|
77575
|
-
throw error51;
|
|
77576
|
-
}
|
|
77577
|
-
}
|
|
77578
|
-
/**
|
|
77579
|
-
* Discover endpoints via 401 Unauthorized response (RFC 9728 Section 5.1)
|
|
77580
|
-
*/
|
|
77581
|
-
async discoverVia401Response() {
|
|
77582
|
-
try {
|
|
77583
|
-
logger.oauth("Attempting discovery via 401 response method");
|
|
77584
|
-
const customHeaders = getCustomHeaders();
|
|
77585
|
-
const response = await proxyFetch(this.config.serverUrl, {
|
|
77586
|
-
headers: {
|
|
77587
|
-
Accept: "application/json",
|
|
77588
|
-
...customHeaders
|
|
77589
|
-
}
|
|
77590
|
-
});
|
|
77591
|
-
if (response.status === 401) {
|
|
77592
|
-
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
77593
|
-
if (wwwAuthHeader) {
|
|
77594
|
-
const authInfo = parseWWWAuthenticateHeader(wwwAuthHeader);
|
|
77595
|
-
logger.oauth("WWW-Authenticate header parsed successfully");
|
|
77596
|
-
if (authInfo.resource_metadata_url) {
|
|
77597
|
-
const metadataResponse = await proxyFetch(authInfo.resource_metadata_url, {
|
|
77598
|
-
headers: {
|
|
77599
|
-
Accept: "application/json",
|
|
77600
|
-
...customHeaders
|
|
77601
|
-
}
|
|
77602
|
-
});
|
|
77603
|
-
if (metadataResponse.ok) {
|
|
77604
|
-
this.resourceMetadata = await metadataResponse.json();
|
|
77605
|
-
logger.oauth("Protected resource metadata obtained via 401 response");
|
|
77606
|
-
}
|
|
77607
|
-
}
|
|
77608
|
-
}
|
|
77609
|
-
}
|
|
77610
|
-
} catch (error51) {
|
|
77611
|
-
logger.warn("401 response discovery method failed", "OAUTH", error51);
|
|
77612
|
-
}
|
|
77655
|
+
async discoverOAuthEndpoints(challenge) {
|
|
77656
|
+
this.resourceMetadata = await discoverProtectedResourceMetadata(
|
|
77657
|
+
this.config.serverUrl,
|
|
77658
|
+
challenge?.resource_metadata
|
|
77659
|
+
);
|
|
77660
|
+
this.authServerMetadata = await discoverAuthorizationServerMetadata(
|
|
77661
|
+
this.resourceMetadata.authorization_servers[0]
|
|
77662
|
+
);
|
|
77663
|
+
this.config.resource = this.resourceMetadata.resource;
|
|
77664
|
+
this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
|
|
77665
|
+
this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
|
|
77666
|
+
this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
|
|
77667
|
+
this.config.clientId = this.configuredClientId;
|
|
77668
|
+
const challenged = challenge?.scope?.split(/\s+/).filter(Boolean);
|
|
77669
|
+
this.config.scopes = [
|
|
77670
|
+
...new Set(
|
|
77671
|
+
challenged ?? (this.explicitScopes.length ? this.explicitScopes : this.resourceMetadata.scopes_supported ?? [])
|
|
77672
|
+
)
|
|
77673
|
+
];
|
|
77613
77674
|
}
|
|
77614
|
-
/**
|
|
77615
|
-
* Get authorization server URL from discovered metadata
|
|
77616
|
-
*/
|
|
77617
77675
|
getAuthorizationServerUrl() {
|
|
77618
|
-
|
|
77619
|
-
return this.resourceMetadata.authorization_servers[0];
|
|
77620
|
-
}
|
|
77621
|
-
try {
|
|
77622
|
-
const url2 = new URL(this.config.serverUrl);
|
|
77623
|
-
return url2.origin;
|
|
77624
|
-
} catch {
|
|
77625
|
-
return null;
|
|
77626
|
-
}
|
|
77676
|
+
return this.authServerMetadata?.issuer ?? this.resourceMetadata?.authorization_servers[0] ?? new URL(this.config.serverUrl).origin;
|
|
77627
77677
|
}
|
|
77628
77678
|
/**
|
|
77629
77679
|
* Perform dynamic client registration if needed and supported
|
|
77630
77680
|
*/
|
|
77631
77681
|
async ensureClientRegistration() {
|
|
77682
|
+
const issuer = this.getAuthorizationServerUrl();
|
|
77683
|
+
const stored = await readClientInfo(this.serverUrlHash);
|
|
77632
77684
|
if (this.config.clientId) {
|
|
77633
|
-
|
|
77685
|
+
if (stored?.issuer && stored.issuer !== issuer && stored.client_id === this.config.clientId) {
|
|
77686
|
+
throw new OAuthError2(
|
|
77687
|
+
"Configured client belongs to another authorization server; update its registration",
|
|
77688
|
+
"CLIENT_ISSUER_MISMATCH"
|
|
77689
|
+
);
|
|
77690
|
+
}
|
|
77691
|
+
await writeClientInfo(this.serverUrlHash, { client_id: this.config.clientId, issuer });
|
|
77634
77692
|
return;
|
|
77635
77693
|
}
|
|
77636
|
-
|
|
77637
|
-
|
|
77638
|
-
this.config.clientId = storedClientInfo.client_id;
|
|
77639
|
-
logger.oauth("Reusing persisted client registration, skipping dynamic registration");
|
|
77694
|
+
if (stored?.issuer === issuer && typeof stored.client_id === "string" && stored.client_id.length > 0) {
|
|
77695
|
+
this.config.clientId = stored.client_id;
|
|
77640
77696
|
return;
|
|
77641
77697
|
}
|
|
77642
|
-
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
|
|
77698
|
+
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
|
|
77643
77699
|
throw new OAuthError2(
|
|
77644
|
-
"No client
|
|
77700
|
+
"No issuer-bound client registration; configure a client ID or enable registration",
|
|
77645
77701
|
"NO_CLIENT_ID"
|
|
77646
77702
|
);
|
|
77647
|
-
|
|
77648
|
-
if (!this.authServerMetadata?.registration_endpoint) {
|
|
77703
|
+
if (!this.authServerMetadata?.registration_endpoint)
|
|
77649
77704
|
throw new OAuthError2(
|
|
77650
|
-
"
|
|
77705
|
+
"Authorization server does not support dynamic registration; configure its client ID",
|
|
77651
77706
|
"NO_DYNAMIC_REGISTRATION"
|
|
77652
77707
|
);
|
|
77653
|
-
|
|
77654
|
-
|
|
77655
|
-
|
|
77656
|
-
|
|
77657
|
-
|
|
77658
|
-
|
|
77659
|
-
|
|
77660
|
-
|
|
77661
|
-
|
|
77662
|
-
|
|
77663
|
-
|
|
77664
|
-
|
|
77665
|
-
|
|
77666
|
-
|
|
77667
|
-
|
|
77668
|
-
|
|
77669
|
-
|
|
77670
|
-
|
|
77671
|
-
|
|
77672
|
-
};
|
|
77673
|
-
await writeClientInfo(this.serverUrlHash, clientInfo);
|
|
77674
|
-
this.config.clientId = registrationResponse.client_id;
|
|
77675
|
-
logger.oauth("Dynamic client registration completed successfully");
|
|
77676
|
-
} catch (error51) {
|
|
77677
|
-
logger.error("Dynamic client registration failed", "OAUTH", error51);
|
|
77678
|
-
throw error51;
|
|
77679
|
-
}
|
|
77708
|
+
const request = {
|
|
77709
|
+
redirect_uris: [this.config.redirectUri],
|
|
77710
|
+
application_type: "native",
|
|
77711
|
+
token_endpoint_auth_method: "none",
|
|
77712
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
77713
|
+
response_types: ["code"],
|
|
77714
|
+
client_name: "WordPress MCP Remote",
|
|
77715
|
+
...this.config.scopes.length ? { scope: this.config.scopes.join(" ") } : {}
|
|
77716
|
+
};
|
|
77717
|
+
const registered = await registerDynamicClient(
|
|
77718
|
+
this.authServerMetadata.registration_endpoint,
|
|
77719
|
+
request
|
|
77720
|
+
);
|
|
77721
|
+
await writeClientInfo(this.serverUrlHash, {
|
|
77722
|
+
...request,
|
|
77723
|
+
client_id: registered.client_id,
|
|
77724
|
+
issuer
|
|
77725
|
+
});
|
|
77726
|
+
this.config.clientId = registered.client_id;
|
|
77680
77727
|
}
|
|
77681
77728
|
/**
|
|
77682
77729
|
* Initiate MCP-compliant OAuth 2.1 authorization flow
|
|
77683
77730
|
*/
|
|
77684
|
-
|
|
77685
|
-
|
|
77686
|
-
|
|
77687
|
-
|
|
77688
|
-
|
|
77689
|
-
|
|
77690
|
-
const
|
|
77691
|
-
|
|
77692
|
-
|
|
77693
|
-
|
|
77731
|
+
satisfies(tokens, request) {
|
|
77732
|
+
return !!tokens && tokens.access_token !== request.rejectedToken && (!request.scopes?.length || request.scopes.every((scope) => tokens.scope?.split(" ").includes(scope)));
|
|
77733
|
+
}
|
|
77734
|
+
async authorize(request = {}) {
|
|
77735
|
+
if (this.authPromise) await this.authPromise;
|
|
77736
|
+
if (!this.authPromise) this.authPromise = this.authorizeWithLock(request);
|
|
77737
|
+
const pending = this.authPromise;
|
|
77738
|
+
try {
|
|
77739
|
+
await pending;
|
|
77740
|
+
} finally {
|
|
77741
|
+
if (this.authPromise === pending) this.authPromise = null;
|
|
77694
77742
|
}
|
|
77695
|
-
|
|
77696
|
-
|
|
77743
|
+
}
|
|
77744
|
+
async authorizeWithLock(request) {
|
|
77745
|
+
const valid = await this.tokens();
|
|
77746
|
+
if (this.satisfies(valid, request)) return valid;
|
|
77747
|
+
await ensureConfigDir();
|
|
77748
|
+
const file2 = getConfigFilePath(this.serverUrlHash, "oauth");
|
|
77749
|
+
let release;
|
|
77697
77750
|
try {
|
|
77698
|
-
await
|
|
77699
|
-
logger.oauth("OAuth 2.1 authorization completed successfully");
|
|
77751
|
+
release = await lockfile.lock(file2, { realpath: false });
|
|
77700
77752
|
} catch (error51) {
|
|
77701
|
-
|
|
77702
|
-
|
|
77753
|
+
if (error51.code !== "ELOCKED") throw error51;
|
|
77754
|
+
const deadline = Date.now() + this.config.timeout;
|
|
77755
|
+
while (Date.now() < deadline) {
|
|
77756
|
+
const tokens = await this.tokens();
|
|
77757
|
+
if (this.satisfies(tokens, request)) return tokens;
|
|
77758
|
+
if (!await lockfile.check(file2, { realpath: false })) {
|
|
77759
|
+
const completed = await this.tokens();
|
|
77760
|
+
if (this.satisfies(completed, request)) return completed;
|
|
77761
|
+
throw new OAuthError2("Authentication in another process did not complete", "AUTH_FAILED");
|
|
77762
|
+
}
|
|
77763
|
+
await delay(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
77764
|
+
}
|
|
77765
|
+
throw new OAuthError2("Timeout waiting for authentication in another process", "LOCK_TIMEOUT");
|
|
77766
|
+
}
|
|
77767
|
+
try {
|
|
77768
|
+
const completed = await this.tokens();
|
|
77769
|
+
if (this.satisfies(completed, request)) return completed;
|
|
77770
|
+
await this.discoverOAuthEndpoints(request.challenge);
|
|
77771
|
+
const issuer = this.getAuthorizationServerUrl();
|
|
77772
|
+
const stored = await readTokens(this.serverUrlHash);
|
|
77773
|
+
const client = await readClientInfo(this.serverUrlHash);
|
|
77774
|
+
if (this.configuredClientId && client?.issuer && client.issuer !== issuer && client.client_id === this.configuredClientId) {
|
|
77775
|
+
throw new OAuthError2(
|
|
77776
|
+
"Configured client belongs to another authorization server",
|
|
77777
|
+
"CLIENT_ISSUER_MISMATCH"
|
|
77778
|
+
);
|
|
77779
|
+
}
|
|
77780
|
+
this.config.scopes = [
|
|
77781
|
+
.../* @__PURE__ */ new Set([
|
|
77782
|
+
...stored?.issuer === issuer ? stored.scope?.split(" ").filter(Boolean) ?? [] : [],
|
|
77783
|
+
...this.config.scopes,
|
|
77784
|
+
...request.scopes ?? []
|
|
77785
|
+
])
|
|
77786
|
+
];
|
|
77787
|
+
const needsScope = !!request.scopes?.some(
|
|
77788
|
+
(scope) => !stored?.scope?.split(" ").includes(scope)
|
|
77789
|
+
);
|
|
77790
|
+
const clientId = this.configuredClientId || client?.client_id;
|
|
77791
|
+
if (stored?.issuer === issuer && client?.issuer === issuer && stored.refresh_token && clientId && (!this.configuredClientId || this.configuredClientId === client.client_id) && !needsScope) {
|
|
77792
|
+
try {
|
|
77793
|
+
const renewed = await refreshAuthorization(issuer, {
|
|
77794
|
+
metadata: { ...this.authServerMetadata },
|
|
77795
|
+
clientInformation: { client_id: clientId },
|
|
77796
|
+
refreshToken: stored.refresh_token,
|
|
77797
|
+
resource: this.resourceIndicator(),
|
|
77798
|
+
fetchFn: (url2, init) => oauthFetch(String(url2), init)
|
|
77799
|
+
});
|
|
77800
|
+
if (renewed.token_type.toLowerCase() !== "bearer")
|
|
77801
|
+
throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
|
|
77802
|
+
const tokens = {
|
|
77803
|
+
...renewed,
|
|
77804
|
+
issuer,
|
|
77805
|
+
scope: renewed.scope ?? stored.scope,
|
|
77806
|
+
obtained_at: Date.now()
|
|
77807
|
+
};
|
|
77808
|
+
await writeTokens(this.serverUrlHash, tokens);
|
|
77809
|
+
return tokens;
|
|
77810
|
+
} catch (error51) {
|
|
77811
|
+
if (!(error51 instanceof InvalidGrantError)) throw error51;
|
|
77812
|
+
await deleteConfigFile(this.serverUrlHash, "tokens.json");
|
|
77813
|
+
}
|
|
77814
|
+
}
|
|
77815
|
+
return await this.performAuthorization();
|
|
77703
77816
|
} finally {
|
|
77704
|
-
|
|
77817
|
+
await release();
|
|
77705
77818
|
}
|
|
77706
77819
|
}
|
|
77820
|
+
resourceIndicator() {
|
|
77821
|
+
if (!CONFIG.OAUTH_RESOURCE_INDICATOR)
|
|
77822
|
+
throw new OAuthError2(
|
|
77823
|
+
"MCP OAuth requires resource indicators; remove OAUTH_RESOURCE_INDICATOR=false",
|
|
77824
|
+
"RESOURCE_REQUIRED"
|
|
77825
|
+
);
|
|
77826
|
+
return this.config.resource;
|
|
77827
|
+
}
|
|
77707
77828
|
/**
|
|
77708
77829
|
* Perform the complete OAuth 2.1 authorization flow
|
|
77709
77830
|
*/
|
|
77710
77831
|
async performAuthorization() {
|
|
77711
77832
|
try {
|
|
77712
|
-
await this.discoverOAuthEndpoints();
|
|
77713
|
-
|
|
77833
|
+
if (!this.authServerMetadata) await this.discoverOAuthEndpoints();
|
|
77834
|
+
if (!this.authServerMetadata?.code_challenge_methods_supported?.includes("S256"))
|
|
77835
|
+
throw new OAuthError2(
|
|
77836
|
+
"Authorization server must advertise PKCE S256 support",
|
|
77837
|
+
"PKCE_REQUIRED"
|
|
77838
|
+
);
|
|
77839
|
+
this.resourceIndicator();
|
|
77840
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(this.config.host))
|
|
77841
|
+
throw new OAuthError2(
|
|
77842
|
+
"Native OAuth callback must bind to loopback",
|
|
77843
|
+
"INVALID_CALLBACK_HOST"
|
|
77844
|
+
);
|
|
77845
|
+
this.currentPKCE = generatePKCE();
|
|
77846
|
+
this.currentState = generateSecureState();
|
|
77847
|
+
const callbackPort = this.config.callbackPort === 0 ? await selectCallbackPort(this.config.serverUrl, void 0, false, this.config.host) : this.config.callbackPort;
|
|
77714
77848
|
const callbackServer = setupWPOAuthCallbackServer(
|
|
77715
77849
|
{
|
|
77716
77850
|
port: callbackPort,
|
|
77717
77851
|
host: this.config.host,
|
|
77718
77852
|
serverUrlHash: this.serverUrlHash,
|
|
77719
|
-
timeout: this.config.timeout
|
|
77853
|
+
timeout: this.config.timeout,
|
|
77854
|
+
state: this.currentState,
|
|
77855
|
+
issuer: this.authServerMetadata.issuer,
|
|
77856
|
+
requireIssuer: this.authServerMetadata.authorization_response_iss_parameter_supported
|
|
77720
77857
|
},
|
|
77721
77858
|
this.events
|
|
77722
77859
|
);
|
|
77723
77860
|
await callbackServer.start();
|
|
77724
77861
|
logger.oauth("OAuth callback server started");
|
|
77725
77862
|
try {
|
|
77726
|
-
const actualRedirectUri = `http://${this.config.host}:${callbackPort}/oauth/callback`;
|
|
77863
|
+
const actualRedirectUri = `http://${this.config.host === "::1" ? "[::1]" : this.config.host}:${callbackPort}/oauth/callback`;
|
|
77727
77864
|
this.config.redirectUri = actualRedirectUri;
|
|
77728
77865
|
await this.ensureClientRegistration();
|
|
77729
|
-
this.currentPKCE = generatePKCE();
|
|
77730
|
-
this.currentState = generateSecureState();
|
|
77731
77866
|
await writeTextFile(this.serverUrlHash, "pkce_verifier.txt", this.currentPKCE.codeVerifier);
|
|
77732
77867
|
await writeTextFile(this.serverUrlHash, "oauth_state.txt", this.currentState);
|
|
77733
77868
|
const authUrl = buildAuthorizationUrl(
|
|
@@ -77737,10 +77872,13 @@ var MCPOAuthProvider = class {
|
|
|
77737
77872
|
this.config.scopes,
|
|
77738
77873
|
this.currentState,
|
|
77739
77874
|
this.currentPKCE.codeChallenge,
|
|
77740
|
-
|
|
77875
|
+
this.resourceIndicator()
|
|
77741
77876
|
);
|
|
77742
77877
|
logger.oauth("Built OAuth 2.1 authorization URL");
|
|
77743
77878
|
logger.debug("Authorization URL", "OAUTH", { url: authUrl });
|
|
77879
|
+
const authorizationCode = this.waitForAuthorizationCode();
|
|
77880
|
+
void authorizationCode.catch(() => {
|
|
77881
|
+
});
|
|
77744
77882
|
try {
|
|
77745
77883
|
await open_default(authUrl);
|
|
77746
77884
|
logger.oauth("Browser opened successfully");
|
|
@@ -77751,13 +77889,20 @@ var MCPOAuthProvider = class {
|
|
|
77751
77889
|
logger.info(`${authUrl}`);
|
|
77752
77890
|
logger.info("===============================\n");
|
|
77753
77891
|
}
|
|
77754
|
-
const authCode = await
|
|
77755
|
-
const
|
|
77892
|
+
const authCode = await authorizationCode;
|
|
77893
|
+
const response = await this.exchangeCodeForTokens(authCode, actualRedirectUri);
|
|
77894
|
+
const tokens = {
|
|
77895
|
+
...response,
|
|
77896
|
+
issuer: this.getAuthorizationServerUrl(),
|
|
77897
|
+
scope: response.scope ?? this.config.scopes.join(" ")
|
|
77898
|
+
};
|
|
77756
77899
|
await writeTokens(this.serverUrlHash, tokens);
|
|
77757
77900
|
logger.oauth("OAuth 2.1 tokens stored successfully");
|
|
77758
77901
|
return tokens;
|
|
77759
77902
|
} finally {
|
|
77760
77903
|
await callbackServer.stop();
|
|
77904
|
+
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
77905
|
+
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
77761
77906
|
}
|
|
77762
77907
|
} catch (error51) {
|
|
77763
77908
|
logger.error("Error during OAuth 2.1 authorization flow", "OAUTH", error51);
|
|
@@ -77778,12 +77923,16 @@ var MCPOAuthProvider = class {
|
|
|
77778
77923
|
this.events.removeAllListeners("oauth-error");
|
|
77779
77924
|
clearTimeout(timeout);
|
|
77780
77925
|
};
|
|
77781
|
-
this.events.once("oauth-code-received", async ({ code, state }) => {
|
|
77926
|
+
this.events.once("oauth-code-received", async ({ code, state, iss }) => {
|
|
77782
77927
|
cleanup();
|
|
77783
77928
|
if (state !== this.currentState) {
|
|
77784
77929
|
reject(new OAuthError2("State parameter mismatch", "INVALID_STATE"));
|
|
77785
77930
|
return;
|
|
77786
77931
|
}
|
|
77932
|
+
if ((iss !== void 0 || this.authServerMetadata?.authorization_response_iss_parameter_supported) && iss !== this.authServerMetadata?.issuer) {
|
|
77933
|
+
reject(new OAuthError2("Authorization response issuer mismatch", "INVALID_ISSUER"));
|
|
77934
|
+
return;
|
|
77935
|
+
}
|
|
77787
77936
|
logger.oauth("Valid authorization code received");
|
|
77788
77937
|
resolve(code);
|
|
77789
77938
|
});
|
|
@@ -77809,7 +77958,7 @@ var MCPOAuthProvider = class {
|
|
|
77809
77958
|
redirectUri,
|
|
77810
77959
|
this.config.clientId,
|
|
77811
77960
|
codeVerifier,
|
|
77812
|
-
|
|
77961
|
+
this.resourceIndicator()
|
|
77813
77962
|
);
|
|
77814
77963
|
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
77815
77964
|
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
@@ -77875,6 +78024,7 @@ import * as path5 from "node:path";
|
|
|
77875
78024
|
import * as os3 from "node:os";
|
|
77876
78025
|
|
|
77877
78026
|
// src/lib/persistent-oauth-client-provider.ts
|
|
78027
|
+
import { randomBytes } from "node:crypto";
|
|
77878
78028
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
77879
78029
|
var PersistentWPOAuthClientProvider = class {
|
|
77880
78030
|
constructor(options) {
|
|
@@ -78032,7 +78182,10 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78032
78182
|
*/
|
|
78033
78183
|
async performAuthorization() {
|
|
78034
78184
|
const callbackPort = this.options.callbackPort === 0 ? await getOAuthCallbackPort() : this.options.callbackPort;
|
|
78185
|
+
const state = this.generateState();
|
|
78035
78186
|
const callbackServerOptions = {
|
|
78187
|
+
flow: "implicit",
|
|
78188
|
+
state,
|
|
78036
78189
|
port: callbackPort,
|
|
78037
78190
|
host: this.options.host,
|
|
78038
78191
|
serverUrlHash: this.serverUrlHash,
|
|
@@ -78044,12 +78197,14 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78044
78197
|
logger.oauth("Starting callback server...");
|
|
78045
78198
|
await callbackServer.start();
|
|
78046
78199
|
logger.oauth("Callback server started successfully");
|
|
78047
|
-
const state = this.generateState();
|
|
78048
78200
|
logger.debug(`Generated state parameter: ${state}`, "OAUTH");
|
|
78049
78201
|
const authUrl = this.buildAuthorizationUrl(callbackServer.getCallbackUrl(), state);
|
|
78050
78202
|
logger.oauth(`Built authorization URL: ${authUrl}`);
|
|
78051
78203
|
logger.debug(`Callback URL: ${callbackServer.getCallbackUrl()}`, "OAUTH");
|
|
78052
78204
|
logger.oauth("Attempting to open browser...");
|
|
78205
|
+
const authorization = this.waitForAuthorizationResult();
|
|
78206
|
+
void authorization.catch(() => {
|
|
78207
|
+
});
|
|
78053
78208
|
try {
|
|
78054
78209
|
await open_default(authUrl);
|
|
78055
78210
|
logger.oauth("Browser opened successfully");
|
|
@@ -78061,7 +78216,7 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78061
78216
|
logger.info("===============================\n");
|
|
78062
78217
|
}
|
|
78063
78218
|
logger.oauth("Waiting for authorization result...");
|
|
78064
|
-
const tokens = await
|
|
78219
|
+
const tokens = await authorization;
|
|
78065
78220
|
logger.oauth("Authorization result received");
|
|
78066
78221
|
logger.oauth("Saving tokens to persistent storage...");
|
|
78067
78222
|
await this.saveTokens(tokens);
|
|
@@ -78114,7 +78269,7 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78114
78269
|
* Generate a secure state parameter
|
|
78115
78270
|
*/
|
|
78116
78271
|
generateState() {
|
|
78117
|
-
return
|
|
78272
|
+
return randomBytes(32).toString("base64url");
|
|
78118
78273
|
}
|
|
78119
78274
|
/**
|
|
78120
78275
|
* Wait for authorization result from callback server
|
|
@@ -78537,6 +78692,12 @@ function constructApiUrl(baseUrl, defaultEndpoint) {
|
|
|
78537
78692
|
return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
|
|
78538
78693
|
}
|
|
78539
78694
|
}
|
|
78695
|
+
function getMCPOAuthProvider() {
|
|
78696
|
+
return mcpOAuthProvider ??= new MCPOAuthProvider({
|
|
78697
|
+
serverUrl: getRequestUrl(),
|
|
78698
|
+
clientId: CONFIG.WP_OAUTH_CLIENT_ID
|
|
78699
|
+
});
|
|
78700
|
+
}
|
|
78540
78701
|
async function getOAuthTokens() {
|
|
78541
78702
|
try {
|
|
78542
78703
|
if (!CONFIG.OAUTH_ENABLED) {
|
|
@@ -78544,7 +78705,7 @@ async function getOAuthTokens() {
|
|
|
78544
78705
|
return null;
|
|
78545
78706
|
}
|
|
78546
78707
|
logger.auth("Attempting to get OAuth tokens for WordPress API (MCP-compliant)...");
|
|
78547
|
-
const serverUrl =
|
|
78708
|
+
const serverUrl = getRequestUrl();
|
|
78548
78709
|
const serverUrlHash = generateServerUrlHash(serverUrl);
|
|
78549
78710
|
const existingTokens = await getValidTokens(serverUrlHash);
|
|
78550
78711
|
if (existingTokens) {
|
|
@@ -78555,16 +78716,10 @@ async function getOAuthTokens() {
|
|
|
78555
78716
|
logger.auth("Starting MCP-compliant OAuth 2.1 authentication flow");
|
|
78556
78717
|
logger.auth("Your browser should open automatically for authentication");
|
|
78557
78718
|
if (CONFIG.OAUTH_FLOW_TYPE === "authorization_code" && CONFIG.OAUTH_USE_PKCE) {
|
|
78558
|
-
if (!mcpOAuthProvider) {
|
|
78559
|
-
mcpOAuthProvider = new MCPOAuthProvider({
|
|
78560
|
-
serverUrl,
|
|
78561
|
-
clientId: CONFIG.WP_OAUTH_CLIENT_ID,
|
|
78562
|
-
scopes: getDefaultOAuthScopes()
|
|
78563
|
-
});
|
|
78564
|
-
}
|
|
78565
78719
|
logger.auth("Using MCP-compliant OAuth 2.1 authorization code flow with PKCE");
|
|
78566
|
-
|
|
78567
|
-
|
|
78720
|
+
const provider = getMCPOAuthProvider();
|
|
78721
|
+
await provider.authorize();
|
|
78722
|
+
const tokens = await provider.tokens();
|
|
78568
78723
|
if (tokens) {
|
|
78569
78724
|
logger.auth("MCP OAuth 2.1 tokens obtained for WordPress API access");
|
|
78570
78725
|
return tokens;
|
|
@@ -78661,7 +78816,7 @@ function updateSessionId(sessionId) {
|
|
|
78661
78816
|
globalSessionId = sessionId;
|
|
78662
78817
|
logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
|
|
78663
78818
|
}
|
|
78664
|
-
async function getWordPressHeaders(requestData) {
|
|
78819
|
+
async function getWordPressHeaders(requestData, options = {}) {
|
|
78665
78820
|
validateEnvironment();
|
|
78666
78821
|
let authHeader = "";
|
|
78667
78822
|
if (CONFIG.JWT_TOKEN) {
|
|
@@ -78670,7 +78825,7 @@ async function getWordPressHeaders(requestData) {
|
|
|
78670
78825
|
logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, "AUTH");
|
|
78671
78826
|
} else if (CONFIG.OAUTH_ENABLED) {
|
|
78672
78827
|
logger.auth("OAuth is the primary authentication method - attempting to get tokens...");
|
|
78673
|
-
const oauthTokens = await getOAuthTokens();
|
|
78828
|
+
const oauthTokens = options.allowUnauthenticated && !CONFIG.WP_API_USERNAME ? await getValidTokens(generateServerUrlHash(getRequestUrl())) : await getOAuthTokens();
|
|
78674
78829
|
if (oauthTokens) {
|
|
78675
78830
|
authHeader = `Bearer ${oauthTokens.access_token}`;
|
|
78676
78831
|
logger.auth("Using OAuth token authentication for WordPress API");
|
|
@@ -78710,7 +78865,7 @@ async function getWordPressHeaders(requestData) {
|
|
|
78710
78865
|
if (name.toLowerCase() === "mcp-protocol-version") delete customHeaders[name];
|
|
78711
78866
|
}
|
|
78712
78867
|
const hasCustomHeaders = Object.keys(customHeaders).length > 0;
|
|
78713
|
-
if (!authHeader && !hasCustomHeaders) {
|
|
78868
|
+
if (!authHeader && !hasCustomHeaders && !(options.allowUnauthenticated && CONFIG.OAUTH_ENABLED)) {
|
|
78714
78869
|
throw new AuthError(
|
|
78715
78870
|
"No authentication method available. Please configure JWT_TOKEN, OAuth, Basic Auth (WP_API_USERNAME+WP_API_PASSWORD), or CUSTOM_HEADERS.",
|
|
78716
78871
|
"NO_AUTH_METHOD"
|