@automattic/mcp-wordpress-remote 0.5.1 → 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 +597 -415
- package/dist/proxy.js +854 -642
- package/package.json +7 -3
package/dist/lib.js
CHANGED
|
@@ -73824,10 +73824,9 @@ var ProgressTokenSchema = union([string2(), number2().int()]);
|
|
|
73824
73824
|
var CursorSchema = string2();
|
|
73825
73825
|
var TaskCreationParamsSchema = looseObject({
|
|
73826
73826
|
/**
|
|
73827
|
-
*
|
|
73828
|
-
* If null, the task has unlimited lifetime until manually cleaned up.
|
|
73827
|
+
* Requested duration in milliseconds to retain task from creation.
|
|
73829
73828
|
*/
|
|
73830
|
-
ttl:
|
|
73829
|
+
ttl: number2().optional(),
|
|
73831
73830
|
/**
|
|
73832
73831
|
* Time in milliseconds to wait between task status requests.
|
|
73833
73832
|
*/
|
|
@@ -74122,7 +74121,11 @@ var ClientCapabilitiesSchema = object({
|
|
|
74122
74121
|
/**
|
|
74123
74122
|
* Present if the client supports task creation.
|
|
74124
74123
|
*/
|
|
74125
|
-
tasks: ClientTasksCapabilitySchema.optional()
|
|
74124
|
+
tasks: ClientTasksCapabilitySchema.optional(),
|
|
74125
|
+
/**
|
|
74126
|
+
* Extensions that the client supports. Keys are extension identifiers (vendor-prefix/extension-name).
|
|
74127
|
+
*/
|
|
74128
|
+
extensions: record(string2(), AssertObjectSchema).optional()
|
|
74126
74129
|
});
|
|
74127
74130
|
var InitializeRequestParamsSchema = BaseRequestParamsSchema.extend({
|
|
74128
74131
|
/**
|
|
@@ -74183,7 +74186,11 @@ var ServerCapabilitiesSchema = object({
|
|
|
74183
74186
|
/**
|
|
74184
74187
|
* Present if the server supports task creation.
|
|
74185
74188
|
*/
|
|
74186
|
-
tasks: ServerTasksCapabilitySchema.optional()
|
|
74189
|
+
tasks: ServerTasksCapabilitySchema.optional(),
|
|
74190
|
+
/**
|
|
74191
|
+
* Extensions that the server supports. Keys are extension identifiers (vendor-prefix/extension-name).
|
|
74192
|
+
*/
|
|
74193
|
+
extensions: record(string2(), AssertObjectSchema).optional()
|
|
74187
74194
|
});
|
|
74188
74195
|
var InitializeResultSchema = ResultSchema.extend({
|
|
74189
74196
|
/**
|
|
@@ -74375,6 +74382,12 @@ var ResourceSchema = object({
|
|
|
74375
74382
|
* The MIME type of this resource, if known.
|
|
74376
74383
|
*/
|
|
74377
74384
|
mimeType: optional(string2()),
|
|
74385
|
+
/**
|
|
74386
|
+
* The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
|
|
74387
|
+
*
|
|
74388
|
+
* This can be used by Hosts to display file sizes and estimate context window usage.
|
|
74389
|
+
*/
|
|
74390
|
+
size: optional(number2()),
|
|
74378
74391
|
/**
|
|
74379
74392
|
* Optional annotations for the client.
|
|
74380
74393
|
*/
|
|
@@ -75393,7 +75406,12 @@ var OAuthTokensSchema = object({
|
|
|
75393
75406
|
token_type: string2(),
|
|
75394
75407
|
expires_in: coerce_exports.number().optional(),
|
|
75395
75408
|
scope: string2().optional(),
|
|
75396
|
-
refresh_token: string2().optional()
|
|
75409
|
+
refresh_token: string2().optional(),
|
|
75410
|
+
/**
|
|
75411
|
+
* Not part of the wire format: the authorization server this value was obtained from, added by
|
|
75412
|
+
* the client's `auth()` before it is stored and compared when it is read back.
|
|
75413
|
+
*/
|
|
75414
|
+
issuer: string2().optional().catch(void 0)
|
|
75397
75415
|
}).strip();
|
|
75398
75416
|
var OAuthErrorResponseSchema = object({
|
|
75399
75417
|
error: string2(),
|
|
@@ -75423,7 +75441,12 @@ var OAuthClientInformationSchema = object({
|
|
|
75423
75441
|
client_id: string2(),
|
|
75424
75442
|
client_secret: string2().optional(),
|
|
75425
75443
|
client_id_issued_at: number2().optional(),
|
|
75426
|
-
client_secret_expires_at: number2().optional()
|
|
75444
|
+
client_secret_expires_at: number2().optional(),
|
|
75445
|
+
/**
|
|
75446
|
+
* Not part of the wire format: the authorization server this value was obtained from, added by
|
|
75447
|
+
* the client's `auth()` before it is stored and compared when it is read back.
|
|
75448
|
+
*/
|
|
75449
|
+
issuer: string2().optional().catch(void 0)
|
|
75427
75450
|
}).strip();
|
|
75428
75451
|
var OAuthClientInformationFullSchema = OAuthClientMetadataSchema.merge(OAuthClientInformationSchema);
|
|
75429
75452
|
var OAuthClientRegistrationErrorSchema = object({
|
|
@@ -75530,6 +75553,154 @@ var OAUTH_ERRORS = {
|
|
|
75530
75553
|
[InvalidTargetError.errorCode]: InvalidTargetError
|
|
75531
75554
|
};
|
|
75532
75555
|
|
|
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
|
+
}
|
|
75624
|
+
var TokenResponseSchema = OAuthTokensSchema.omit({ issuer: true });
|
|
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
|
+
}
|
|
75703
|
+
|
|
75533
75704
|
// src/lib/utils.ts
|
|
75534
75705
|
import * as fs2 from "node:fs";
|
|
75535
75706
|
import * as path3 from "node:path";
|
|
@@ -75641,7 +75812,19 @@ async function writeTokens(serverUrlHash, tokens) {
|
|
|
75641
75812
|
...tokens,
|
|
75642
75813
|
obtained_at: Date.now()
|
|
75643
75814
|
};
|
|
75644
|
-
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
|
+
}
|
|
75645
75828
|
logger.info(`Stored tokens for server hash: ${serverUrlHash}`, "AUTH");
|
|
75646
75829
|
} catch (error51) {
|
|
75647
75830
|
logger.error(`Error writing tokens for ${serverUrlHash}`, "AUTH", error51);
|
|
@@ -75674,7 +75857,7 @@ function isTokenValid(tokens) {
|
|
|
75674
75857
|
if (!tokens?.access_token) {
|
|
75675
75858
|
return { isValid: false, error: "No access token" };
|
|
75676
75859
|
}
|
|
75677
|
-
if (
|
|
75860
|
+
if (tokens.expires_in === void 0 || tokens.obtained_at === void 0) {
|
|
75678
75861
|
return { isValid: true };
|
|
75679
75862
|
}
|
|
75680
75863
|
const now = Date.now();
|
|
@@ -75713,12 +75896,12 @@ var OAuthClientInfoSchema = external_exports.object({
|
|
|
75713
75896
|
client_id: external_exports.string().optional(),
|
|
75714
75897
|
client_secret: external_exports.string().optional()
|
|
75715
75898
|
});
|
|
75716
|
-
async function findAvailablePort(preferredPort) {
|
|
75899
|
+
async function findAvailablePort(preferredPort, host = "127.0.0.1") {
|
|
75717
75900
|
return new Promise((resolve, reject) => {
|
|
75718
75901
|
const server = net.createServer();
|
|
75719
75902
|
server.on("error", (err) => {
|
|
75720
75903
|
if (err.code === "EADDRINUSE") {
|
|
75721
|
-
server.listen(0);
|
|
75904
|
+
server.listen(0, host);
|
|
75722
75905
|
} else {
|
|
75723
75906
|
reject(err);
|
|
75724
75907
|
}
|
|
@@ -75729,7 +75912,7 @@ async function findAvailablePort(preferredPort) {
|
|
|
75729
75912
|
resolve(port);
|
|
75730
75913
|
});
|
|
75731
75914
|
});
|
|
75732
|
-
server.listen(preferredPort || 0);
|
|
75915
|
+
server.listen(preferredPort || 0, host);
|
|
75733
75916
|
});
|
|
75734
75917
|
}
|
|
75735
75918
|
function calculateDefaultPort(serverUrlHash) {
|
|
@@ -75757,24 +75940,17 @@ async function findExistingClientPort(serverUrlHash) {
|
|
|
75757
75940
|
function getServerUrlHash(serverUrl) {
|
|
75758
75941
|
return crypto3.createHash("md5").update(serverUrl).digest("hex");
|
|
75759
75942
|
}
|
|
75760
|
-
async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
|
|
75943
|
+
async function selectCallbackPort(serverUrl, specifiedPort, _unused, host = "127.0.0.1") {
|
|
75761
75944
|
const serverUrlHash = getServerUrlHash(serverUrl);
|
|
75762
75945
|
if (specifiedPort) {
|
|
75763
75946
|
return specifiedPort;
|
|
75764
75947
|
}
|
|
75765
|
-
const
|
|
75766
|
-
|
|
75767
|
-
findExistingClientPort(serverUrlHash),
|
|
75768
|
-
findAvailablePort(defaultPort)
|
|
75769
|
-
]);
|
|
75770
|
-
if (existingClientPort) {
|
|
75771
|
-
return existingClientPort;
|
|
75772
|
-
}
|
|
75773
|
-
return availablePort;
|
|
75948
|
+
const existingClientPort = await findExistingClientPort(serverUrlHash);
|
|
75949
|
+
return findAvailablePort(existingClientPort ?? calculateDefaultPort(serverUrlHash), host);
|
|
75774
75950
|
}
|
|
75775
75951
|
|
|
75776
75952
|
// src/lib/config.ts
|
|
75777
|
-
var MCP_WORDPRESS_REMOTE_VERSION = "0.5.
|
|
75953
|
+
var MCP_WORDPRESS_REMOTE_VERSION = "0.5.3";
|
|
75778
75954
|
function parsePositiveIntEnv(value, defaultValue) {
|
|
75779
75955
|
if (!value) {
|
|
75780
75956
|
return defaultValue;
|
|
@@ -75975,8 +76151,8 @@ async function getOAuthCallbackPort() {
|
|
|
75975
76151
|
return selectCallbackPort(
|
|
75976
76152
|
CONFIG.WP_API_URL,
|
|
75977
76153
|
CONFIG.OAUTH_CALLBACK_PORT,
|
|
75978
|
-
false
|
|
75979
|
-
|
|
76154
|
+
false,
|
|
76155
|
+
CONFIG.OAUTH_HOST
|
|
75980
76156
|
);
|
|
75981
76157
|
}
|
|
75982
76158
|
function formatConfigError(error51) {
|
|
@@ -76896,10 +77072,48 @@ defineLazyProperty(apps, "browserPrivate", () => "browserPrivate");
|
|
|
76896
77072
|
var open_default = open;
|
|
76897
77073
|
|
|
76898
77074
|
// src/lib/mcp-oauth-provider.ts
|
|
77075
|
+
import lockfile from "proper-lockfile";
|
|
77076
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
76899
77077
|
import { EventEmitter } from "node:events";
|
|
76900
77078
|
|
|
76901
77079
|
// src/lib/mcp-oauth-utils.ts
|
|
76902
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
|
|
76903
77117
|
function generatePKCE() {
|
|
76904
77118
|
const codeVerifier = crypto4.randomBytes(64).toString("base64url").substring(0, 128);
|
|
76905
77119
|
const codeChallenge = crypto4.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
@@ -76927,118 +77141,76 @@ function generateCanonicalResourceURI(serverUrl) {
|
|
|
76927
77141
|
}
|
|
76928
77142
|
}
|
|
76929
77143
|
async function discoverAuthorizationServerMetadata(authorizationServerUrl) {
|
|
76930
|
-
|
|
76931
|
-
|
|
76932
|
-
|
|
76933
|
-
const
|
|
76934
|
-
|
|
76935
|
-
|
|
76936
|
-
Accept: "application/json",
|
|
76937
|
-
...customHeaders
|
|
76938
|
-
}
|
|
76939
|
-
});
|
|
76940
|
-
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)
|
|
76941
77150
|
throw new OAuthError2(
|
|
76942
|
-
`
|
|
77151
|
+
`Metadata request failed (${response.status})`,
|
|
76943
77152
|
"METADATA_DISCOVERY_FAILED"
|
|
76944
77153
|
);
|
|
76945
|
-
|
|
76946
|
-
|
|
76947
|
-
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
77154
|
+
const parsed = OAuthMetadataSchema.safeParse(await response.json());
|
|
77155
|
+
if (!parsed.success || parsed.data.issuer !== authorizationServerUrl) {
|
|
76948
77156
|
throw new OAuthError2(
|
|
76949
|
-
"Authorization
|
|
77157
|
+
"Authorization metadata has an invalid or mismatched issuer",
|
|
76950
77158
|
"INVALID_METADATA"
|
|
76951
77159
|
);
|
|
76952
77160
|
}
|
|
76953
|
-
|
|
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);
|
|
76954
77166
|
return metadata;
|
|
76955
|
-
} catch (error51) {
|
|
76956
|
-
if (error51 instanceof OAuthError2) {
|
|
76957
|
-
throw error51;
|
|
76958
|
-
}
|
|
76959
|
-
throw new OAuthError2(
|
|
76960
|
-
`Error discovering authorization server metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
|
|
76961
|
-
"METADATA_DISCOVERY_ERROR"
|
|
76962
|
-
);
|
|
76963
77167
|
}
|
|
77168
|
+
throw new OAuthError2("No authorization server metadata found", "METADATA_DISCOVERY_FAILED");
|
|
76964
77169
|
}
|
|
76965
|
-
async function discoverProtectedResourceMetadata(resourceUrl) {
|
|
76966
|
-
|
|
76967
|
-
|
|
76968
|
-
|
|
76969
|
-
|
|
76970
|
-
|
|
76971
|
-
|
|
76972
|
-
|
|
76973
|
-
|
|
76974
|
-
|
|
76975
|
-
}
|
|
76976
|
-
|
|
76977
|
-
|
|
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)
|
|
76978
77188
|
throw new OAuthError2(
|
|
76979
|
-
`
|
|
77189
|
+
`Resource metadata request failed (${response.status})`,
|
|
76980
77190
|
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
76981
77191
|
);
|
|
76982
|
-
|
|
76983
|
-
|
|
76984
|
-
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) {
|
|
76985
77194
|
throw new OAuthError2(
|
|
76986
|
-
"Protected resource metadata
|
|
77195
|
+
"Protected resource metadata has an invalid or mismatched resource",
|
|
76987
77196
|
"INVALID_RESOURCE_METADATA"
|
|
76988
77197
|
);
|
|
76989
77198
|
}
|
|
76990
|
-
|
|
76991
|
-
return
|
|
76992
|
-
} catch (error51) {
|
|
76993
|
-
if (error51 instanceof OAuthError2) {
|
|
76994
|
-
throw error51;
|
|
76995
|
-
}
|
|
76996
|
-
throw new OAuthError2(
|
|
76997
|
-
`Error discovering protected resource metadata: ${error51 instanceof Error ? error51.message : String(error51)}`,
|
|
76998
|
-
"RESOURCE_METADATA_DISCOVERY_ERROR"
|
|
76999
|
-
);
|
|
77199
|
+
for (const issuer of parsed.data.authorization_servers) validateOAuthUrl(issuer);
|
|
77200
|
+
return parsed.data;
|
|
77000
77201
|
}
|
|
77001
|
-
|
|
77002
|
-
|
|
77003
|
-
|
|
77004
|
-
|
|
77005
|
-
const result = { scheme };
|
|
77006
|
-
const paramString = parts.slice(1).join(" ");
|
|
77007
|
-
const paramMatches = paramString.matchAll(/(\w+)="([^"]+)"/g);
|
|
77008
|
-
for (const match of paramMatches) {
|
|
77009
|
-
const [, key, value] = match;
|
|
77010
|
-
switch (key) {
|
|
77011
|
-
case "realm":
|
|
77012
|
-
result.realm = value;
|
|
77013
|
-
break;
|
|
77014
|
-
case "scope":
|
|
77015
|
-
result.scope = value;
|
|
77016
|
-
break;
|
|
77017
|
-
case "error":
|
|
77018
|
-
result.error = value;
|
|
77019
|
-
break;
|
|
77020
|
-
case "error_description":
|
|
77021
|
-
result.error_description = value;
|
|
77022
|
-
break;
|
|
77023
|
-
case "error_uri":
|
|
77024
|
-
result.error_uri = value;
|
|
77025
|
-
break;
|
|
77026
|
-
case "resource_metadata_url":
|
|
77027
|
-
result.resource_metadata_url = value;
|
|
77028
|
-
break;
|
|
77029
|
-
}
|
|
77030
|
-
}
|
|
77031
|
-
return result;
|
|
77202
|
+
throw new OAuthError2(
|
|
77203
|
+
"No protected resource metadata found",
|
|
77204
|
+
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
77205
|
+
);
|
|
77032
77206
|
}
|
|
77033
77207
|
async function registerDynamicClient(registrationEndpoint, registrationRequest) {
|
|
77034
77208
|
try {
|
|
77035
77209
|
logger.oauth(`Attempting dynamic client registration: ${registrationEndpoint}`);
|
|
77036
|
-
const
|
|
77037
|
-
const response = await proxyFetch(registrationEndpoint, {
|
|
77210
|
+
const response = await oauthFetch(registrationEndpoint, {
|
|
77038
77211
|
method: "POST",
|
|
77039
77212
|
headers: {
|
|
77040
|
-
"Content-Type": "application/json"
|
|
77041
|
-
...customHeaders
|
|
77213
|
+
"Content-Type": "application/json"
|
|
77042
77214
|
},
|
|
77043
77215
|
body: JSON.stringify(registrationRequest)
|
|
77044
77216
|
});
|
|
@@ -77050,12 +77222,26 @@ async function registerDynamicClient(registrationEndpoint, registrationRequest)
|
|
|
77050
77222
|
);
|
|
77051
77223
|
}
|
|
77052
77224
|
const registrationResponse = await response.json();
|
|
77053
|
-
if (!registrationResponse.client_id) {
|
|
77225
|
+
if (typeof registrationResponse.client_id !== "string" || !registrationResponse.client_id) {
|
|
77054
77226
|
throw new OAuthError2(
|
|
77055
77227
|
"Dynamic client registration response missing client_id",
|
|
77056
77228
|
"INVALID_REGISTRATION_RESPONSE"
|
|
77057
77229
|
);
|
|
77058
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
|
+
}
|
|
77059
77245
|
logger.oauth("Dynamic client registration successful");
|
|
77060
77246
|
logger.debug("Client ID obtained", "OAUTH", {
|
|
77061
77247
|
client_id: registrationResponse.client_id,
|
|
@@ -77085,12 +77271,10 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
|
|
|
77085
77271
|
if (resource) {
|
|
77086
77272
|
params.set("resource", resource);
|
|
77087
77273
|
}
|
|
77088
|
-
const
|
|
77089
|
-
const response = await proxyFetch(tokenEndpoint, {
|
|
77274
|
+
const response = await oauthFetch(tokenEndpoint, {
|
|
77090
77275
|
method: "POST",
|
|
77091
77276
|
headers: {
|
|
77092
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
77093
|
-
...customHeaders
|
|
77277
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
77094
77278
|
},
|
|
77095
77279
|
body: params.toString()
|
|
77096
77280
|
});
|
|
@@ -77101,7 +77285,9 @@ async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clien
|
|
|
77101
77285
|
"TOKEN_EXCHANGE_FAILED"
|
|
77102
77286
|
);
|
|
77103
77287
|
}
|
|
77104
|
-
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");
|
|
77105
77291
|
if (!tokenResponse.access_token) {
|
|
77106
77292
|
throw new OAuthError2("Token response missing access_token", "INVALID_TOKEN_RESPONSE");
|
|
77107
77293
|
}
|
|
@@ -77130,15 +77316,18 @@ function buildAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, sco
|
|
|
77130
77316
|
// OAuth 2.1 uses authorization code flow
|
|
77131
77317
|
client_id: clientId,
|
|
77132
77318
|
redirect_uri: redirectUri,
|
|
77133
|
-
scope: scopes.join(" "),
|
|
77134
77319
|
state,
|
|
77135
77320
|
code_challenge: codeChallenge,
|
|
77136
77321
|
code_challenge_method: "S256"
|
|
77137
77322
|
});
|
|
77323
|
+
if (scopes.length) params.set("scope", scopes.join(" "));
|
|
77138
77324
|
if (resource) {
|
|
77139
77325
|
params.set("resource", resource);
|
|
77140
77326
|
}
|
|
77141
|
-
|
|
77327
|
+
validateOAuthUrl(authorizationEndpoint);
|
|
77328
|
+
const url2 = new URL(authorizationEndpoint);
|
|
77329
|
+
params.forEach((value, key) => url2.searchParams.set(key, value));
|
|
77330
|
+
return url2.toString();
|
|
77142
77331
|
}
|
|
77143
77332
|
function generateSecureState() {
|
|
77144
77333
|
return crypto4.randomBytes(32).toString("base64url");
|
|
@@ -77219,110 +77408,26 @@ var AUTHORIZATION_CODE_HTML = `
|
|
|
77219
77408
|
</div>
|
|
77220
77409
|
|
|
77221
77410
|
<script>
|
|
77222
|
-
|
|
77223
|
-
|
|
77224
|
-
|
|
77225
|
-
|
|
77226
|
-
|
|
77227
|
-
|
|
77228
|
-
|
|
77229
|
-
|
|
77230
|
-
|
|
77231
|
-
|
|
77232
|
-
|
|
77233
|
-
|
|
77234
|
-
|
|
77235
|
-
|
|
77236
|
-
|
|
77237
|
-
|
|
77238
|
-
|
|
77239
|
-
|
|
77240
|
-
|
|
77241
|
-
|
|
77242
|
-
// Check for authorization code first (OAuth 2.1 flow)
|
|
77243
|
-
if (urlParams.has('code')) {
|
|
77244
|
-
const authData = {
|
|
77245
|
-
code: urlParams.get('code'),
|
|
77246
|
-
state: urlParams.get('state'),
|
|
77247
|
-
// Additional parameters for validation
|
|
77248
|
-
iss: urlParams.get('iss'),
|
|
77249
|
-
};
|
|
77250
|
-
|
|
77251
|
-
// Send authorization code to server for token exchange
|
|
77252
|
-
fetch('/oauth/callback', {
|
|
77253
|
-
method: 'POST',
|
|
77254
|
-
headers: {
|
|
77255
|
-
'Content-Type': 'application/json'
|
|
77256
|
-
},
|
|
77257
|
-
body: JSON.stringify(authData)
|
|
77258
|
-
})
|
|
77259
|
-
.then(response => {
|
|
77260
|
-
if (response.ok) {
|
|
77261
|
-
displayStatus('\u2705 Authorization successful!', 'success');
|
|
77262
|
-
displayDetails('You can safely close this window.');
|
|
77263
|
-
} else {
|
|
77264
|
-
return response.json().then(err => {
|
|
77265
|
-
throw new Error(err.error || 'Failed to exchange authorization code');
|
|
77266
|
-
});
|
|
77267
|
-
}
|
|
77268
|
-
})
|
|
77269
|
-
.catch(error => {
|
|
77270
|
-
displayStatus('\u274C Error completing authorization', 'error');
|
|
77271
|
-
displayDetails(error.message + '. Please try again.');
|
|
77272
|
-
});
|
|
77273
|
-
}
|
|
77274
|
-
// Check for access token in URL fragment (implicit flow)
|
|
77275
|
-
else if (hashParams.has('access_token')) {
|
|
77276
|
-
const tokens = {
|
|
77277
|
-
access_token: hashParams.get('access_token'),
|
|
77278
|
-
token_type: hashParams.get('token_type') || 'Bearer',
|
|
77279
|
-
expires_in: hashParams.get('expires_in') ? parseInt(hashParams.get('expires_in')) : 3600,
|
|
77280
|
-
scope: hashParams.get('scope'),
|
|
77281
|
-
state: hashParams.get('state'),
|
|
77282
|
-
obtained_at: Date.now()
|
|
77283
|
-
};
|
|
77284
|
-
|
|
77285
|
-
// Send tokens to server for storage
|
|
77286
|
-
fetch('/oauth/tokens', {
|
|
77287
|
-
method: 'POST',
|
|
77288
|
-
headers: {
|
|
77289
|
-
'Content-Type': 'application/json'
|
|
77290
|
-
},
|
|
77291
|
-
body: JSON.stringify(tokens)
|
|
77292
|
-
})
|
|
77293
|
-
.then(response => {
|
|
77294
|
-
if (response.ok) {
|
|
77295
|
-
displayStatus('\u2705 Authorization successful!', 'success');
|
|
77296
|
-
displayDetails('You can safely close this window.');
|
|
77297
|
-
} else {
|
|
77298
|
-
return response.json().then(err => {
|
|
77299
|
-
throw new Error(err.error || 'Failed to store access token');
|
|
77300
|
-
});
|
|
77301
|
-
}
|
|
77302
|
-
})
|
|
77303
|
-
.catch(error => {
|
|
77304
|
-
displayStatus('\u274C Error completing authorization', 'error');
|
|
77305
|
-
displayDetails(error.message + '. Please try again.');
|
|
77306
|
-
});
|
|
77307
|
-
}
|
|
77308
|
-
// Check for errors in query parameters
|
|
77309
|
-
else if (urlParams.has('error')) {
|
|
77310
|
-
const error = urlParams.get('error');
|
|
77311
|
-
const errorDescription = urlParams.get('error_description');
|
|
77312
|
-
displayStatus('\u274C Authorization failed', 'error');
|
|
77313
|
-
displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
|
|
77314
|
-
}
|
|
77315
|
-
// Check for errors in URL fragment (implicit flow errors)
|
|
77316
|
-
else if (hashParams.has('error')) {
|
|
77317
|
-
const error = hashParams.get('error');
|
|
77318
|
-
const errorDescription = hashParams.get('error_description');
|
|
77319
|
-
displayStatus('\u274C Authorization failed', 'error');
|
|
77320
|
-
displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
|
|
77321
|
-
}
|
|
77322
|
-
// No authorization code or access token found
|
|
77323
|
-
else {
|
|
77324
|
-
displayStatus('\u274C No authorization code or access token received', 'error');
|
|
77325
|
-
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';
|
|
77326
77431
|
}
|
|
77327
77432
|
</script>
|
|
77328
77433
|
</body>
|
|
@@ -77337,6 +77442,17 @@ var OAuthCallbackServer = class {
|
|
|
77337
77442
|
this.setupRoutes();
|
|
77338
77443
|
}
|
|
77339
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
|
+
});
|
|
77340
77456
|
this.app.use(import_express.default.json());
|
|
77341
77457
|
this.app.get("/oauth/callback", (req, res) => {
|
|
77342
77458
|
logger.oauth("OAuth 2.1 authorization callback page requested");
|
|
@@ -77344,46 +77460,47 @@ var OAuthCallbackServer = class {
|
|
|
77344
77460
|
});
|
|
77345
77461
|
this.app.post("/oauth/callback", async (req, res) => {
|
|
77346
77462
|
try {
|
|
77347
|
-
const { code, state, iss } = req.body;
|
|
77348
|
-
|
|
77349
|
-
|
|
77350
|
-
|
|
77351
|
-
|
|
77352
|
-
|
|
77353
|
-
|
|
77354
|
-
state: state || "none",
|
|
77355
|
-
issuer: iss || "none"
|
|
77356
|
-
});
|
|
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");
|
|
77357
77470
|
this.events.emit("oauth-code-received", { code, state, iss });
|
|
77358
|
-
res.json({
|
|
77471
|
+
res.json({
|
|
77472
|
+
success: true,
|
|
77473
|
+
message: "Authorization code received; check connection status in the app"
|
|
77474
|
+
});
|
|
77359
77475
|
} catch (error51) {
|
|
77360
|
-
|
|
77361
|
-
|
|
77362
|
-
|
|
77363
|
-
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 });
|
|
77364
77479
|
}
|
|
77365
77480
|
});
|
|
77366
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
|
+
}
|
|
77367
77486
|
try {
|
|
77368
|
-
|
|
77369
|
-
if (!
|
|
77370
|
-
throw new OAuthError2("
|
|
77371
|
-
|
|
77372
|
-
|
|
77373
|
-
|
|
77374
|
-
|
|
77375
|
-
|
|
77376
|
-
|
|
77377
|
-
|
|
77378
|
-
});
|
|
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
|
+
};
|
|
77379
77497
|
await writeTokens(this.options.serverUrlHash, tokens);
|
|
77380
77498
|
this.events.emit("oauth-success", tokens);
|
|
77381
|
-
res.json({ success: true
|
|
77499
|
+
res.json({ success: true });
|
|
77382
77500
|
} catch (error51) {
|
|
77383
|
-
|
|
77384
|
-
|
|
77385
|
-
|
|
77386
|
-
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 });
|
|
77387
77504
|
}
|
|
77388
77505
|
});
|
|
77389
77506
|
this.app.get("/oauth/health", (req, res) => {
|
|
@@ -77400,21 +77517,33 @@ var OAuthCallbackServer = class {
|
|
|
77400
77517
|
}
|
|
77401
77518
|
);
|
|
77402
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
|
+
}
|
|
77403
77526
|
async start() {
|
|
77404
77527
|
return new Promise((resolve, reject) => {
|
|
77405
77528
|
try {
|
|
77406
|
-
this.server = this.app.listen(this.options.port, this.options.host, () => {
|
|
77529
|
+
this.server = this.app.listen(this.options.port, this.options.host, (error51) => {
|
|
77530
|
+
clearTimeout(this.startupTimer);
|
|
77531
|
+
if (error51) {
|
|
77532
|
+
reject(error51);
|
|
77533
|
+
return;
|
|
77534
|
+
}
|
|
77407
77535
|
logger.oauth(
|
|
77408
77536
|
`OAuth callback server listening on http://${this.options.host}:${this.options.port}`
|
|
77409
77537
|
);
|
|
77410
77538
|
resolve();
|
|
77411
77539
|
});
|
|
77412
77540
|
this.server.on("error", (error51) => {
|
|
77541
|
+
clearTimeout(this.startupTimer);
|
|
77413
77542
|
logger.error("OAuth callback server error", "OAUTH", error51);
|
|
77414
77543
|
reject(error51);
|
|
77415
77544
|
});
|
|
77416
77545
|
if (this.options.timeout) {
|
|
77417
|
-
setTimeout(() => {
|
|
77546
|
+
this.startupTimer = setTimeout(() => {
|
|
77418
77547
|
if (!this.server?.listening) {
|
|
77419
77548
|
reject(new OAuthError2("OAuth callback server startup timeout", "TIMEOUT"));
|
|
77420
77549
|
}
|
|
@@ -77426,6 +77555,7 @@ var OAuthCallbackServer = class {
|
|
|
77426
77555
|
});
|
|
77427
77556
|
}
|
|
77428
77557
|
async stop() {
|
|
77558
|
+
clearTimeout(this.startupTimer);
|
|
77429
77559
|
if (!this.server) return;
|
|
77430
77560
|
const server = this.server;
|
|
77431
77561
|
this.server = null;
|
|
@@ -77457,7 +77587,9 @@ var MCPOAuthProvider = class {
|
|
|
77457
77587
|
this.authServerMetadata = null;
|
|
77458
77588
|
this.resourceMetadata = null;
|
|
77459
77589
|
const serverUrl = options.serverUrl || CONFIG.WP_API_URL;
|
|
77460
|
-
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;
|
|
77461
77593
|
this.config = {
|
|
77462
77594
|
serverUrl,
|
|
77463
77595
|
resource: generateCanonicalResourceURI(serverUrl),
|
|
@@ -77516,187 +77648,217 @@ var MCPOAuthProvider = class {
|
|
|
77516
77648
|
/**
|
|
77517
77649
|
* Discover OAuth 2.1 server endpoints using MCP-compliant discovery
|
|
77518
77650
|
*/
|
|
77519
|
-
async discoverOAuthEndpoints() {
|
|
77520
|
-
|
|
77521
|
-
|
|
77522
|
-
|
|
77523
|
-
|
|
77524
|
-
|
|
77525
|
-
|
|
77526
|
-
|
|
77527
|
-
|
|
77528
|
-
|
|
77529
|
-
|
|
77530
|
-
|
|
77531
|
-
|
|
77532
|
-
|
|
77533
|
-
|
|
77534
|
-
|
|
77535
|
-
|
|
77536
|
-
|
|
77537
|
-
|
|
77538
|
-
this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
|
|
77539
|
-
} else {
|
|
77540
|
-
throw new OAuthError2("Could not determine authorization server URL", "DISCOVERY_FAILED");
|
|
77541
|
-
}
|
|
77542
|
-
} catch (error51) {
|
|
77543
|
-
logger.error("OAuth endpoint discovery failed", "OAUTH", error51);
|
|
77544
|
-
throw error51;
|
|
77545
|
-
}
|
|
77546
|
-
}
|
|
77547
|
-
/**
|
|
77548
|
-
* Discover endpoints via 401 Unauthorized response (RFC 9728 Section 5.1)
|
|
77549
|
-
*/
|
|
77550
|
-
async discoverVia401Response() {
|
|
77551
|
-
try {
|
|
77552
|
-
logger.oauth("Attempting discovery via 401 response method");
|
|
77553
|
-
const customHeaders = getCustomHeaders();
|
|
77554
|
-
const response = await proxyFetch(this.config.serverUrl, {
|
|
77555
|
-
headers: {
|
|
77556
|
-
Accept: "application/json",
|
|
77557
|
-
...customHeaders
|
|
77558
|
-
}
|
|
77559
|
-
});
|
|
77560
|
-
if (response.status === 401) {
|
|
77561
|
-
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
77562
|
-
if (wwwAuthHeader) {
|
|
77563
|
-
const authInfo = parseWWWAuthenticateHeader(wwwAuthHeader);
|
|
77564
|
-
logger.oauth("WWW-Authenticate header parsed successfully");
|
|
77565
|
-
if (authInfo.resource_metadata_url) {
|
|
77566
|
-
const metadataResponse = await proxyFetch(authInfo.resource_metadata_url, {
|
|
77567
|
-
headers: {
|
|
77568
|
-
Accept: "application/json",
|
|
77569
|
-
...customHeaders
|
|
77570
|
-
}
|
|
77571
|
-
});
|
|
77572
|
-
if (metadataResponse.ok) {
|
|
77573
|
-
this.resourceMetadata = await metadataResponse.json();
|
|
77574
|
-
logger.oauth("Protected resource metadata obtained via 401 response");
|
|
77575
|
-
}
|
|
77576
|
-
}
|
|
77577
|
-
}
|
|
77578
|
-
}
|
|
77579
|
-
} catch (error51) {
|
|
77580
|
-
logger.warn("401 response discovery method failed", "OAUTH", error51);
|
|
77581
|
-
}
|
|
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
|
+
];
|
|
77582
77670
|
}
|
|
77583
|
-
/**
|
|
77584
|
-
* Get authorization server URL from discovered metadata
|
|
77585
|
-
*/
|
|
77586
77671
|
getAuthorizationServerUrl() {
|
|
77587
|
-
|
|
77588
|
-
return this.resourceMetadata.authorization_servers[0];
|
|
77589
|
-
}
|
|
77590
|
-
try {
|
|
77591
|
-
const url2 = new URL(this.config.serverUrl);
|
|
77592
|
-
return url2.origin;
|
|
77593
|
-
} catch {
|
|
77594
|
-
return null;
|
|
77595
|
-
}
|
|
77672
|
+
return this.authServerMetadata?.issuer ?? this.resourceMetadata?.authorization_servers[0] ?? new URL(this.config.serverUrl).origin;
|
|
77596
77673
|
}
|
|
77597
77674
|
/**
|
|
77598
77675
|
* Perform dynamic client registration if needed and supported
|
|
77599
77676
|
*/
|
|
77600
77677
|
async ensureClientRegistration() {
|
|
77678
|
+
const issuer = this.getAuthorizationServerUrl();
|
|
77679
|
+
const stored = await readClientInfo(this.serverUrlHash);
|
|
77601
77680
|
if (this.config.clientId) {
|
|
77602
|
-
|
|
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 });
|
|
77603
77688
|
return;
|
|
77604
77689
|
}
|
|
77605
|
-
|
|
77606
|
-
|
|
77607
|
-
this.config.clientId = storedClientInfo.client_id;
|
|
77608
|
-
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;
|
|
77609
77692
|
return;
|
|
77610
77693
|
}
|
|
77611
|
-
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
|
|
77694
|
+
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION)
|
|
77612
77695
|
throw new OAuthError2(
|
|
77613
|
-
"No client
|
|
77696
|
+
"No issuer-bound client registration; configure a client ID or enable registration",
|
|
77614
77697
|
"NO_CLIENT_ID"
|
|
77615
77698
|
);
|
|
77616
|
-
|
|
77617
|
-
if (!this.authServerMetadata?.registration_endpoint) {
|
|
77699
|
+
if (!this.authServerMetadata?.registration_endpoint)
|
|
77618
77700
|
throw new OAuthError2(
|
|
77619
|
-
"
|
|
77701
|
+
"Authorization server does not support dynamic registration; configure its client ID",
|
|
77620
77702
|
"NO_DYNAMIC_REGISTRATION"
|
|
77621
77703
|
);
|
|
77622
|
-
|
|
77623
|
-
|
|
77624
|
-
|
|
77625
|
-
|
|
77626
|
-
|
|
77627
|
-
|
|
77628
|
-
|
|
77629
|
-
|
|
77630
|
-
|
|
77631
|
-
|
|
77632
|
-
|
|
77633
|
-
|
|
77634
|
-
|
|
77635
|
-
|
|
77636
|
-
|
|
77637
|
-
|
|
77638
|
-
|
|
77639
|
-
|
|
77640
|
-
|
|
77641
|
-
};
|
|
77642
|
-
await writeClientInfo(this.serverUrlHash, clientInfo);
|
|
77643
|
-
this.config.clientId = registrationResponse.client_id;
|
|
77644
|
-
logger.oauth("Dynamic client registration completed successfully");
|
|
77645
|
-
} catch (error51) {
|
|
77646
|
-
logger.error("Dynamic client registration failed", "OAUTH", error51);
|
|
77647
|
-
throw error51;
|
|
77648
|
-
}
|
|
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;
|
|
77649
77723
|
}
|
|
77650
77724
|
/**
|
|
77651
77725
|
* Initiate MCP-compliant OAuth 2.1 authorization flow
|
|
77652
77726
|
*/
|
|
77653
|
-
|
|
77654
|
-
|
|
77655
|
-
|
|
77656
|
-
|
|
77657
|
-
|
|
77658
|
-
|
|
77659
|
-
const
|
|
77660
|
-
|
|
77661
|
-
|
|
77662
|
-
|
|
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;
|
|
77663
77738
|
}
|
|
77664
|
-
|
|
77665
|
-
|
|
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;
|
|
77666
77746
|
try {
|
|
77667
|
-
await
|
|
77668
|
-
logger.oauth("OAuth 2.1 authorization completed successfully");
|
|
77747
|
+
release = await lockfile.lock(file2, { realpath: false });
|
|
77669
77748
|
} catch (error51) {
|
|
77670
|
-
|
|
77671
|
-
|
|
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();
|
|
77672
77812
|
} finally {
|
|
77673
|
-
|
|
77813
|
+
await release();
|
|
77674
77814
|
}
|
|
77675
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
|
+
}
|
|
77676
77824
|
/**
|
|
77677
77825
|
* Perform the complete OAuth 2.1 authorization flow
|
|
77678
77826
|
*/
|
|
77679
77827
|
async performAuthorization() {
|
|
77680
77828
|
try {
|
|
77681
|
-
await this.discoverOAuthEndpoints();
|
|
77682
|
-
|
|
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;
|
|
77683
77844
|
const callbackServer = setupWPOAuthCallbackServer(
|
|
77684
77845
|
{
|
|
77685
77846
|
port: callbackPort,
|
|
77686
77847
|
host: this.config.host,
|
|
77687
77848
|
serverUrlHash: this.serverUrlHash,
|
|
77688
|
-
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
|
|
77689
77853
|
},
|
|
77690
77854
|
this.events
|
|
77691
77855
|
);
|
|
77692
77856
|
await callbackServer.start();
|
|
77693
77857
|
logger.oauth("OAuth callback server started");
|
|
77694
77858
|
try {
|
|
77695
|
-
const actualRedirectUri = `http://${this.config.host}:${callbackPort}/oauth/callback`;
|
|
77859
|
+
const actualRedirectUri = `http://${this.config.host === "::1" ? "[::1]" : this.config.host}:${callbackPort}/oauth/callback`;
|
|
77696
77860
|
this.config.redirectUri = actualRedirectUri;
|
|
77697
77861
|
await this.ensureClientRegistration();
|
|
77698
|
-
this.currentPKCE = generatePKCE();
|
|
77699
|
-
this.currentState = generateSecureState();
|
|
77700
77862
|
await writeTextFile(this.serverUrlHash, "pkce_verifier.txt", this.currentPKCE.codeVerifier);
|
|
77701
77863
|
await writeTextFile(this.serverUrlHash, "oauth_state.txt", this.currentState);
|
|
77702
77864
|
const authUrl = buildAuthorizationUrl(
|
|
@@ -77706,10 +77868,13 @@ var MCPOAuthProvider = class {
|
|
|
77706
77868
|
this.config.scopes,
|
|
77707
77869
|
this.currentState,
|
|
77708
77870
|
this.currentPKCE.codeChallenge,
|
|
77709
|
-
|
|
77871
|
+
this.resourceIndicator()
|
|
77710
77872
|
);
|
|
77711
77873
|
logger.oauth("Built OAuth 2.1 authorization URL");
|
|
77712
77874
|
logger.debug("Authorization URL", "OAUTH", { url: authUrl });
|
|
77875
|
+
const authorizationCode = this.waitForAuthorizationCode();
|
|
77876
|
+
void authorizationCode.catch(() => {
|
|
77877
|
+
});
|
|
77713
77878
|
try {
|
|
77714
77879
|
await open_default(authUrl);
|
|
77715
77880
|
logger.oauth("Browser opened successfully");
|
|
@@ -77720,13 +77885,20 @@ var MCPOAuthProvider = class {
|
|
|
77720
77885
|
logger.info(`${authUrl}`);
|
|
77721
77886
|
logger.info("===============================\n");
|
|
77722
77887
|
}
|
|
77723
|
-
const authCode = await
|
|
77724
|
-
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
|
+
};
|
|
77725
77895
|
await writeTokens(this.serverUrlHash, tokens);
|
|
77726
77896
|
logger.oauth("OAuth 2.1 tokens stored successfully");
|
|
77727
77897
|
return tokens;
|
|
77728
77898
|
} finally {
|
|
77729
77899
|
await callbackServer.stop();
|
|
77900
|
+
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
77901
|
+
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
77730
77902
|
}
|
|
77731
77903
|
} catch (error51) {
|
|
77732
77904
|
logger.error("Error during OAuth 2.1 authorization flow", "OAUTH", error51);
|
|
@@ -77747,12 +77919,16 @@ var MCPOAuthProvider = class {
|
|
|
77747
77919
|
this.events.removeAllListeners("oauth-error");
|
|
77748
77920
|
clearTimeout(timeout);
|
|
77749
77921
|
};
|
|
77750
|
-
this.events.once("oauth-code-received", async ({ code, state }) => {
|
|
77922
|
+
this.events.once("oauth-code-received", async ({ code, state, iss }) => {
|
|
77751
77923
|
cleanup();
|
|
77752
77924
|
if (state !== this.currentState) {
|
|
77753
77925
|
reject(new OAuthError2("State parameter mismatch", "INVALID_STATE"));
|
|
77754
77926
|
return;
|
|
77755
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
|
+
}
|
|
77756
77932
|
logger.oauth("Valid authorization code received");
|
|
77757
77933
|
resolve(code);
|
|
77758
77934
|
});
|
|
@@ -77778,7 +77954,7 @@ var MCPOAuthProvider = class {
|
|
|
77778
77954
|
redirectUri,
|
|
77779
77955
|
this.config.clientId,
|
|
77780
77956
|
codeVerifier,
|
|
77781
|
-
|
|
77957
|
+
this.resourceIndicator()
|
|
77782
77958
|
);
|
|
77783
77959
|
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
77784
77960
|
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
@@ -77844,6 +78020,7 @@ import * as path5 from "node:path";
|
|
|
77844
78020
|
import * as os3 from "node:os";
|
|
77845
78021
|
|
|
77846
78022
|
// src/lib/persistent-oauth-client-provider.ts
|
|
78023
|
+
import { randomBytes } from "node:crypto";
|
|
77847
78024
|
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
77848
78025
|
var PersistentWPOAuthClientProvider = class {
|
|
77849
78026
|
constructor(options) {
|
|
@@ -78001,7 +78178,10 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78001
78178
|
*/
|
|
78002
78179
|
async performAuthorization() {
|
|
78003
78180
|
const callbackPort = this.options.callbackPort === 0 ? await getOAuthCallbackPort() : this.options.callbackPort;
|
|
78181
|
+
const state = this.generateState();
|
|
78004
78182
|
const callbackServerOptions = {
|
|
78183
|
+
flow: "implicit",
|
|
78184
|
+
state,
|
|
78005
78185
|
port: callbackPort,
|
|
78006
78186
|
host: this.options.host,
|
|
78007
78187
|
serverUrlHash: this.serverUrlHash,
|
|
@@ -78013,12 +78193,14 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78013
78193
|
logger.oauth("Starting callback server...");
|
|
78014
78194
|
await callbackServer.start();
|
|
78015
78195
|
logger.oauth("Callback server started successfully");
|
|
78016
|
-
const state = this.generateState();
|
|
78017
78196
|
logger.debug(`Generated state parameter: ${state}`, "OAUTH");
|
|
78018
78197
|
const authUrl = this.buildAuthorizationUrl(callbackServer.getCallbackUrl(), state);
|
|
78019
78198
|
logger.oauth(`Built authorization URL: ${authUrl}`);
|
|
78020
78199
|
logger.debug(`Callback URL: ${callbackServer.getCallbackUrl()}`, "OAUTH");
|
|
78021
78200
|
logger.oauth("Attempting to open browser...");
|
|
78201
|
+
const authorization = this.waitForAuthorizationResult();
|
|
78202
|
+
void authorization.catch(() => {
|
|
78203
|
+
});
|
|
78022
78204
|
try {
|
|
78023
78205
|
await open_default(authUrl);
|
|
78024
78206
|
logger.oauth("Browser opened successfully");
|
|
@@ -78030,7 +78212,7 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78030
78212
|
logger.info("===============================\n");
|
|
78031
78213
|
}
|
|
78032
78214
|
logger.oauth("Waiting for authorization result...");
|
|
78033
|
-
const tokens = await
|
|
78215
|
+
const tokens = await authorization;
|
|
78034
78216
|
logger.oauth("Authorization result received");
|
|
78035
78217
|
logger.oauth("Saving tokens to persistent storage...");
|
|
78036
78218
|
await this.saveTokens(tokens);
|
|
@@ -78083,7 +78265,7 @@ var PersistentWPOAuthClientProvider = class {
|
|
|
78083
78265
|
* Generate a secure state parameter
|
|
78084
78266
|
*/
|
|
78085
78267
|
generateState() {
|
|
78086
|
-
return
|
|
78268
|
+
return randomBytes(32).toString("base64url");
|
|
78087
78269
|
}
|
|
78088
78270
|
/**
|
|
78089
78271
|
* Wait for authorization result from callback server
|
|
@@ -78506,6 +78688,12 @@ function constructApiUrl(baseUrl, defaultEndpoint) {
|
|
|
78506
78688
|
return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
|
|
78507
78689
|
}
|
|
78508
78690
|
}
|
|
78691
|
+
function getMCPOAuthProvider() {
|
|
78692
|
+
return mcpOAuthProvider ??= new MCPOAuthProvider({
|
|
78693
|
+
serverUrl: getRequestUrl(),
|
|
78694
|
+
clientId: CONFIG.WP_OAUTH_CLIENT_ID
|
|
78695
|
+
});
|
|
78696
|
+
}
|
|
78509
78697
|
async function getOAuthTokens() {
|
|
78510
78698
|
try {
|
|
78511
78699
|
if (!CONFIG.OAUTH_ENABLED) {
|
|
@@ -78513,7 +78701,7 @@ async function getOAuthTokens() {
|
|
|
78513
78701
|
return null;
|
|
78514
78702
|
}
|
|
78515
78703
|
logger.auth("Attempting to get OAuth tokens for WordPress API (MCP-compliant)...");
|
|
78516
|
-
const serverUrl =
|
|
78704
|
+
const serverUrl = getRequestUrl();
|
|
78517
78705
|
const serverUrlHash = generateServerUrlHash(serverUrl);
|
|
78518
78706
|
const existingTokens = await getValidTokens(serverUrlHash);
|
|
78519
78707
|
if (existingTokens) {
|
|
@@ -78524,16 +78712,10 @@ async function getOAuthTokens() {
|
|
|
78524
78712
|
logger.auth("Starting MCP-compliant OAuth 2.1 authentication flow");
|
|
78525
78713
|
logger.auth("Your browser should open automatically for authentication");
|
|
78526
78714
|
if (CONFIG.OAUTH_FLOW_TYPE === "authorization_code" && CONFIG.OAUTH_USE_PKCE) {
|
|
78527
|
-
if (!mcpOAuthProvider) {
|
|
78528
|
-
mcpOAuthProvider = new MCPOAuthProvider({
|
|
78529
|
-
serverUrl,
|
|
78530
|
-
clientId: CONFIG.WP_OAUTH_CLIENT_ID,
|
|
78531
|
-
scopes: getDefaultOAuthScopes()
|
|
78532
|
-
});
|
|
78533
|
-
}
|
|
78534
78715
|
logger.auth("Using MCP-compliant OAuth 2.1 authorization code flow with PKCE");
|
|
78535
|
-
|
|
78536
|
-
|
|
78716
|
+
const provider = getMCPOAuthProvider();
|
|
78717
|
+
await provider.authorize();
|
|
78718
|
+
const tokens = await provider.tokens();
|
|
78537
78719
|
if (tokens) {
|
|
78538
78720
|
logger.auth("MCP OAuth 2.1 tokens obtained for WordPress API access");
|
|
78539
78721
|
return tokens;
|
|
@@ -78630,7 +78812,7 @@ function updateSessionId(sessionId) {
|
|
|
78630
78812
|
globalSessionId = sessionId;
|
|
78631
78813
|
logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
|
|
78632
78814
|
}
|
|
78633
|
-
async function getWordPressHeaders(requestData) {
|
|
78815
|
+
async function getWordPressHeaders(requestData, options = {}) {
|
|
78634
78816
|
validateEnvironment();
|
|
78635
78817
|
let authHeader = "";
|
|
78636
78818
|
if (CONFIG.JWT_TOKEN) {
|
|
@@ -78639,7 +78821,7 @@ async function getWordPressHeaders(requestData) {
|
|
|
78639
78821
|
logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, "AUTH");
|
|
78640
78822
|
} else if (CONFIG.OAUTH_ENABLED) {
|
|
78641
78823
|
logger.auth("OAuth is the primary authentication method - attempting to get tokens...");
|
|
78642
|
-
const oauthTokens = await getOAuthTokens();
|
|
78824
|
+
const oauthTokens = options.allowUnauthenticated && !CONFIG.WP_API_USERNAME ? await getValidTokens(generateServerUrlHash(getRequestUrl())) : await getOAuthTokens();
|
|
78643
78825
|
if (oauthTokens) {
|
|
78644
78826
|
authHeader = `Bearer ${oauthTokens.access_token}`;
|
|
78645
78827
|
logger.auth("Using OAuth token authentication for WordPress API");
|
|
@@ -78679,7 +78861,7 @@ async function getWordPressHeaders(requestData) {
|
|
|
78679
78861
|
if (name.toLowerCase() === "mcp-protocol-version") delete customHeaders[name];
|
|
78680
78862
|
}
|
|
78681
78863
|
const hasCustomHeaders = Object.keys(customHeaders).length > 0;
|
|
78682
|
-
if (!authHeader && !hasCustomHeaders) {
|
|
78864
|
+
if (!authHeader && !hasCustomHeaders && !(options.allowUnauthenticated && CONFIG.OAUTH_ENABLED)) {
|
|
78683
78865
|
throw new AuthError(
|
|
78684
78866
|
"No authentication method available. Please configure JWT_TOKEN, OAuth, Basic Auth (WP_API_USERNAME+WP_API_PASSWORD), or CUSTOM_HEADERS.",
|
|
78685
78867
|
"NO_AUTH_METHOD"
|