@automattic/mcp-wordpress-remote 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Docs/authentication.md +45 -0
- package/README.md +5 -5
- package/dist/lib.d.ts +18 -12
- package/dist/lib.js +558 -407
- package/dist/proxy.js +487 -461
- package/package.json +7 -3
package/dist/lib.js
CHANGED
|
@@ -75554,8 +75554,152 @@ var OAUTH_ERRORS = {
|
|
|
75554
75554
|
};
|
|
75555
75555
|
|
|
75556
75556
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js
|
|
75557
|
+
function isClientAuthMethod(method) {
|
|
75558
|
+
return ["client_secret_basic", "client_secret_post", "none"].includes(method);
|
|
75559
|
+
}
|
|
75560
|
+
function selectClientAuthMethod(clientInformation, supportedMethods) {
|
|
75561
|
+
const hasClientSecret = clientInformation.client_secret !== void 0;
|
|
75562
|
+
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))) {
|
|
75563
|
+
return clientInformation.token_endpoint_auth_method;
|
|
75564
|
+
}
|
|
75565
|
+
if (supportedMethods.length === 0) {
|
|
75566
|
+
return hasClientSecret ? "client_secret_basic" : "none";
|
|
75567
|
+
}
|
|
75568
|
+
if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
|
|
75569
|
+
return "client_secret_basic";
|
|
75570
|
+
}
|
|
75571
|
+
if (hasClientSecret && supportedMethods.includes("client_secret_post")) {
|
|
75572
|
+
return "client_secret_post";
|
|
75573
|
+
}
|
|
75574
|
+
if (supportedMethods.includes("none")) {
|
|
75575
|
+
return "none";
|
|
75576
|
+
}
|
|
75577
|
+
return hasClientSecret ? "client_secret_post" : "none";
|
|
75578
|
+
}
|
|
75579
|
+
function applyClientAuthentication(method, clientInformation, headers, params) {
|
|
75580
|
+
const { client_id, client_secret } = clientInformation;
|
|
75581
|
+
switch (method) {
|
|
75582
|
+
case "client_secret_basic":
|
|
75583
|
+
applyBasicAuth(client_id, client_secret, headers);
|
|
75584
|
+
return;
|
|
75585
|
+
case "client_secret_post":
|
|
75586
|
+
applyPostAuth(client_id, client_secret, params);
|
|
75587
|
+
return;
|
|
75588
|
+
case "none":
|
|
75589
|
+
applyPublicAuth(client_id, params);
|
|
75590
|
+
return;
|
|
75591
|
+
default:
|
|
75592
|
+
throw new Error(`Unsupported client authentication method: ${method}`);
|
|
75593
|
+
}
|
|
75594
|
+
}
|
|
75595
|
+
function applyBasicAuth(clientId, clientSecret, headers) {
|
|
75596
|
+
if (!clientSecret) {
|
|
75597
|
+
throw new Error("client_secret_basic authentication requires a client_secret");
|
|
75598
|
+
}
|
|
75599
|
+
const credentials = btoa(`${clientId}:${clientSecret}`);
|
|
75600
|
+
headers.set("Authorization", `Basic ${credentials}`);
|
|
75601
|
+
}
|
|
75602
|
+
function applyPostAuth(clientId, clientSecret, params) {
|
|
75603
|
+
params.set("client_id", clientId);
|
|
75604
|
+
if (clientSecret) {
|
|
75605
|
+
params.set("client_secret", clientSecret);
|
|
75606
|
+
}
|
|
75607
|
+
}
|
|
75608
|
+
function applyPublicAuth(clientId, params) {
|
|
75609
|
+
params.set("client_id", clientId);
|
|
75610
|
+
}
|
|
75611
|
+
async function parseErrorResponse(input) {
|
|
75612
|
+
const statusCode = input instanceof Response ? input.status : void 0;
|
|
75613
|
+
const body = input instanceof Response ? await input.text() : input;
|
|
75614
|
+
try {
|
|
75615
|
+
const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
|
|
75616
|
+
const { error: error51, error_description, error_uri } = result;
|
|
75617
|
+
const errorClass = OAUTH_ERRORS[error51] || ServerError;
|
|
75618
|
+
return new errorClass(error_description || "", error_uri);
|
|
75619
|
+
} catch (error51) {
|
|
75620
|
+
const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ""}Invalid OAuth error response: ${error51}. Raw body: ${body}`;
|
|
75621
|
+
return new ServerError(errorMessage);
|
|
75622
|
+
}
|
|
75623
|
+
}
|
|
75557
75624
|
var TokenResponseSchema = OAuthTokensSchema.omit({ issuer: true });
|
|
75558
75625
|
var RegistrationResponseSchema = OAuthClientInformationFullSchema.omit({ issuer: true });
|
|
75626
|
+
function buildDiscoveryUrls(authorizationServerUrl) {
|
|
75627
|
+
const url2 = typeof authorizationServerUrl === "string" ? new URL(authorizationServerUrl) : authorizationServerUrl;
|
|
75628
|
+
const hasPath = url2.pathname !== "/";
|
|
75629
|
+
const urlsToTry = [];
|
|
75630
|
+
if (!hasPath) {
|
|
75631
|
+
urlsToTry.push({
|
|
75632
|
+
url: new URL("/.well-known/oauth-authorization-server", url2.origin),
|
|
75633
|
+
type: "oauth"
|
|
75634
|
+
});
|
|
75635
|
+
urlsToTry.push({
|
|
75636
|
+
url: new URL(`/.well-known/openid-configuration`, url2.origin),
|
|
75637
|
+
type: "oidc"
|
|
75638
|
+
});
|
|
75639
|
+
return urlsToTry;
|
|
75640
|
+
}
|
|
75641
|
+
let pathname = url2.pathname;
|
|
75642
|
+
if (pathname.endsWith("/")) {
|
|
75643
|
+
pathname = pathname.slice(0, -1);
|
|
75644
|
+
}
|
|
75645
|
+
urlsToTry.push({
|
|
75646
|
+
url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url2.origin),
|
|
75647
|
+
type: "oauth"
|
|
75648
|
+
});
|
|
75649
|
+
urlsToTry.push({
|
|
75650
|
+
url: new URL(`/.well-known/openid-configuration${pathname}`, url2.origin),
|
|
75651
|
+
type: "oidc"
|
|
75652
|
+
});
|
|
75653
|
+
urlsToTry.push({
|
|
75654
|
+
url: new URL(`${pathname}/.well-known/openid-configuration`, url2.origin),
|
|
75655
|
+
type: "oidc"
|
|
75656
|
+
});
|
|
75657
|
+
return urlsToTry;
|
|
75658
|
+
}
|
|
75659
|
+
function resourceIndicatorToString(resource) {
|
|
75660
|
+
return typeof resource === "string" ? resource : resource.href;
|
|
75661
|
+
}
|
|
75662
|
+
async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication, resource, fetchFn }) {
|
|
75663
|
+
const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
|
|
75664
|
+
const headers = new Headers({
|
|
75665
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
75666
|
+
Accept: "application/json"
|
|
75667
|
+
});
|
|
75668
|
+
if (resource) {
|
|
75669
|
+
tokenRequestParams.set("resource", resourceIndicatorToString(resource));
|
|
75670
|
+
}
|
|
75671
|
+
if (addClientAuthentication) {
|
|
75672
|
+
await addClientAuthentication(headers, tokenRequestParams, tokenUrl, metadata);
|
|
75673
|
+
} else if (clientInformation) {
|
|
75674
|
+
const supportedMethods = metadata?.token_endpoint_auth_methods_supported ?? [];
|
|
75675
|
+
const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
|
|
75676
|
+
applyClientAuthentication(authMethod, clientInformation, headers, tokenRequestParams);
|
|
75677
|
+
}
|
|
75678
|
+
const response = await (fetchFn ?? fetch)(tokenUrl, {
|
|
75679
|
+
method: "POST",
|
|
75680
|
+
headers,
|
|
75681
|
+
body: tokenRequestParams
|
|
75682
|
+
});
|
|
75683
|
+
if (!response.ok) {
|
|
75684
|
+
throw await parseErrorResponse(response);
|
|
75685
|
+
}
|
|
75686
|
+
return TokenResponseSchema.parse(await response.json());
|
|
75687
|
+
}
|
|
75688
|
+
async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
|
|
75689
|
+
const tokenRequestParams = new URLSearchParams({
|
|
75690
|
+
grant_type: "refresh_token",
|
|
75691
|
+
refresh_token: refreshToken
|
|
75692
|
+
});
|
|
75693
|
+
const tokens = await executeTokenRequest(authorizationServerUrl, {
|
|
75694
|
+
metadata,
|
|
75695
|
+
tokenRequestParams,
|
|
75696
|
+
clientInformation,
|
|
75697
|
+
addClientAuthentication,
|
|
75698
|
+
resource,
|
|
75699
|
+
fetchFn
|
|
75700
|
+
});
|
|
75701
|
+
return { refresh_token: refreshToken, ...tokens };
|
|
75702
|
+
}
|
|
75559
75703
|
|
|
75560
75704
|
// src/lib/utils.ts
|
|
75561
75705
|
import * as fs2 from "node:fs";
|
|
@@ -75668,7 +75812,19 @@ async function writeTokens(serverUrlHash, tokens) {
|
|
|
75668
75812
|
...tokens,
|
|
75669
75813
|
obtained_at: Date.now()
|
|
75670
75814
|
};
|
|
75671
|
-
await
|
|
75815
|
+
await ensureConfigDir();
|
|
75816
|
+
const file2 = getConfigFilePath(serverUrlHash, "tokens.json");
|
|
75817
|
+
const temporary = `${file2}.${crypto2.randomUUID()}.tmp`;
|
|
75818
|
+
try {
|
|
75819
|
+
await fs.writeFile(temporary, JSON.stringify(tokensWithTimestamp, null, 2), {
|
|
75820
|
+
encoding: "utf-8",
|
|
75821
|
+
mode: 384,
|
|
75822
|
+
flag: "wx"
|
|
75823
|
+
});
|
|
75824
|
+
await fs.rename(temporary, file2);
|
|
75825
|
+
} finally {
|
|
75826
|
+
await fs.rm(temporary, { force: true });
|
|
75827
|
+
}
|
|
75672
75828
|
logger.info(`Stored tokens for server hash: ${serverUrlHash}`, "AUTH");
|
|
75673
75829
|
} catch (error51) {
|
|
75674
75830
|
logger.error(`Error writing tokens for ${serverUrlHash}`, "AUTH", error51);
|
|
@@ -75701,7 +75857,7 @@ function isTokenValid(tokens) {
|
|
|
75701
75857
|
if (!tokens?.access_token) {
|
|
75702
75858
|
return { isValid: false, error: "No access token" };
|
|
75703
75859
|
}
|
|
75704
|
-
if (
|
|
75860
|
+
if (tokens.expires_in === void 0 || tokens.obtained_at === void 0) {
|
|
75705
75861
|
return { isValid: true };
|
|
75706
75862
|
}
|
|
75707
75863
|
const now = Date.now();
|
|
@@ -75740,12 +75896,12 @@ var OAuthClientInfoSchema = external_exports.object({
|
|
|
75740
75896
|
client_id: external_exports.string().optional(),
|
|
75741
75897
|
client_secret: external_exports.string().optional()
|
|
75742
75898
|
});
|
|
75743
|
-
async function findAvailablePort(preferredPort) {
|
|
75899
|
+
async function findAvailablePort(preferredPort, host = "127.0.0.1") {
|
|
75744
75900
|
return new Promise((resolve, reject) => {
|
|
75745
75901
|
const server = net.createServer();
|
|
75746
75902
|
server.on("error", (err) => {
|
|
75747
75903
|
if (err.code === "EADDRINUSE") {
|
|
75748
|
-
server.listen(0);
|
|
75904
|
+
server.listen(0, host);
|
|
75749
75905
|
} else {
|
|
75750
75906
|
reject(err);
|
|
75751
75907
|
}
|
|
@@ -75756,7 +75912,7 @@ async function findAvailablePort(preferredPort) {
|
|
|
75756
75912
|
resolve(port);
|
|
75757
75913
|
});
|
|
75758
75914
|
});
|
|
75759
|
-
server.listen(preferredPort || 0);
|
|
75915
|
+
server.listen(preferredPort || 0, host);
|
|
75760
75916
|
});
|
|
75761
75917
|
}
|
|
75762
75918
|
function calculateDefaultPort(serverUrlHash) {
|
|
@@ -75784,24 +75940,17 @@ async function findExistingClientPort(serverUrlHash) {
|
|
|
75784
75940
|
function getServerUrlHash(serverUrl) {
|
|
75785
75941
|
return crypto3.createHash("md5").update(serverUrl).digest("hex");
|
|
75786
75942
|
}
|
|
75787
|
-
async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
|
|
75943
|
+
async function selectCallbackPort(serverUrl, specifiedPort, _unused, host = "127.0.0.1") {
|
|
75788
75944
|
const serverUrlHash = getServerUrlHash(serverUrl);
|
|
75789
75945
|
if (specifiedPort) {
|
|
75790
75946
|
return specifiedPort;
|
|
75791
75947
|
}
|
|
75792
|
-
const
|
|
75793
|
-
|
|
75794
|
-
findExistingClientPort(serverUrlHash),
|
|
75795
|
-
findAvailablePort(defaultPort)
|
|
75796
|
-
]);
|
|
75797
|
-
if (existingClientPort) {
|
|
75798
|
-
return existingClientPort;
|
|
75799
|
-
}
|
|
75800
|
-
return availablePort;
|
|
75948
|
+
const existingClientPort = await findExistingClientPort(serverUrlHash);
|
|
75949
|
+
return findAvailablePort(existingClientPort ?? calculateDefaultPort(serverUrlHash), host);
|
|
75801
75950
|
}
|
|
75802
75951
|
|
|
75803
75952
|
// src/lib/config.ts
|
|
75804
|
-
var MCP_WORDPRESS_REMOTE_VERSION = "0.5.
|
|
75953
|
+
var MCP_WORDPRESS_REMOTE_VERSION = "0.5.3";
|
|
75805
75954
|
function parsePositiveIntEnv(value, defaultValue) {
|
|
75806
75955
|
if (!value) {
|
|
75807
75956
|
return defaultValue;
|
|
@@ -76002,8 +76151,8 @@ async function getOAuthCallbackPort() {
|
|
|
76002
76151
|
return selectCallbackPort(
|
|
76003
76152
|
CONFIG.WP_API_URL,
|
|
76004
76153
|
CONFIG.OAUTH_CALLBACK_PORT,
|
|
76005
|
-
false
|
|
76006
|
-
|
|
76154
|
+
false,
|
|
76155
|
+
CONFIG.OAUTH_HOST
|
|
76007
76156
|
);
|
|
76008
76157
|
}
|
|
76009
76158
|
function formatConfigError(error51) {
|
|
@@ -76923,10 +77072,48 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
|
76923
77072
|
var open_default = open;
|
|
76924
77073
|
|
|
76925
77074
|
// src/lib/mcp-oauth-provider.ts
|
|
77075
|
+
import lockfile from "proper-lockfile";
|
|
77076
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
76926
77077
|
import { EventEmitter } from "node:events";
|
|
76927
77078
|
|
|
76928
77079
|
// src/lib/mcp-oauth-utils.ts
|
|
76929
77080
|
import crypto4 from "crypto";
|
|
77081
|
+
|
|
77082
|
+
// src/lib/oauth-http.ts
|
|
77083
|
+
function validateOAuthUrl(value) {
|
|
77084
|
+
let url2;
|
|
77085
|
+
try {
|
|
77086
|
+
url2 = new URL(value);
|
|
77087
|
+
} catch {
|
|
77088
|
+
throw new OAuthError2("Invalid OAuth URL", "INVALID_OAUTH_URL");
|
|
77089
|
+
}
|
|
77090
|
+
const loopback = url2.hostname === "localhost" || url2.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url2.hostname);
|
|
77091
|
+
if (url2.protocol !== "https:" && !(url2.protocol === "http:" && loopback) || url2.username || url2.password || url2.hash) {
|
|
77092
|
+
throw new OAuthError2(
|
|
77093
|
+
"OAuth URLs require HTTPS (HTTP is allowed only on loopback)",
|
|
77094
|
+
"INSECURE_OAUTH_URL"
|
|
77095
|
+
);
|
|
77096
|
+
}
|
|
77097
|
+
return url2;
|
|
77098
|
+
}
|
|
77099
|
+
async function oauthFetch(value, init = {}) {
|
|
77100
|
+
const url2 = validateOAuthUrl(value);
|
|
77101
|
+
const headers = new Headers(
|
|
77102
|
+
url2.origin === new URL(CONFIG.WP_API_URL).origin ? getCustomHeaders() : {}
|
|
77103
|
+
);
|
|
77104
|
+
new Headers(init.headers).forEach((value2, key) => headers.set(key, value2));
|
|
77105
|
+
const response = await proxyFetch(url2.toString(), {
|
|
77106
|
+
...init,
|
|
77107
|
+
headers,
|
|
77108
|
+
redirect: "error",
|
|
77109
|
+
signal: init.signal ?? AbortSignal.timeout(CONFIG.OAUTH_TIMEOUT)
|
|
77110
|
+
});
|
|
77111
|
+
if (response.status >= 300 && response.status < 400)
|
|
77112
|
+
throw new OAuthError2("OAuth endpoint redirects are not accepted", "OAUTH_REDIRECT");
|
|
77113
|
+
return response;
|
|
77114
|
+
}
|
|
77115
|
+
|
|
77116
|
+
// src/lib/mcp-oauth-utils.ts
|
|
76930
77117
|
function generatePKCE() {
|
|
76931
77118
|
const codeVerifier = crypto4.randomBytes(64).toString("base64url").substring(0, 128);
|
|
76932
77119
|
const codeChallenge = crypto4.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
@@ -76954,118 +77141,76 @@ function generateCanonicalResourceURI(serverUrl) {
|
|
|
76954
77141
|
}
|
|
76955
77142
|
}
|
|
76956
77143
|
async function discoverAuthorizationServerMetadata(authorizationServerUrl) {
|
|
76957
|
-
|
|
76958
|
-
|
|
76959
|
-
|
|
76960
|
-
const
|
|
76961
|
-
|
|
76962
|
-
|
|
76963
|
-
Accept: "application/json",
|
|
76964
|
-
...customHeaders
|
|
76965
|
-
}
|
|
76966
|
-
});
|
|
76967
|
-
if (!response.ok) {
|
|
77144
|
+
const issuer = validateOAuthUrl(authorizationServerUrl);
|
|
77145
|
+
if (issuer.search) throw new OAuthError2("Issuer must not contain a query", "INVALID_METADATA");
|
|
77146
|
+
for (const { url: url2 } of buildDiscoveryUrls(issuer)) {
|
|
77147
|
+
const response = await oauthFetch(url2.toString(), { headers: { Accept: "application/json" } });
|
|
77148
|
+
if ([404, 405].includes(response.status)) continue;
|
|
77149
|
+
if (!response.ok)
|
|
76968
77150
|
throw new OAuthError2(
|
|
76969
|
-
`
|
|
77151
|
+
`Metadata request failed (${response.status})`,
|
|
76970
77152
|
"METADATA_DISCOVERY_FAILED"
|
|
76971
77153
|
);
|
|
76972
|
-
|
|
76973
|
-
|
|
76974
|
-
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
77154
|
+
const parsed = OAuthMetadataSchema.safeParse(await response.json());
|
|
77155
|
+
if (!parsed.success || parsed.data.issuer !== authorizationServerUrl) {
|
|
76975
77156
|
throw new OAuthError2(
|
|
76976
|
-
"Authorization
|
|
77157
|
+
"Authorization metadata has an invalid or mismatched issuer",
|
|
76977
77158
|
"INVALID_METADATA"
|
|
76978
77159
|
);
|
|
76979
77160
|
}
|
|
76980
|
-
|
|
77161
|
+
const metadata = parsed.data;
|
|
77162
|
+
validateOAuthUrl(metadata.issuer);
|
|
77163
|
+
validateOAuthUrl(metadata.authorization_endpoint);
|
|
77164
|
+
validateOAuthUrl(metadata.token_endpoint);
|
|
77165
|
+
if (metadata.registration_endpoint) validateOAuthUrl(metadata.registration_endpoint);
|
|
76981
77166
|
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
77167
|
}
|
|
77168
|
+
throw new OAuthError2("No authorization server metadata found", "METADATA_DISCOVERY_FAILED");
|
|
76991
77169
|
}
|
|
76992
|
-
async function discoverProtectedResourceMetadata(resourceUrl) {
|
|
76993
|
-
|
|
76994
|
-
|
|
76995
|
-
|
|
76996
|
-
|
|
76997
|
-
|
|
76998
|
-
|
|
76999
|
-
|
|
77000
|
-
|
|
77001
|
-
|
|
77002
|
-
}
|
|
77003
|
-
|
|
77004
|
-
|
|
77170
|
+
async function discoverProtectedResourceMetadata(resourceUrl, resourceMetadataUrl) {
|
|
77171
|
+
const resource = validateOAuthUrl(resourceUrl);
|
|
77172
|
+
const candidates = resourceMetadataUrl ? [{ url: resourceMetadataUrl, expected: resourceUrl }] : [
|
|
77173
|
+
...resource.pathname !== "/" ? [
|
|
77174
|
+
{
|
|
77175
|
+
url: `${resource.origin}/.well-known/oauth-protected-resource${resource.pathname}`,
|
|
77176
|
+
expected: resourceUrl
|
|
77177
|
+
}
|
|
77178
|
+
] : [],
|
|
77179
|
+
{
|
|
77180
|
+
url: `${resource.origin}/.well-known/oauth-protected-resource`,
|
|
77181
|
+
expected: resource.pathname === "/" ? resourceUrl : resource.origin
|
|
77182
|
+
}
|
|
77183
|
+
];
|
|
77184
|
+
for (const candidate of candidates) {
|
|
77185
|
+
const response = await oauthFetch(candidate.url, { headers: { Accept: "application/json" } });
|
|
77186
|
+
if (!resourceMetadataUrl && [404, 405].includes(response.status)) continue;
|
|
77187
|
+
if (!response.ok)
|
|
77005
77188
|
throw new OAuthError2(
|
|
77006
|
-
`
|
|
77189
|
+
`Resource metadata request failed (${response.status})`,
|
|
77007
77190
|
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
77008
77191
|
);
|
|
77009
|
-
|
|
77010
|
-
|
|
77011
|
-
if (!metadata.authorization_servers || metadata.authorization_servers.length === 0) {
|
|
77192
|
+
const parsed = OAuthProtectedResourceMetadataSchema.safeParse(await response.json());
|
|
77193
|
+
if (!parsed.success || parsed.data.resource !== candidate.expected || !parsed.data.authorization_servers?.length) {
|
|
77012
77194
|
throw new OAuthError2(
|
|
77013
|
-
"Protected resource metadata
|
|
77195
|
+
"Protected resource metadata has an invalid or mismatched resource",
|
|
77014
77196
|
"INVALID_RESOURCE_METADATA"
|
|
77015
77197
|
);
|
|
77016
77198
|
}
|
|
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
|
-
);
|
|
77199
|
+
for (const issuer of parsed.data.authorization_servers) validateOAuthUrl(issuer);
|
|
77200
|
+
return parsed.data;
|
|
77027
77201
|
}
|
|
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;
|
|
77202
|
+
throw new OAuthError2(
|
|
77203
|
+
"No protected resource metadata found",
|
|
77204
|
+
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
77205
|
+
);
|
|
77059
77206
|
}
|
|
77060
77207
|
async function registerDynamicClient(registrationEndpoint, registrationRequest) {
|
|
77061
77208
|
try {
|
|
77062
77209
|
logger.oauth(`Attempting dynamic client registration: ${registrationEndpoint}`);
|
|
77063
|
-
const
|
|
77064
|
-
const response = await proxyFetch(registrationEndpoint, {
|
|
77210
|
+
const response = await oauthFetch(registrationEndpoint, {
|
|
77065
77211
|
method: "POST",
|
|
77066
77212
|
headers: {
|
|
77067
|
-
"Content-Type": "application/json"
|
|
77068
|
-
...customHeaders
|
|
77213
|
+
"Content-Type": "application/json"
|
|
77069
77214
|
},
|
|
77070
77215
|
body: JSON.stringify(registrationRequest)
|
|
77071
77216
|
});
|
|
@@ -77077,12 +77222,26 @@ async function registerDynamicClient(registrationEndpoint, registrationRequest)
|
|
|
77077
77222
|
);
|
|
77078
77223
|
}
|
|
77079
77224
|
const registrationResponse = await response.json();
|
|
77080
|
-
if (!registrationResponse.client_id) {
|
|
77225
|
+
if (typeof registrationResponse.client_id !== "string" || !registrationResponse.client_id) {
|
|
77081
77226
|
throw new OAuthError2(
|
|
77082
77227
|
"Dynamic client registration response missing client_id",
|
|
77083
77228
|
"INVALID_REGISTRATION_RESPONSE"
|
|
77084
77229
|
);
|
|
77085
77230
|
}
|
|
77231
|
+
if (registrationResponse.token_endpoint_auth_method && registrationResponse.token_endpoint_auth_method !== registrationRequest.token_endpoint_auth_method) {
|
|
77232
|
+
throw new OAuthError2(
|
|
77233
|
+
"Authorization server changed the requested client authentication method",
|
|
77234
|
+
"INVALID_REGISTRATION_RESPONSE"
|
|
77235
|
+
);
|
|
77236
|
+
}
|
|
77237
|
+
if (registrationResponse.redirect_uris && (!Array.isArray(registrationResponse.redirect_uris) || !registrationRequest.redirect_uris.every(
|
|
77238
|
+
(uri) => registrationResponse.redirect_uris.includes(uri)
|
|
77239
|
+
))) {
|
|
77240
|
+
throw new OAuthError2(
|
|
77241
|
+
"Authorization server changed the registered callback URI",
|
|
77242
|
+
"INVALID_REGISTRATION_RESPONSE"
|
|
77243
|
+
);
|
|
77244
|
+
}
|
|
77086
77245
|
logger.oauth("Dynamic client registration successful");
|
|
77087
77246
|
logger.debug("Client ID obtained", "OAUTH", {
|
|
77088
77247
|
client_id: registrationResponse.client_id,
|
|
@@ -77112,12 +77271,10 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
|
|
|
77112
77271
|
if (resource) {
|
|
77113
77272
|
params.set("resource", resource);
|
|
77114
77273
|
}
|
|
77115
|
-
const
|
|
77116
|
-
const response = await proxyFetch(tokenEndpoint, {
|
|
77274
|
+
const response = await oauthFetch(tokenEndpoint, {
|
|
77117
77275
|
method: "POST",
|
|
77118
77276
|
headers: {
|
|
77119
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
77120
|
-
...customHeaders
|
|
77277
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
77121
77278
|
},
|
|
77122
77279
|
body: params.toString()
|
|
77123
77280
|
});
|
|
@@ -77128,7 +77285,9 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
|
|
|
77128
77285
|
"TOKEN_EXCHANGE_FAILED"
|
|
77129
77286
|
);
|
|
77130
77287
|
}
|
|
77131
|
-
const tokenResponse = await response.json();
|
|
77288
|
+
const tokenResponse = OAuthTokensSchema.parse(await response.json());
|
|
77289
|
+
if (tokenResponse.token_type.toLowerCase() !== "bearer")
|
|
77290
|
+
throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
|
|
77132
77291
|
if (!tokenResponse.access_token) {
|
|
77133
77292
|
throw new OAuthError2("Token response missing access_token", "INVALID_TOKEN_RESPONSE");
|
|
77134
77293
|
}
|
|
@@ -77157,15 +77316,18 @@ function buildAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, sco
|
|
|
77157
77316
|
// OAuth 2.1 uses authorization code flow
|
|
77158
77317
|
client_id: clientId,
|
|
77159
77318
|
redirect_uri: redirectUri,
|
|
77160
|
-
scope: scopes.join(" "),
|
|
77161
77319
|
state,
|
|
77162
77320
|
code_challenge: codeChallenge,
|
|
77163
77321
|
code_challenge_method: "S256"
|
|
77164
77322
|
});
|
|
77323
|
+
if (scopes.length) params.set("scope", scopes.join(" "));
|
|
77165
77324
|
if (resource) {
|
|
77166
77325
|
params.set("resource", resource);
|
|
77167
77326
|
}
|
|
77168
|
-
|
|
77327
|
+
validateOAuthUrl(authorizationEndpoint);
|
|
77328
|
+
const url2 = new URL(authorizationEndpoint);
|
|
77329
|
+
params.forEach((value, key) => url2.searchParams.set(key, value));
|
|
77330
|
+
return url2.toString();
|
|
77169
77331
|
}
|
|
77170
77332
|
function generateSecureState() {
|
|
77171
77333
|
return crypto4.randomBytes(32).toString("base64url");
|
|
@@ -77246,110 +77408,26 @@ var AUTHORIZATION_CODE_HTML = `
|
|
|
77246
77408
|
</div>
|
|
77247
77409
|
|
|
77248
77410
|
<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.');
|
|
77411
|
+
const query = new URLSearchParams(window.location.search);
|
|
77412
|
+
const fragment = new URLSearchParams(window.location.hash.substring(1));
|
|
77413
|
+
const status = document.getElementById('status');
|
|
77414
|
+
const details = document.getElementById('details');
|
|
77415
|
+
const data = Object.fromEntries(query.has('code') || query.has('error') ? query : fragment);
|
|
77416
|
+
const endpoint = data.access_token ? '/oauth/tokens' : '/oauth/callback';
|
|
77417
|
+
if (data.code || data.error || data.access_token) {
|
|
77418
|
+
fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
|
|
77419
|
+
.then(async response => {
|
|
77420
|
+
const result = await response.json();
|
|
77421
|
+
if (!response.ok) throw new Error(result.error || 'Authorization response was rejected');
|
|
77422
|
+
status.textContent = data.access_token ? 'Authorization successful' : 'Authorization code received';
|
|
77423
|
+
status.className = 'success';
|
|
77424
|
+
details.textContent = data.access_token ? 'You can close this window.' : 'Return to your app to check the connection. Token exchange may still fail.';
|
|
77425
|
+
})
|
|
77426
|
+
.catch(error => { status.textContent = 'Authorization failed'; status.className = 'error'; details.textContent = error.message; })
|
|
77427
|
+
.finally(() => { document.getElementById('spinner').style.display = 'none'; });
|
|
77428
|
+
} else {
|
|
77429
|
+
status.textContent = 'No authorization response received';
|
|
77430
|
+
document.getElementById('spinner').style.display = 'none';
|
|
77353
77431
|
}
|
|
77354
77432
|
</script>
|
|
77355
77433
|
</body>
|
|
@@ -77364,6 +77442,17 @@ var OAuthCallbackServer = class {
|
|
|
77364
77442
|
this.setupRoutes();
|
|
77365
77443
|
}
|
|
77366
77444
|
setupRoutes() {
|
|
77445
|
+
this.app.use((req, res, next) => {
|
|
77446
|
+
const host = this.options.host === "::1" ? "[::1]" : this.options.host;
|
|
77447
|
+
const origin = req.headers.origin;
|
|
77448
|
+
const port = this.server?.address();
|
|
77449
|
+
const expectedOrigin = `http://${host}:${port && typeof port !== "string" ? port.port : this.options.port}`;
|
|
77450
|
+
if (req.hostname !== host || origin && origin !== expectedOrigin) {
|
|
77451
|
+
res.status(403).json({ error: "Untrusted callback origin" });
|
|
77452
|
+
return;
|
|
77453
|
+
}
|
|
77454
|
+
next();
|
|
77455
|
+
});
|
|
77367
77456
|
this.app.use(import_express.default.json());
|
|
77368
77457
|
this.app.get("/oauth/callback", (req, res) => {
|
|
77369
77458
|
logger.oauth("OAuth 2.1 authorization callback page requested");
|
|
@@ -77371,46 +77460,47 @@ var OAuthCallbackServer = class {
|
|
|
77371
77460
|
});
|
|
77372
77461
|
this.app.post("/oauth/callback", async (req, res) => {
|
|
77373
77462
|
try {
|
|
77374
|
-
const { code, state, iss } = req.body;
|
|
77375
|
-
|
|
77376
|
-
|
|
77377
|
-
|
|
77378
|
-
|
|
77379
|
-
|
|
77380
|
-
|
|
77381
|
-
state: state || "none",
|
|
77382
|
-
issuer: iss || "none"
|
|
77383
|
-
});
|
|
77463
|
+
const { code, state, iss, error: error51 } = req.body;
|
|
77464
|
+
this.validateResponse(state, iss);
|
|
77465
|
+
if (error51) throw new OAuthError2("Authorization denied or failed", "AUTH_DENIED");
|
|
77466
|
+
if (this.options.flow === "implicit")
|
|
77467
|
+
throw new OAuthError2("Authorization-code callback is disabled", "WRONG_FLOW");
|
|
77468
|
+
if (typeof code !== "string" || !code)
|
|
77469
|
+
throw new OAuthError2("No authorization code received", "INVALID_CODE");
|
|
77384
77470
|
this.events.emit("oauth-code-received", { code, state, iss });
|
|
77385
|
-
res.json({
|
|
77471
|
+
res.json({
|
|
77472
|
+
success: true,
|
|
77473
|
+
message: "Authorization code received; check connection status in the app"
|
|
77474
|
+
});
|
|
77386
77475
|
} catch (error51) {
|
|
77387
|
-
|
|
77388
|
-
|
|
77389
|
-
|
|
77390
|
-
res.status(400).json({ error: errorMessage });
|
|
77476
|
+
const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid authorization response");
|
|
77477
|
+
if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
|
|
77478
|
+
res.status(400).json({ error: failure.message });
|
|
77391
77479
|
}
|
|
77392
77480
|
});
|
|
77393
77481
|
this.app.post("/oauth/tokens", async (req, res) => {
|
|
77482
|
+
if (this.options.flow !== "implicit") {
|
|
77483
|
+
res.status(404).json({ error: "Implicit token callback is disabled" });
|
|
77484
|
+
return;
|
|
77485
|
+
}
|
|
77394
77486
|
try {
|
|
77395
|
-
|
|
77396
|
-
if (!
|
|
77397
|
-
throw new OAuthError2("
|
|
77398
|
-
|
|
77399
|
-
|
|
77400
|
-
|
|
77401
|
-
|
|
77402
|
-
|
|
77403
|
-
|
|
77404
|
-
|
|
77405
|
-
});
|
|
77487
|
+
this.validateResponse(req.body.state, req.body.iss);
|
|
77488
|
+
if (typeof req.body.access_token !== "string" || !req.body.access_token || req.body.token_type && req.body.token_type.toLowerCase() !== "bearer")
|
|
77489
|
+
throw new OAuthError2("Invalid implicit token response");
|
|
77490
|
+
const tokens = {
|
|
77491
|
+
access_token: req.body.access_token,
|
|
77492
|
+
token_type: "Bearer",
|
|
77493
|
+
expires_in: req.body.expires_in === void 0 ? void 0 : Number(req.body.expires_in),
|
|
77494
|
+
scope: req.body.scope,
|
|
77495
|
+
obtained_at: Date.now()
|
|
77496
|
+
};
|
|
77406
77497
|
await writeTokens(this.options.serverUrlHash, tokens);
|
|
77407
77498
|
this.events.emit("oauth-success", tokens);
|
|
77408
|
-
res.json({ success: true
|
|
77499
|
+
res.json({ success: true });
|
|
77409
77500
|
} catch (error51) {
|
|
77410
|
-
|
|
77411
|
-
|
|
77412
|
-
|
|
77413
|
-
res.status(400).json({ error: errorMessage });
|
|
77501
|
+
const failure = error51 instanceof OAuthError2 ? error51 : new OAuthError2("Invalid implicit response");
|
|
77502
|
+
if (failure.code !== "INVALID_STATE") this.events.emit("oauth-error", failure);
|
|
77503
|
+
res.status(400).json({ error: failure.message });
|
|
77414
77504
|
}
|
|
77415
77505
|
});
|
|
77416
77506
|
this.app.get("/oauth/health", (req, res) => {
|
|
@@ -77427,10 +77517,17 @@ var OAuthCallbackServer = class {
|
|
|
77427
77517
|
}
|
|
77428
77518
|
);
|
|
77429
77519
|
}
|
|
77520
|
+
validateResponse(state, issuer) {
|
|
77521
|
+
if (!this.options.state || state !== this.options.state)
|
|
77522
|
+
throw new OAuthError2("Invalid callback state", "INVALID_STATE");
|
|
77523
|
+
if ((issuer !== void 0 || this.options.requireIssuer) && issuer !== this.options.issuer)
|
|
77524
|
+
throw new OAuthError2("Invalid callback issuer", "INVALID_ISSUER");
|
|
77525
|
+
}
|
|
77430
77526
|
async start() {
|
|
77431
77527
|
return new Promise((resolve, reject) => {
|
|
77432
77528
|
try {
|
|
77433
77529
|
this.server = this.app.listen(this.options.port, this.options.host, (error51) => {
|
|
77530
|
+
clearTimeout(this.startupTimer);
|
|
77434
77531
|
if (error51) {
|
|
77435
77532
|
reject(error51);
|
|
77436
77533
|
return;
|
|
@@ -77441,11 +77538,12 @@ var OAuthCallbackServer = class {
|
|
|
77441
77538
|
resolve();
|
|
77442
77539
|
});
|
|
77443
77540
|
this.server.on("error", (error51) => {
|
|
77541
|
+
clearTimeout(this.startupTimer);
|
|
77444
77542
|
logger.error("OAuth callback server error", "OAUTH", error51);
|
|
77445
77543
|
reject(error51);
|
|
77446
77544
|
});
|
|
77447
77545
|
if (this.options.timeout) {
|
|
77448
|
-
setTimeout(() => {
|
|
77546
|
+
this.startupTimer = setTimeout(() => {
|
|
77449
77547
|
if (!this.server?.listening) {
|
|
77450
77548
|
reject(new OAuthError2("OAuth callback server startup timeout", "TIMEOUT"));
|
|
77451
77549
|
}
|
|
@@ -77457,6 +77555,7 @@ var OAuthCallbackServer = class {
|
|
|
77457
77555
|
});
|
|
77458
77556
|
}
|
|
77459
77557
|
async stop() {
|
|
77558
|
+
clearTimeout(this.startupTimer);
|
|
77460
77559
|
if (!this.server) return;
|
|
77461
77560
|
const server = this.server;
|
|
77462
77561
|
this.server = null;
|
|
@@ -77488,7 +77587,9 @@ var MCPOAuthProvider = class {
|
|
|
77488
77587
|
this.authServerMetadata = null;
|
|
77489
77588
|
this.resourceMetadata = null;
|
|
77490
77589
|
const serverUrl = options.serverUrl || CONFIG.WP_API_URL;
|
|
77491
|
-
const defaultScopes = getDefaultOAuthScopes();
|
|
77590
|
+
const defaultScopes = CONFIG.OAUTH_SCOPES ? getDefaultOAuthScopes() : [];
|
|
77591
|
+
this.explicitScopes = options.scopes ?? defaultScopes;
|
|
77592
|
+
this.configuredClientId = options.clientId || CONFIG.WP_OAUTH_CLIENT_ID || void 0;
|
|
77492
77593
|
this.config = {
|
|
77493
77594
|
serverUrl,
|
|
77494
77595
|
resource: generateCanonicalResourceURI(serverUrl),
|
|
@@ -77547,187 +77648,217 @@ var MCPOAuthProvider = class {
|
|
|
77547
77648
|
/**
|
|
77548
77649
|
* Discover OAuth 2.1 server endpoints using MCP-compliant discovery
|
|
77549
77650
|
*/
|
|
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
|
-
}
|
|
77651
|
+
async discoverOAuthEndpoints(challenge) {
|
|
77652
|
+
this.resourceMetadata = await discoverProtectedResourceMetadata(
|
|
77653
|
+
this.config.serverUrl,
|
|
77654
|
+
challenge?.resource_metadata
|
|
77655
|
+
);
|
|
77656
|
+
this.authServerMetadata = await discoverAuthorizationServerMetadata(
|
|
77657
|
+
this.resourceMetadata.authorization_servers[0]
|
|
77658
|
+
);
|
|
77659
|
+
this.config.resource = this.resourceMetadata.resource;
|
|
77660
|
+
this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
|
|
77661
|
+
this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
|
|
77662
|
+
this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
|
|
77663
|
+
this.config.clientId = this.configuredClientId;
|
|
77664
|
+
const challenged = challenge?.scope?.split(/\s+/).filter(Boolean);
|
|
77665
|
+
this.config.scopes = [
|
|
77666
|
+
...new Set(
|
|
77667
|
+
challenged ?? (this.explicitScopes.length ? this.explicitScopes : this.resourceMetadata.scopes_supported ?? [])
|
|
77668
|
+
)
|
|
77669
|
+
];
|
|
77613
77670
|
}
|
|
77614
|
-
/**
|
|
77615
|
-
* Get authorization server URL from discovered metadata
|
|
77616
|
-
*/
|
|
77617
77671
|
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
|
-
}
|
|
77672
|
+
return this.authServerMetadata?.issuer ?? this.resourceMetadata?.authorization_servers[0] ?? new URL(this.config.serverUrl).origin;
|
|
77627
77673
|
}
|
|
77628
77674
|
/**
|
|
77629
77675
|
* Perform dynamic client registration if needed and supported
|
|
77630
77676
|
*/
|
|
77631
77677
|
async ensureClientRegistration() {
|
|
77678
|
+
const issuer = this.getAuthorizationServerUrl();
|
|
77679
|
+
const stored = await readClientInfo(this.serverUrlHash);
|
|
77632
77680
|
if (this.config.clientId) {
|
|
77633
|
-
|
|
77681
|
+
if (stored?.issuer && stored.issuer !== issuer && stored.client_id === this.config.clientId) {
|
|
77682
|
+
throw new OAuthError2(
|
|
77683
|
+
"Configured client belongs to another authorization server; update its registration",
|
|
77684
|
+
"CLIENT_ISSUER_MISMATCH"
|
|
77685
|
+
);
|
|
77686
|
+
}
|
|
77687
|
+
await writeClientInfo(this.serverUrlHash, { client_id: this.config.clientId, issuer });
|
|
77634
77688
|
return;
|
|
77635
77689
|
}
|
|
77636
|
-
|
|
77637
|
-
|
|
77638
|
-
this.config.clientId = storedClientInfo.client_id;
|
|
77639
|
-
logger.oauth("Reusing persisted client registration, skipping dynamic registration");
|
|
77690
|
+
if (stored?.issuer === issuer && typeof stored.client_id === "string" && stored.client_id.length > 0) {
|
|
77691
|
+
this.config.clientId = stored.client_id;
|
|
77640
77692
|
return;
|
|
77641
77693
|
}
|
|
77642
|
-
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
|
|
77694
|
+
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
|
|
77643
77695
|
throw new OAuthError2(
|
|
77644
|
-
"No client
|
|
77696
|
+
"No issuer-bound client registration; configure a client ID or enable registration",
|
|
77645
77697
|
"NO_CLIENT_ID"
|
|
77646
77698
|
);
|
|
77647
|
-
|
|
77648
|
-
if (!this.authServerMetadata?.registration_endpoint) {
|
|
77699
|
+
if (!this.authServerMetadata?.registration_endpoint)
|
|
77649
77700
|
throw new OAuthError2(
|
|
77650
|
-
"
|
|
77701
|
+
"Authorization server does not support dynamic registration; configure its client ID",
|
|
77651
77702
|
"NO_DYNAMIC_REGISTRATION"
|
|
77652
77703
|
);
|
|
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
|
-
}
|
|
77704
|
+
const request = {
|
|
77705
|
+
redirect_uris: [this.config.redirectUri],
|
|
77706
|
+
application_type: "native",
|
|
77707
|
+
token_endpoint_auth_method: "none",
|
|
77708
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
77709
|
+
response_types: ["code"],
|
|
77710
|
+
client_name: "WordPress MCP Remote",
|
|
77711
|
+
...this.config.scopes.length ? { scope: this.config.scopes.join(" ") } : {}
|
|
77712
|
+
};
|
|
77713
|
+
const registered = await registerDynamicClient(
|
|
77714
|
+
this.authServerMetadata.registration_endpoint,
|
|
77715
|
+
request
|
|
77716
|
+
);
|
|
77717
|
+
await writeClientInfo(this.serverUrlHash, {
|
|
77718
|
+
...request,
|
|
77719
|
+
client_id: registered.client_id,
|
|
77720
|
+
issuer
|
|
77721
|
+
});
|
|
77722
|
+
this.config.clientId = registered.client_id;
|
|
77680
77723
|
}
|
|
77681
77724
|
/**
|
|
77682
77725
|
* Initiate MCP-compliant OAuth 2.1 authorization flow
|
|
77683
77726
|
*/
|
|
77684
|
-
|
|
77685
|
-
|
|
77686
|
-
|
|
77687
|
-
|
|
77688
|
-
|
|
77689
|
-
|
|
77690
|
-
const
|
|
77691
|
-
|
|
77692
|
-
|
|
77693
|
-
|
|
77727
|
+
satisfies(tokens, request) {
|
|
77728
|
+
return !!tokens && tokens.access_token !== request.rejectedToken && (!request.scopes?.length || request.scopes.every((scope) => tokens.scope?.split(" ").includes(scope)));
|
|
77729
|
+
}
|
|
77730
|
+
async authorize(request = {}) {
|
|
77731
|
+
if (this.authPromise) await this.authPromise;
|
|
77732
|
+
if (!this.authPromise) this.authPromise = this.authorizeWithLock(request);
|
|
77733
|
+
const pending = this.authPromise;
|
|
77734
|
+
try {
|
|
77735
|
+
await pending;
|
|
77736
|
+
} finally {
|
|
77737
|
+
if (this.authPromise === pending) this.authPromise = null;
|
|
77694
77738
|
}
|
|
77695
|
-
|
|
77696
|
-
|
|
77739
|
+
}
|
|
77740
|
+
async authorizeWithLock(request) {
|
|
77741
|
+
const valid = await this.tokens();
|
|
77742
|
+
if (this.satisfies(valid, request)) return valid;
|
|
77743
|
+
await ensureConfigDir();
|
|
77744
|
+
const file2 = getConfigFilePath(this.serverUrlHash, "oauth");
|
|
77745
|
+
let release;
|
|
77697
77746
|
try {
|
|
77698
|
-
await
|
|
77699
|
-
logger.oauth("OAuth 2.1 authorization completed successfully");
|
|
77747
|
+
release = await lockfile.lock(file2, { realpath: false });
|
|
77700
77748
|
} catch (error51) {
|
|
77701
|
-
|
|
77702
|
-
|
|
77749
|
+
if (error51.code !== "ELOCKED") throw error51;
|
|
77750
|
+
const deadline = Date.now() + this.config.timeout;
|
|
77751
|
+
while (Date.now() < deadline) {
|
|
77752
|
+
const tokens = await this.tokens();
|
|
77753
|
+
if (this.satisfies(tokens, request)) return tokens;
|
|
77754
|
+
if (!await lockfile.check(file2, { realpath: false })) {
|
|
77755
|
+
const completed = await this.tokens();
|
|
77756
|
+
if (this.satisfies(completed, request)) return completed;
|
|
77757
|
+
throw new OAuthError2("Authentication in another process did not complete", "AUTH_FAILED");
|
|
77758
|
+
}
|
|
77759
|
+
await delay(Math.min(100, Math.max(0, deadline - Date.now())));
|
|
77760
|
+
}
|
|
77761
|
+
throw new OAuthError2("Timeout waiting for authentication in another process", "LOCK_TIMEOUT");
|
|
77762
|
+
}
|
|
77763
|
+
try {
|
|
77764
|
+
const completed = await this.tokens();
|
|
77765
|
+
if (this.satisfies(completed, request)) return completed;
|
|
77766
|
+
await this.discoverOAuthEndpoints(request.challenge);
|
|
77767
|
+
const issuer = this.getAuthorizationServerUrl();
|
|
77768
|
+
const stored = await readTokens(this.serverUrlHash);
|
|
77769
|
+
const client = await readClientInfo(this.serverUrlHash);
|
|
77770
|
+
if (this.configuredClientId && client?.issuer && client.issuer !== issuer && client.client_id === this.configuredClientId) {
|
|
77771
|
+
throw new OAuthError2(
|
|
77772
|
+
"Configured client belongs to another authorization server",
|
|
77773
|
+
"CLIENT_ISSUER_MISMATCH"
|
|
77774
|
+
);
|
|
77775
|
+
}
|
|
77776
|
+
this.config.scopes = [
|
|
77777
|
+
.../* @__PURE__ */ new Set([
|
|
77778
|
+
...stored?.issuer === issuer ? stored.scope?.split(" ").filter(Boolean) ?? [] : [],
|
|
77779
|
+
...this.config.scopes,
|
|
77780
|
+
...request.scopes ?? []
|
|
77781
|
+
])
|
|
77782
|
+
];
|
|
77783
|
+
const needsScope = !!request.scopes?.some(
|
|
77784
|
+
(scope) => !stored?.scope?.split(" ").includes(scope)
|
|
77785
|
+
);
|
|
77786
|
+
const clientId = this.configuredClientId || client?.client_id;
|
|
77787
|
+
if (stored?.issuer === issuer && client?.issuer === issuer && stored.refresh_token && clientId && (!this.configuredClientId || this.configuredClientId === client.client_id) && !needsScope) {
|
|
77788
|
+
try {
|
|
77789
|
+
const renewed = await refreshAuthorization(issuer, {
|
|
77790
|
+
metadata: { ...this.authServerMetadata },
|
|
77791
|
+
clientInformation: { client_id: clientId },
|
|
77792
|
+
refreshToken: stored.refresh_token,
|
|
77793
|
+
resource: this.resourceIndicator(),
|
|
77794
|
+
fetchFn: (url2, init) => oauthFetch(String(url2), init)
|
|
77795
|
+
});
|
|
77796
|
+
if (renewed.token_type.toLowerCase() !== "bearer")
|
|
77797
|
+
throw new OAuthError2("Unsupported token type", "INVALID_TOKEN_RESPONSE");
|
|
77798
|
+
const tokens = {
|
|
77799
|
+
...renewed,
|
|
77800
|
+
issuer,
|
|
77801
|
+
scope: renewed.scope ?? stored.scope,
|
|
77802
|
+
obtained_at: Date.now()
|
|
77803
|
+
};
|
|
77804
|
+
await writeTokens(this.serverUrlHash, tokens);
|
|
77805
|
+
return tokens;
|
|
77806
|
+
} catch (error51) {
|
|
77807
|
+
if (!(error51 instanceof InvalidGrantError)) throw error51;
|
|
77808
|
+
await deleteConfigFile(this.serverUrlHash, "tokens.json");
|
|
77809
|
+
}
|
|
77810
|
+
}
|
|
77811
|
+
return await this.performAuthorization();
|
|
77703
77812
|
} finally {
|
|
77704
|
-
|
|
77813
|
+
await release();
|
|
77705
77814
|
}
|
|
77706
77815
|
}
|
|
77816
|
+
resourceIndicator() {
|
|
77817
|
+
if (!CONFIG.OAUTH_RESOURCE_INDICATOR)
|
|
77818
|
+
throw new OAuthError2(
|
|
77819
|
+
"MCP OAuth requires resource indicators; remove OAUTH_RESOURCE_INDICATOR=false",
|
|
77820
|
+
"RESOURCE_REQUIRED"
|
|
77821
|
+
);
|
|
77822
|
+
return this.config.resource;
|
|
77823
|
+
}
|
|
77707
77824
|
/**
|
|
77708
77825
|
* Perform the complete OAuth 2.1 authorization flow
|
|
77709
77826
|
*/
|
|
77710
77827
|
async performAuthorization() {
|
|
77711
77828
|
try {
|
|
77712
|
-
await this.discoverOAuthEndpoints();
|
|
77713
|
-
|
|
77829
|
+
if (!this.authServerMetadata) await this.discoverOAuthEndpoints();
|
|
77830
|
+
if (!this.authServerMetadata?.code_challenge_methods_supported?.includes("S256"))
|
|
77831
|
+
throw new OAuthError2(
|
|
77832
|
+
"Authorization server must advertise PKCE S256 support",
|
|
77833
|
+
"PKCE_REQUIRED"
|
|
77834
|
+
);
|
|
77835
|
+
this.resourceIndicator();
|
|
77836
|
+
if (!["127.0.0.1", "localhost", "::1"].includes(this.config.host))
|
|
77837
|
+
throw new OAuthError2(
|
|
77838
|
+
"Native OAuth callback must bind to loopback",
|
|
77839
|
+
"INVALID_CALLBACK_HOST"
|
|
77840
|
+
);
|
|
77841
|
+
this.currentPKCE = generatePKCE();
|
|
77842
|
+
this.currentState = generateSecureState();
|
|
77843
|
+
const callbackPort = this.config.callbackPort === 0 ? await selectCallbackPort(this.config.serverUrl, void 0, false, this.config.host) : this.config.callbackPort;
|
|
77714
77844
|
const callbackServer = setupWPOAuthCallbackServer(
|
|
77715
77845
|
{
|
|
77716
77846
|
port: callbackPort,
|
|
77717
77847
|
host: this.config.host,
|
|
77718
77848
|
serverUrlHash: this.serverUrlHash,
|
|
77719
|
-
timeout: this.config.timeout
|
|
77849
|
+
timeout: this.config.timeout,
|
|
77850
|
+
state: this.currentState,
|
|
77851
|
+
issuer: this.authServerMetadata.issuer,
|
|
77852
|
+
requireIssuer: this.authServerMetadata.authorization_response_iss_parameter_supported
|
|
77720
77853
|
},
|
|
77721
77854
|
this.events
|
|
77722
77855
|
);
|
|
77723
77856
|
await callbackServer.start();
|
|
77724
77857
|
logger.oauth("OAuth callback server started");
|
|
77725
77858
|
try {
|
|
77726
|
-
const actualRedirectUri = `http://${this.config.host}:${callbackPort}/oauth/callback`;
|
|
77859
|
+
const actualRedirectUri = `http://${this.config.host === "::1" ? "[::1]" : this.config.host}:${callbackPort}/oauth/callback`;
|
|
77727
77860
|
this.config.redirectUri = actualRedirectUri;
|
|
77728
77861
|
await this.ensureClientRegistration();
|
|
77729
|
-
this.currentPKCE = generatePKCE();
|
|
77730
|
-
this.currentState = generateSecureState();
|
|
77731
77862
|
await writeTextFile(this.serverUrlHash, "pkce_verifier.txt", this.currentPKCE.codeVerifier);
|
|
77732
77863
|
await writeTextFile(this.serverUrlHash, "oauth_state.txt", this.currentState);
|
|
77733
77864
|
const authUrl = buildAuthorizationUrl(
|
|
@@ -77737,10 +77868,13 @@ var MCPOAuthProvider = class {
|
|
|
77737
77868
|
this.config.scopes,
|
|
77738
77869
|
this.currentState,
|
|
77739
77870
|
this.currentPKCE.codeChallenge,
|
|
77740
|
-
|
|
77871
|
+
this.resourceIndicator()
|
|
77741
77872
|
);
|
|
77742
77873
|
logger.oauth("Built OAuth 2.1 authorization URL");
|
|
77743
77874
|
logger.debug("Authorization URL", "OAUTH", { url: authUrl });
|
|
77875
|
+
const authorizationCode = this.waitForAuthorizationCode();
|
|
77876
|
+
void authorizationCode.catch(() => {
|
|
77877
|
+
});
|
|
77744
77878
|
try {
|
|
77745
77879
|
await open_default(authUrl);
|
|
77746
77880
|
logger.oauth("Browser opened successfully");
|
|
@@ -77751,13 +77885,20 @@ var MCPOAuthProvider = class {
|
|
|
77751
77885
|
logger.info(`${authUrl}`);
|
|
77752
77886
|
logger.info("===============================\n");
|
|
77753
77887
|
}
|
|
77754
|
-
const authCode = await
|
|
77755
|
-
const
|
|
77888
|
+
const authCode = await authorizationCode;
|
|
77889
|
+
const response = await this.exchangeCodeForTokens(authCode, actualRedirectUri);
|
|
77890
|
+
const tokens = {
|
|
77891
|
+
...response,
|
|
77892
|
+
issuer: this.getAuthorizationServerUrl(),
|
|
77893
|
+
scope: response.scope ?? this.config.scopes.join(" ")
|
|
77894
|
+
};
|
|
77756
77895
|
await writeTokens(this.serverUrlHash, tokens);
|
|
77757
77896
|
logger.oauth("OAuth 2.1 tokens stored successfully");
|
|
77758
77897
|
return tokens;
|
|
77759
77898
|
} finally {
|
|
77760
77899
|
await callbackServer.stop();
|
|
77900
|
+
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
77901
|
+
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
77761
77902
|
}
|
|
77762
77903
|
} catch (error51) {
|
|
77763
77904
|
logger.error("Error during OAuth 2.1 authorization flow", "OAUTH", error51);
|
|
@@ -77778,12 +77919,16 @@ var MCPOAuthProvider = class {
|
|
|
77778
77919
|
this.events.removeAllListeners("oauth-error");
|
|
77779
77920
|
clearTimeout(timeout);
|
|
77780
77921
|
};
|
|
77781
|
-
this.events.once("oauth-code-received", async ({ code, state }) => {
|
|
77922
|
+
this.events.once("oauth-code-received", async ({ code, state, iss }) => {
|
|
77782
77923
|
cleanup();
|
|
77783
77924
|
if (state !== this.currentState) {
|
|
77784
77925
|
reject(new OAuthError2("State parameter mismatch", "INVALID_STATE"));
|
|
77785
77926
|
return;
|
|
77786
77927
|
}
|
|
77928
|
+
if ((iss !== void 0 || this.authServerMetadata?.authorization_response_iss_parameter_supported) && iss !== this.authServerMetadata?.issuer) {
|
|
77929
|
+
reject(new OAuthError2("Authorization response issuer mismatch", "INVALID_ISSUER"));
|
|
77930
|
+
return;
|
|
77931
|
+
}
|
|
77787
77932
|
logger.oauth("Valid authorization code received");
|
|
77788
77933
|
resolve(code);
|
|
77789
77934
|
});
|
|
@@ -77809,7 +77954,7 @@ var MCPOAuthProvider = class {
|
|
|
77809
77954
|
redirectUri,
|
|
77810
77955
|
this.config.clientId,
|
|
77811
77956
|
codeVerifier,
|
|
77812
|
-
|
|
77957
|
+
this.resourceIndicator()
|
|
77813
77958
|
);
|
|
77814
77959
|
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
77815
77960
|
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
@@ -77875,6 +78020,7 @@ import * as path5 from "node:path";
|
|
|
77875
78020
|
import * as os3 from "node:os";
|
|
77876
78021
|
|
|
77877
78022
|
// src/lib/persistent-oauth-client-provider.ts
|
|
78023
|
+
import { randomBytes } from "node:crypto";
|
|
77878
78024
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
77879
78025
|
var PersistentWPOAuthClientProvider = class {
|
|
77880
78026
|
constructor(options) {
|
|
@@ -78032,7 +78178,10 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78032
78178
|
*/
|
|
78033
78179
|
async performAuthorization() {
|
|
78034
78180
|
const callbackPort = this.options.callbackPort === 0 ? await getOAuthCallbackPort() : this.options.callbackPort;
|
|
78181
|
+
const state = this.generateState();
|
|
78035
78182
|
const callbackServerOptions = {
|
|
78183
|
+
flow: "implicit",
|
|
78184
|
+
state,
|
|
78036
78185
|
port: callbackPort,
|
|
78037
78186
|
host: this.options.host,
|
|
78038
78187
|
serverUrlHash: this.serverUrlHash,
|
|
@@ -78044,12 +78193,14 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78044
78193
|
logger.oauth("Starting callback server...");
|
|
78045
78194
|
await callbackServer.start();
|
|
78046
78195
|
logger.oauth("Callback server started successfully");
|
|
78047
|
-
const state = this.generateState();
|
|
78048
78196
|
logger.debug(`Generated state parameter: ${state}`, "OAUTH");
|
|
78049
78197
|
const authUrl = this.buildAuthorizationUrl(callbackServer.getCallbackUrl(), state);
|
|
78050
78198
|
logger.oauth(`Built authorization URL: ${authUrl}`);
|
|
78051
78199
|
logger.debug(`Callback URL: ${callbackServer.getCallbackUrl()}`, "OAUTH");
|
|
78052
78200
|
logger.oauth("Attempting to open browser...");
|
|
78201
|
+
const authorization = this.waitForAuthorizationResult();
|
|
78202
|
+
void authorization.catch(() => {
|
|
78203
|
+
});
|
|
78053
78204
|
try {
|
|
78054
78205
|
await open_default(authUrl);
|
|
78055
78206
|
logger.oauth("Browser opened successfully");
|
|
@@ -78061,7 +78212,7 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78061
78212
|
logger.info("===============================\n");
|
|
78062
78213
|
}
|
|
78063
78214
|
logger.oauth("Waiting for authorization result...");
|
|
78064
|
-
const tokens = await
|
|
78215
|
+
const tokens = await authorization;
|
|
78065
78216
|
logger.oauth("Authorization result received");
|
|
78066
78217
|
logger.oauth("Saving tokens to persistent storage...");
|
|
78067
78218
|
await this.saveTokens(tokens);
|
|
@@ -78114,7 +78265,7 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78114
78265
|
* Generate a secure state parameter
|
|
78115
78266
|
*/
|
|
78116
78267
|
generateState() {
|
|
78117
|
-
return
|
|
78268
|
+
return randomBytes(32).toString("base64url");
|
|
78118
78269
|
}
|
|
78119
78270
|
/**
|
|
78120
78271
|
* Wait for authorization result from callback server
|
|
@@ -78537,6 +78688,12 @@ function constructApiUrl(baseUrl, defaultEndpoint) {
|
|
|
78537
78688
|
return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
|
|
78538
78689
|
}
|
|
78539
78690
|
}
|
|
78691
|
+
function getMCPOAuthProvider() {
|
|
78692
|
+
return mcpOAuthProvider ??= new MCPOAuthProvider({
|
|
78693
|
+
serverUrl: getRequestUrl(),
|
|
78694
|
+
clientId: CONFIG.WP_OAUTH_CLIENT_ID
|
|
78695
|
+
});
|
|
78696
|
+
}
|
|
78540
78697
|
async function getOAuthTokens() {
|
|
78541
78698
|
try {
|
|
78542
78699
|
if (!CONFIG.OAUTH_ENABLED) {
|
|
@@ -78544,7 +78701,7 @@ async function getOAuthTokens() {
|
|
|
78544
78701
|
return null;
|
|
78545
78702
|
}
|
|
78546
78703
|
logger.auth("Attempting to get OAuth tokens for WordPress API (MCP-compliant)...");
|
|
78547
|
-
const serverUrl =
|
|
78704
|
+
const serverUrl = getRequestUrl();
|
|
78548
78705
|
const serverUrlHash = generateServerUrlHash(serverUrl);
|
|
78549
78706
|
const existingTokens = await getValidTokens(serverUrlHash);
|
|
78550
78707
|
if (existingTokens) {
|
|
@@ -78555,16 +78712,10 @@ async function getOAuthTokens() {
|
|
|
78555
78712
|
logger.auth("Starting MCP-compliant OAuth 2.1 authentication flow");
|
|
78556
78713
|
logger.auth("Your browser should open automatically for authentication");
|
|
78557
78714
|
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
78715
|
logger.auth("Using MCP-compliant OAuth 2.1 authorization code flow with PKCE");
|
|
78566
|
-
|
|
78567
|
-
|
|
78716
|
+
const provider = getMCPOAuthProvider();
|
|
78717
|
+
await provider.authorize();
|
|
78718
|
+
const tokens = await provider.tokens();
|
|
78568
78719
|
if (tokens) {
|
|
78569
78720
|
logger.auth("MCP OAuth 2.1 tokens obtained for WordPress API access");
|
|
78570
78721
|
return tokens;
|
|
@@ -78661,7 +78812,7 @@ function updateSessionId(sessionId) {
|
|
|
78661
78812
|
globalSessionId = sessionId;
|
|
78662
78813
|
logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
|
|
78663
78814
|
}
|
|
78664
|
-
async function getWordPressHeaders(requestData) {
|
|
78815
|
+
async function getWordPressHeaders(requestData, options = {}) {
|
|
78665
78816
|
validateEnvironment();
|
|
78666
78817
|
let authHeader = "";
|
|
78667
78818
|
if (CONFIG.JWT_TOKEN) {
|
|
@@ -78670,7 +78821,7 @@ async function getWordPressHeaders(requestData) {
|
|
|
78670
78821
|
logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, "AUTH");
|
|
78671
78822
|
} else if (CONFIG.OAUTH_ENABLED) {
|
|
78672
78823
|
logger.auth("OAuth is the primary authentication method - attempting to get tokens...");
|
|
78673
|
-
const oauthTokens = await getOAuthTokens();
|
|
78824
|
+
const oauthTokens = options.allowUnauthenticated && !CONFIG.WP_API_USERNAME ? await getValidTokens(generateServerUrlHash(getRequestUrl())) : await getOAuthTokens();
|
|
78674
78825
|
if (oauthTokens) {
|
|
78675
78826
|
authHeader = `Bearer ${oauthTokens.access_token}`;
|
|
78676
78827
|
logger.auth("Using OAuth token authentication for WordPress API");
|
|
@@ -78710,7 +78861,7 @@ async function getWordPressHeaders(requestData) {
|
|
|
78710
78861
|
if (name.toLowerCase() === "mcp-protocol-version") delete customHeaders[name];
|
|
78711
78862
|
}
|
|
78712
78863
|
const hasCustomHeaders = Object.keys(customHeaders).length > 0;
|
|
78713
|
-
if (!authHeader && !hasCustomHeaders) {
|
|
78864
|
+
if (!authHeader && !hasCustomHeaders && !(options.allowUnauthenticated && CONFIG.OAUTH_ENABLED)) {
|
|
78714
78865
|
throw new AuthError(
|
|
78715
78866
|
"No authentication method available. Please configure JWT_TOKEN, OAuth, Basic Auth (WP_API_USERNAME+WP_API_PASSWORD), or CUSTOM_HEADERS.",
|
|
78716
78867
|
"NO_AUTH_METHOD"
|