@lifeaitools/clauth 1.5.18 → 1.5.20
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/cli/commands/serve.js +178 -18
- package/package.json +1 -1
package/cli/commands/serve.js
CHANGED
|
@@ -2878,19 +2878,136 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2878
2878
|
return res.end();
|
|
2879
2879
|
}
|
|
2880
2880
|
|
|
2881
|
-
// ── OAuth Discovery
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2881
|
+
// ── OAuth Discovery (RFC 9728 + RFC 8414) ──────────────
|
|
2882
|
+
// claude.ai probes these for ALL remote MCP connections.
|
|
2883
|
+
// resource MUST match the connector URL configured in claude.ai (/sse).
|
|
2884
|
+
if (reqPath.startsWith("/.well-known/oauth-protected-resource")) {
|
|
2885
|
+
const base = oauthBase();
|
|
2886
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2887
|
+
return res.end(JSON.stringify({
|
|
2888
|
+
resource: `${base}/sse`,
|
|
2889
|
+
authorization_servers: [base],
|
|
2890
|
+
scopes_supported: ["mcp:tools"],
|
|
2891
|
+
bearer_methods_supported: ["header"],
|
|
2892
|
+
}));
|
|
2886
2893
|
}
|
|
2887
2894
|
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2895
|
+
if (reqPath === "/.well-known/oauth-authorization-server") {
|
|
2896
|
+
const base = oauthBase();
|
|
2897
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
2898
|
+
return res.end(JSON.stringify({
|
|
2899
|
+
issuer: base,
|
|
2900
|
+
authorization_endpoint: `${base}/authorize`,
|
|
2901
|
+
token_endpoint: `${base}/token`,
|
|
2902
|
+
registration_endpoint: `${base}/register`,
|
|
2903
|
+
response_types_supported: ["code"],
|
|
2904
|
+
grant_types_supported: ["authorization_code"],
|
|
2905
|
+
code_challenge_methods_supported: ["S256"],
|
|
2906
|
+
scopes_supported: ["mcp:tools"],
|
|
2907
|
+
}));
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// ── Dynamic Client Registration (RFC 7591) ──────────────
|
|
2911
|
+
if (method === "POST" && reqPath === "/register") {
|
|
2912
|
+
let body;
|
|
2913
|
+
try { body = await readBody(req); } catch {
|
|
2914
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
2915
|
+
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
2916
|
+
}
|
|
2917
|
+
const clientId = crypto.randomBytes(16).toString("hex");
|
|
2918
|
+
const clientSecret = crypto.randomBytes(32).toString("hex");
|
|
2919
|
+
const client = {
|
|
2920
|
+
client_id: clientId, client_secret: clientSecret,
|
|
2921
|
+
client_name: body.client_name || "unknown",
|
|
2922
|
+
redirect_uris: body.redirect_uris || [],
|
|
2923
|
+
grant_types: body.grant_types || ["authorization_code"],
|
|
2924
|
+
response_types: body.response_types || ["code"],
|
|
2925
|
+
token_endpoint_auth_method: body.token_endpoint_auth_method || "client_secret_post",
|
|
2926
|
+
};
|
|
2927
|
+
oauthClients.set(clientId, client);
|
|
2928
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: registered client ${clientId} (${client.client_name})\n`;
|
|
2929
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
2930
|
+
res.writeHead(201, { "Content-Type": "application/json", ...CORS });
|
|
2931
|
+
return res.end(JSON.stringify(client));
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// ── Authorization endpoint — auto-approve ──────────────
|
|
2935
|
+
if (method === "GET" && reqPath === "/authorize") {
|
|
2936
|
+
const clientId = url.searchParams.get("client_id");
|
|
2937
|
+
const redirectUri = url.searchParams.get("redirect_uri");
|
|
2938
|
+
const state = url.searchParams.get("state");
|
|
2939
|
+
const codeChallenge = url.searchParams.get("code_challenge");
|
|
2940
|
+
const codeChallengeMethod = url.searchParams.get("code_challenge_method");
|
|
2941
|
+
|
|
2942
|
+
if (!clientId || !redirectUri) {
|
|
2943
|
+
res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
|
|
2944
|
+
return res.end("Missing client_id or redirect_uri");
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
const code = crypto.randomBytes(32).toString("hex");
|
|
2948
|
+
oauthCodes.set(code, {
|
|
2949
|
+
client_id: clientId, redirect_uri: redirectUri,
|
|
2950
|
+
code_challenge: codeChallenge, code_challenge_method: codeChallengeMethod,
|
|
2951
|
+
expires: Date.now() + 300_000,
|
|
2952
|
+
});
|
|
2953
|
+
|
|
2954
|
+
const redirect = new URL(redirectUri);
|
|
2955
|
+
redirect.searchParams.set("code", code);
|
|
2956
|
+
if (state) redirect.searchParams.set("state", state);
|
|
2957
|
+
|
|
2958
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: authorize → code issued for ${clientId}, redirecting to ${redirect.origin}\n`;
|
|
2959
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
2960
|
+
res.writeHead(302, { Location: redirect.toString(), ...CORS });
|
|
2961
|
+
return res.end();
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
// ── Token endpoint ──────────────────────────────────────
|
|
2965
|
+
if (method === "POST" && reqPath === "/token") {
|
|
2966
|
+
let body;
|
|
2967
|
+
const ct = req.headers["content-type"] || "";
|
|
2968
|
+
try {
|
|
2969
|
+
if (ct.includes("application/json")) {
|
|
2970
|
+
body = await readBody(req);
|
|
2971
|
+
} else {
|
|
2972
|
+
const raw = await readRawBody(req);
|
|
2973
|
+
body = Object.fromEntries(new URLSearchParams(raw));
|
|
2974
|
+
}
|
|
2975
|
+
} catch {
|
|
2976
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
2977
|
+
return res.end(JSON.stringify({ error: "invalid_request" }));
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
if (body.grant_type !== "authorization_code") {
|
|
2981
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
2982
|
+
return res.end(JSON.stringify({ error: "unsupported_grant_type" }));
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
const stored = oauthCodes.get(body.code);
|
|
2986
|
+
if (!stored || stored.expires < Date.now()) {
|
|
2987
|
+
oauthCodes.delete(body.code);
|
|
2988
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
2989
|
+
return res.end(JSON.stringify({ error: "invalid_grant" }));
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
// PKCE verification
|
|
2993
|
+
if (stored.code_challenge && body.code_verifier) {
|
|
2994
|
+
const computed = sha256base64url(body.code_verifier);
|
|
2995
|
+
if (computed !== stored.code_challenge) {
|
|
2996
|
+
oauthCodes.delete(body.code);
|
|
2997
|
+
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
2998
|
+
return res.end(JSON.stringify({ error: "invalid_grant", error_description: "PKCE verification failed" }));
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
oauthCodes.delete(body.code);
|
|
3003
|
+
const accessToken = crypto.randomBytes(32).toString("hex");
|
|
3004
|
+
oauthTokens.add(accessToken);
|
|
3005
|
+
saveTokens(oauthTokens);
|
|
3006
|
+
|
|
3007
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: token issued for client ${stored.client_id} (token=${accessToken.slice(0,8)}…)\n`;
|
|
3008
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3009
|
+
res.writeHead(200, { "Content-Type": "application/json", ...CORS });
|
|
3010
|
+
return res.end(JSON.stringify({ access_token: accessToken, token_type: "Bearer", expires_in: 86400 }));
|
|
2894
3011
|
}
|
|
2895
3012
|
|
|
2896
3013
|
// ── MCP path helpers ──
|
|
@@ -2907,23 +3024,46 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2907
3024
|
return "clauth";
|
|
2908
3025
|
}
|
|
2909
3026
|
|
|
2910
|
-
// ── MCP endpoint auth
|
|
2911
|
-
//
|
|
2912
|
-
//
|
|
2913
|
-
if (method === "POST" && isMcpPath) {
|
|
3027
|
+
// ── MCP endpoint auth ──
|
|
3028
|
+
// Tunnel requests (Host = tunnel hostname) MUST have Bearer token → 401 triggers OAuth.
|
|
3029
|
+
// Local requests (Host = 127.0.0.1) pass through without auth.
|
|
3030
|
+
if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
|
|
2914
3031
|
const authHeader = req.headers.authorization;
|
|
3032
|
+
const host = (req.headers.host || "").split(":")[0];
|
|
3033
|
+
const isTunnelReq = tunnelUrl && host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
|
|
2915
3034
|
const authLogMsg = [
|
|
2916
3035
|
`[${new Date().toISOString()}] MCP POST ${reqPath}`,
|
|
3036
|
+
` Host: ${req.headers.host || "(none)"} (tunnel=${isTunnelReq})`,
|
|
2917
3037
|
` Authorization: ${authHeader ? (authHeader.startsWith("Bearer ") ? `Bearer ${authHeader.slice(7, 15)}… (known=${oauthTokens.has(authHeader.slice(7))})` : authHeader.slice(0, 30) + "…") : "(none)"}`,
|
|
2918
3038
|
` mcp-protocol-version: ${req.headers["mcp-protocol-version"] || "(not set)"}`,
|
|
2919
3039
|
` accept: ${req.headers["accept"] || "(not set)"}`,
|
|
2920
3040
|
].join("\n") + "\n";
|
|
2921
3041
|
try { fs.appendFileSync(LOG_FILE, authLogMsg); } catch {}
|
|
2922
|
-
|
|
2923
|
-
if (
|
|
3042
|
+
|
|
3043
|
+
if (isTunnelReq) {
|
|
3044
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
3045
|
+
const base = oauthBase();
|
|
3046
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: 401 → requiring auth for tunnel POST ${reqPath}\n`;
|
|
3047
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3048
|
+
res.writeHead(401, {
|
|
3049
|
+
"Content-Type": "application/json",
|
|
3050
|
+
"WWW-Authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`,
|
|
3051
|
+
...CORS,
|
|
3052
|
+
});
|
|
3053
|
+
return res.end(JSON.stringify({ error: "unauthorized" }));
|
|
3054
|
+
}
|
|
3055
|
+
const token = authHeader.slice(7);
|
|
3056
|
+
if (!oauthTokens.has(token)) {
|
|
3057
|
+
const logMsg = `[${new Date().toISOString()}] OAuth: REJECTED token ${token.slice(0,8)}… (pool=${oauthTokens.size})\n`;
|
|
3058
|
+
try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
|
|
3059
|
+
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3060
|
+
return res.end(JSON.stringify({ error: "invalid_token" }));
|
|
3061
|
+
}
|
|
3062
|
+
req._clauthRemote = true;
|
|
3063
|
+
} else if (authHeader?.startsWith("Bearer ") && oauthTokens.has(authHeader.slice(7))) {
|
|
2924
3064
|
req._clauthRemote = true;
|
|
2925
3065
|
}
|
|
2926
|
-
// fall through to MCP handling
|
|
3066
|
+
// fall through to MCP handling
|
|
2927
3067
|
}
|
|
2928
3068
|
|
|
2929
3069
|
// ── MCP Streamable HTTP transport (2025-03-26 spec) ──
|
|
@@ -2984,7 +3124,27 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
2984
3124
|
|
|
2985
3125
|
// ── MCP SSE transport — /sse and namespaced paths ────
|
|
2986
3126
|
// GET /sse|/gws|/clauth — open SSE stream, receive endpoint event
|
|
3127
|
+
// Tunnel requests require Bearer token (same as POST above)
|
|
2987
3128
|
if (method === "GET" && (reqPath === "/sse" || isMcpPath)) {
|
|
3129
|
+
const authHeader = req.headers.authorization;
|
|
3130
|
+
const host = (req.headers.host || "").split(":")[0];
|
|
3131
|
+
const isTunnelReq = tunnelUrl && host !== "127.0.0.1" && host !== "localhost" && host !== "::1";
|
|
3132
|
+
if (isTunnelReq) {
|
|
3133
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
3134
|
+
const base = oauthBase();
|
|
3135
|
+
res.writeHead(401, {
|
|
3136
|
+
"Content-Type": "application/json",
|
|
3137
|
+
"WWW-Authenticate": `Bearer resource_metadata="${base}/.well-known/oauth-protected-resource"`,
|
|
3138
|
+
...CORS,
|
|
3139
|
+
});
|
|
3140
|
+
return res.end(JSON.stringify({ error: "unauthorized" }));
|
|
3141
|
+
}
|
|
3142
|
+
if (!oauthTokens.has(authHeader.slice(7))) {
|
|
3143
|
+
res.writeHead(401, { "Content-Type": "application/json", ...CORS });
|
|
3144
|
+
return res.end(JSON.stringify({ error: "invalid_token" }));
|
|
3145
|
+
}
|
|
3146
|
+
// Token valid — mark as remote
|
|
3147
|
+
}
|
|
2988
3148
|
const sessionId = `ses_${++sseCounter}_${Date.now()}`;
|
|
2989
3149
|
|
|
2990
3150
|
res.writeHead(200, {
|