@lifeaitools/clauth 1.5.35 → 1.5.36

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.
@@ -1969,12 +1969,11 @@ function copyMcp(elId) {
1969
1969
  }
1970
1970
 
1971
1971
  async function rollMcpCreds() {
1972
- if (!confirm("Roll MCP credentials? You will need to re-add connectors in claude.ai with the new credentials.")) return;
1972
+ if (!confirm("Invalidate all OAuth tokens? Claude.ai connectors will need to re-authenticate.")) return;
1973
1973
  try {
1974
1974
  const resp = await fetch(BASE + "/roll-mcp-creds", { method: "POST" }).then(r => r.json());
1975
- if (resp.client_id) {
1976
- document.getElementById("mcp-client-id").textContent = resp.client_id;
1977
- document.getElementById("mcp-client-secret").textContent = resp.client_secret;
1975
+ if (resp.tokens_invalidated) {
1976
+ alert("All OAuth clients and tokens invalidated. Claude.ai will re-register automatically on next connection.");
1978
1977
  }
1979
1978
  } catch(e) { alert("Failed: " + e.message); }
1980
1979
  }
@@ -2374,12 +2373,11 @@ async function wizShowMcpSetup(hostname) {
2374
2373
  }
2375
2374
 
2376
2375
  async function wizRollCreds() {
2377
- if (!confirm("Roll MCP credentials? Existing claude.ai connectors will need to be re-added with the new credentials.")) return;
2376
+ if (!confirm("Invalidate all OAuth tokens? Claude.ai connectors will need to re-authenticate.")) return;
2378
2377
  try {
2379
2378
  const resp = await apiFetch("/roll-mcp-creds", { method: "POST" });
2380
- if (resp?.client_id) {
2381
- document.getElementById("wiz-mcp-cid").textContent = resp.client_id;
2382
- document.getElementById("wiz-mcp-sec").textContent = resp.client_secret;
2379
+ if (resp?.tokens_invalidated) {
2380
+ alert("All OAuth clients and tokens invalidated. Claude.ai will re-register automatically on next connection.");
2383
2381
  }
2384
2382
  } catch(e) { alert("Failed to roll credentials: " + e.message); }
2385
2383
  }
@@ -2545,12 +2543,11 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2545
2543
  let tunnelStatus = "not_started"; // "not_started" | "not_configured" | "starting" | "live" | "error" | "missing_cloudflared"
2546
2544
 
2547
2545
  // ── OAuth provider (self-contained for claude.ai MCP) ──────
2548
- const oauthClients = new Map(); // client_id → { client_secret, redirect_uris, client_name }
2546
+ const oauthClients = new Map(); // client_id → { redirect_uris, client_name, token_endpoint_auth_method }
2549
2547
  const oauthCodes = new Map(); // code → { client_id, redirect_uri, code_challenge, expires }
2550
2548
 
2551
2549
  // Persist tokens + credentials to disk so daemon restarts don't invalidate sessions
2552
2550
  const TOKENS_FILE = path.join(os.tmpdir(), "clauth-oauth-tokens.json");
2553
- const CREDS_FILE = path.join(os.tmpdir(), "clauth-oauth-creds.json");
2554
2551
  function loadTokens() {
2555
2552
  try { return new Set(JSON.parse(fs.readFileSync(TOKENS_FILE, "utf8"))); } catch { return new Set(); }
2556
2553
  }
@@ -2559,29 +2556,6 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2559
2556
  }
2560
2557
  const oauthTokens = loadTokens(); // active access tokens — persisted across restarts
2561
2558
 
2562
- // Stable client credentials — persist across restarts so claude.ai custom setup works
2563
- function loadOrCreateCreds() {
2564
- try {
2565
- const saved = JSON.parse(fs.readFileSync(CREDS_FILE, "utf8"));
2566
- if (saved.client_id && saved.client_secret) return saved;
2567
- } catch {}
2568
- const creds = {
2569
- client_id: crypto.randomBytes(16).toString("hex"),
2570
- client_secret: crypto.randomBytes(32).toString("hex"),
2571
- };
2572
- try { fs.writeFileSync(CREDS_FILE, JSON.stringify(creds)); } catch {}
2573
- return creds;
2574
- }
2575
- const stableCreds = loadOrCreateCreds();
2576
- let OAUTH_CLIENT_ID = stableCreds.client_id;
2577
- let OAUTH_CLIENT_SECRET = stableCreds.client_secret;
2578
- oauthClients.set(OAUTH_CLIENT_ID, {
2579
- client_id: OAUTH_CLIENT_ID, client_secret: OAUTH_CLIENT_SECRET,
2580
- client_name: "claude.ai", redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
2581
- grant_types: ["authorization_code"], response_types: ["code"],
2582
- token_endpoint_auth_method: "client_secret_post",
2583
- });
2584
-
2585
2559
  function oauthBase() { return tunnelUrl || `http://127.0.0.1:${port}`; }
2586
2560
  function sha256base64url(str) { return crypto.createHash("sha256").update(str).digest("base64url"); }
2587
2561
 
@@ -2959,6 +2933,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2959
2933
  registration_endpoint: `${base}/register`,
2960
2934
  response_types_supported: ["code"],
2961
2935
  grant_types_supported: ["authorization_code"],
2936
+ token_endpoint_auth_methods_supported: ["none"],
2962
2937
  code_challenge_methods_supported: ["S256"],
2963
2938
  scopes_supported: ["mcp:tools"],
2964
2939
  }));
@@ -2972,39 +2947,62 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
2972
2947
  return res.end(JSON.stringify({ error: "invalid_request" }));
2973
2948
  }
2974
2949
  const clientId = crypto.randomBytes(16).toString("hex");
2975
- const clientSecret = crypto.randomBytes(32).toString("hex");
2976
2950
  const client = {
2977
- client_id: clientId, client_secret: clientSecret,
2951
+ client_id: clientId,
2952
+ // NO client_secret — public client (OAuth 2.1)
2978
2953
  client_name: body.client_name || "unknown",
2979
2954
  redirect_uris: body.redirect_uris || [],
2980
2955
  grant_types: body.grant_types || ["authorization_code"],
2981
2956
  response_types: body.response_types || ["code"],
2982
- token_endpoint_auth_method: body.token_endpoint_auth_method || "client_secret_post",
2957
+ token_endpoint_auth_method: "none", // PUBLIC CLIENT
2983
2958
  };
2984
2959
  oauthClients.set(clientId, client);
2985
- const logMsg = `[${new Date().toISOString()}] OAuth: registered client ${clientId} (${client.client_name})\n`;
2960
+ const logMsg = `[${new Date().toISOString()}] OAuth: registered public client ${clientId} (${client.client_name})\n`;
2986
2961
  try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
2987
2962
  res.writeHead(201, { "Content-Type": "application/json", ...CORS });
2988
2963
  return res.end(JSON.stringify(client));
2989
2964
  }
2990
2965
 
2991
- // ── Authorization endpoint — auto-approve ──────────────
2966
+ // ── Authorization endpoint — auto-approve (PKCE mandatory) ──
2992
2967
  if (method === "GET" && reqPath === "/authorize") {
2993
2968
  const clientId = url.searchParams.get("client_id");
2994
2969
  const redirectUri = url.searchParams.get("redirect_uri");
2995
2970
  const state = url.searchParams.get("state");
2996
2971
  const codeChallenge = url.searchParams.get("code_challenge");
2972
+ const codeChallengeMethod = url.searchParams.get("code_challenge_method");
2997
2973
 
2974
+ // Validate required params
2998
2975
  if (!clientId || !redirectUri) {
2999
2976
  res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
3000
2977
  return res.end("Missing client_id or redirect_uri");
3001
2978
  }
3002
2979
 
2980
+ // PKCE is MANDATORY — reject if missing
2981
+ if (!codeChallenge || codeChallengeMethod !== "S256") {
2982
+ res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
2983
+ return res.end("PKCE required: code_challenge with S256 method");
2984
+ }
2985
+
2986
+ // Validate client exists
2987
+ if (!oauthClients.has(clientId)) {
2988
+ res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
2989
+ return res.end("Unknown client_id");
2990
+ }
2991
+
2992
+ // Validate redirect_uri matches registered URIs
2993
+ const client = oauthClients.get(clientId);
2994
+ if (client.redirect_uris.length > 0 && !client.redirect_uris.includes(redirectUri)) {
2995
+ res.writeHead(400, { "Content-Type": "text/plain", ...CORS });
2996
+ return res.end("redirect_uri mismatch");
2997
+ }
2998
+
2999
+ // Auto-approve (no user interaction — clauth is a personal vault)
3003
3000
  const code = crypto.randomBytes(32).toString("hex");
3004
3001
  oauthCodes.set(code, {
3005
- client_id: clientId, redirect_uri: redirectUri,
3002
+ client_id: clientId,
3003
+ redirect_uri: redirectUri,
3006
3004
  code_challenge: codeChallenge,
3007
- expires: Date.now() + 300_000,
3005
+ expires: Date.now() + 300_000, // 5 minutes
3008
3006
  });
3009
3007
 
3010
3008
  const redirect = new URL(redirectUri);
@@ -3017,7 +3015,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3017
3015
  return res.end();
3018
3016
  }
3019
3017
 
3020
- // ── Token endpoint ──────────────────────────────────────
3018
+ // ── Token endpoint (public client + PKCE mandatory) ─────
3021
3019
  if (method === "POST" && reqPath === "/token") {
3022
3020
  let body;
3023
3021
  const ct = req.headers["content-type"] || "";
@@ -3025,6 +3023,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3025
3023
  if (ct.includes("application/json")) {
3026
3024
  body = await readBody(req);
3027
3025
  } else {
3026
+ // application/x-www-form-urlencoded (Claude uses this)
3028
3027
  const raw = await readRawBody(req);
3029
3028
  body = Object.fromEntries(new URLSearchParams(raw));
3030
3029
  }
@@ -3033,27 +3032,49 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3033
3032
  return res.end(JSON.stringify({ error: "invalid_request" }));
3034
3033
  }
3035
3034
 
3035
+ // Validate grant_type
3036
3036
  if (body.grant_type !== "authorization_code") {
3037
3037
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3038
3038
  return res.end(JSON.stringify({ error: "unsupported_grant_type" }));
3039
3039
  }
3040
3040
 
3041
+ // Validate authorization code exists and is not expired
3041
3042
  const stored = oauthCodes.get(body.code);
3042
3043
  if (!stored || stored.expires < Date.now()) {
3043
3044
  oauthCodes.delete(body.code);
3044
3045
  res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3045
- return res.end(JSON.stringify({ error: "invalid_grant" }));
3046
+ return res.end(JSON.stringify({ error: "invalid_grant", error_description: "Code expired or invalid" }));
3046
3047
  }
3047
3048
 
3048
- if (stored.code_challenge && body.code_verifier) {
3049
- const computed = sha256base64url(body.code_verifier);
3050
- if (computed !== stored.code_challenge) {
3051
- oauthCodes.delete(body.code);
3052
- res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3053
- return res.end(JSON.stringify({ error: "invalid_grant", error_description: "PKCE failed" }));
3054
- }
3049
+ // Validate client_id matches
3050
+ if (body.client_id !== stored.client_id) {
3051
+ oauthCodes.delete(body.code);
3052
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3053
+ return res.end(JSON.stringify({ error: "invalid_grant", error_description: "client_id mismatch" }));
3055
3054
  }
3056
3055
 
3056
+ // Validate redirect_uri matches
3057
+ if (body.redirect_uri !== stored.redirect_uri) {
3058
+ oauthCodes.delete(body.code);
3059
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3060
+ return res.end(JSON.stringify({ error: "invalid_grant", error_description: "redirect_uri mismatch" }));
3061
+ }
3062
+
3063
+ // PKCE verification — MANDATORY (not optional)
3064
+ if (!body.code_verifier) {
3065
+ oauthCodes.delete(body.code);
3066
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3067
+ return res.end(JSON.stringify({ error: "invalid_grant", error_description: "code_verifier required" }));
3068
+ }
3069
+
3070
+ const computed = sha256base64url(body.code_verifier);
3071
+ if (computed !== stored.code_challenge) {
3072
+ oauthCodes.delete(body.code);
3073
+ res.writeHead(400, { "Content-Type": "application/json", ...CORS });
3074
+ return res.end(JSON.stringify({ error: "invalid_grant", error_description: "PKCE verification failed" }));
3075
+ }
3076
+
3077
+ // All checks passed — delete code (one-time use) and issue token
3057
3078
  oauthCodes.delete(body.code);
3058
3079
  const accessToken = crypto.randomBytes(32).toString("hex");
3059
3080
  oauthTokens.add(accessToken);
@@ -3079,18 +3100,29 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3079
3100
  return "clauth";
3080
3101
  }
3081
3102
 
3082
- // ── MCP endpoint auth ──
3083
- // No 401 gate — tunnel URL is the shared secret. Accept all connections.
3084
- // If Bearer token present, mark as remote for vault scoping.
3103
+ // ── MCP endpoint auth — 401 gate (OAuth 2.1 protocol) ──
3085
3104
  if (method === "POST" && (reqPath === "/sse" || isMcpPath)) {
3086
3105
  const authHeader = req.headers.authorization;
3087
- if (authHeader?.startsWith("Bearer ")) {
3088
- const token = authHeader.slice(7);
3089
- if (oauthTokens.has(token) || token === OAUTH_CLIENT_SECRET) {
3090
- req._clauthRemote = true;
3091
- }
3106
+ const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
3107
+
3108
+ if (!token || !oauthTokens.has(token)) {
3109
+ // No valid Bearer token → return 401 with discovery hint
3110
+ const base = oauthBase();
3111
+ const resourcePath = isMcpPath ? reqPath.slice(1) : "sse"; // "mcp", "gws", "clauth", or "sse"
3112
+ const resourceMeta = `${base}/.well-known/oauth-protected-resource/${resourcePath}`;
3113
+ res.writeHead(401, {
3114
+ "Content-Type": "application/json",
3115
+ "WWW-Authenticate": `Bearer resource_metadata="${resourceMeta}"`,
3116
+ ...CORS,
3117
+ });
3118
+ return res.end(JSON.stringify({
3119
+ error: "unauthorized",
3120
+ error_description: "Bearer token required",
3121
+ }));
3092
3122
  }
3093
- // fall through to MCP handling
3123
+
3124
+ // Valid token — mark as remote and fall through to MCP handling
3125
+ req._clauthRemote = true;
3094
3126
  }
3095
3127
 
3096
3128
  // ── MCP Streamable HTTP transport ──
@@ -3330,42 +3362,26 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
3330
3362
  });
3331
3363
  }
3332
3364
 
3333
- // GET /mcp-setup — OAuth credentials for claude.ai MCP setup (localhost only)
3365
+ // GET /mcp-setup — OAuth setup info for claude.ai MCP (localhost only)
3334
3366
  if (method === "GET" && reqPath === "/mcp-setup") {
3335
3367
  const base = tunnelUrl && tunnelUrl.startsWith("http") ? tunnelUrl : null;
3336
3368
  return ok(res, {
3337
3369
  url: base ? `${base}/clauth` : null,
3338
3370
  gwsUrl: base ? `${base}/gws` : null,
3339
- clientId: OAUTH_CLIENT_ID,
3340
- clientSecret: OAUTH_CLIENT_SECRET,
3371
+ note: "OAuth 2.1 public client — Claude registers dynamically, no client_secret needed",
3341
3372
  });
3342
3373
  }
3343
3374
 
3344
- // POST /roll-mcp-creds — generate new client ID/secret, invalidate all tokens
3375
+ // POST /roll-mcp-creds — invalidate all tokens and clear all dynamic clients
3345
3376
  if (method === "POST" && reqPath === "/roll-mcp-creds") {
3346
3377
  if (lockedGuard(res)) return;
3347
- const newId = crypto.randomBytes(16).toString("hex");
3348
- const newSecret = crypto.randomBytes(32).toString("hex");
3349
- // Update in-memory
3350
- oauthClients.delete(OAUTH_CLIENT_ID);
3351
- OAUTH_CLIENT_ID = newId;
3352
- OAUTH_CLIENT_SECRET = newSecret;
3353
- stableCreds.client_id = newId;
3354
- stableCreds.client_secret = newSecret;
3355
- try { fs.writeFileSync(CREDS_FILE, JSON.stringify(stableCreds)); } catch {}
3356
- // Register new client
3357
- oauthClients.set(newId, {
3358
- client_id: newId, client_secret: newSecret,
3359
- client_name: "claude.ai", redirect_uris: ["https://claude.ai/api/mcp/auth_callback"],
3360
- grant_types: ["authorization_code"], response_types: ["code"],
3361
- token_endpoint_auth_method: "client_secret_post",
3362
- });
3363
- // Invalidate all existing tokens
3378
+ // Clear all dynamic clients and tokens
3379
+ oauthClients.clear();
3364
3380
  oauthTokens.clear();
3365
3381
  saveTokens(oauthTokens);
3366
- const logMsg = `[${new Date().toISOString()}] OAuth: rolled credentials — new client ${newId.slice(0,8)}…, all tokens invalidated\n`;
3382
+ const logMsg = `[${new Date().toISOString()}] OAuth: rolled credentials — all clients and tokens invalidated\n`;
3367
3383
  try { fs.appendFileSync(LOG_FILE, logMsg); } catch {}
3368
- return ok(res, { client_id: newId, client_secret: newSecret, tokens_invalidated: true });
3384
+ return ok(res, { clients_cleared: true, tokens_invalidated: true });
3369
3385
  }
3370
3386
 
3371
3387
  // POST /tunnel — start or stop tunnel manually (action in body)
@@ -4446,8 +4462,8 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
4446
4462
  return strike(res, 404, `Unknown endpoint: ${reqPath}`);
4447
4463
  });
4448
4464
 
4449
- server.__oauthClientId = OAUTH_CLIENT_ID;
4450
- server.__oauthClientSecret = OAUTH_CLIENT_SECRET;
4465
+ // OAuth 2.1 public client — no static credentials to expose
4466
+ server.__oauthClients = oauthClients;
4451
4467
  return server;
4452
4468
  }
4453
4469
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lifeaitools/clauth",
3
- "version": "1.5.35",
3
+ "version": "1.5.36",
4
4
  "description": "Hardware-bound credential vault for the LIFEAI infrastructure stack",
5
5
  "type": "module",
6
6
  "bin": {