Files
Arcrun/mcp/src/oauth/consent.ts
T
Leo b8ca98cb49 MCP 認證改用 Portal 帳密,廢掉 MCP_OWNER_SECRET(leo 指定做法)
leo:「claude 裡有一個直接輸入帳密連線的,為什麼不用那個?跟他輸入 portal 的帳密一樣不就好了?」

為什麼換(三個封測撞出來的實際問題):
① 沒人給得了封測者——安裝器產生後從不顯示(完成頁 grep「Owner 祕密」=0),
   CF secret 又唯寫讀不回 ⇒ 用戶卡在同意頁,只能找 leo 手動 wrangler 覆寫
② 多一把要記的金鑰——違反「拿一把金鑰就很難了」
③ 全實例共用一把,無法分辨誰連上來(企業多人版必要)

風險評估(leo 判斷,總管原本誇大成「繞過帳密的旁路」已認錯):
secret 要貼進 claude.ai(本身有帳密保護)⇒ 洩漏 secret 與洩漏 portal 帳密風險相同。

改動:
- consent.ts:一個「Owner 祕密」欄位 → email + password 兩欄
- routes.ts POST /authorize:constantTimeEqual(MCP_OWNER_SECRET)
  → 走 CYPHER_EXECUTOR binding 打 /portal/login(認證下沉到唯一真相源,
    同樣吃它的節流與停用檢查)
- routes.ts GET /authorize:移除「未設 MCP_OWNER_SECRET → 503」
  (那是「每個封測者都死在這頁」的直接原因)

驗:tsc 零錯誤;已部署 youlin。
2026-07-31 00:44:15 +08:00

91 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// /authorize 同意頁:極簡單檔 HTML,要求輸入 owner 祕密才發碼。零外部資源。
// 所有反射進 HTML 的 OAuth 參數都 escape,防 XSSredirect_uri / state / client_id 由外部帶入)。
/** HTML attribute / text 跳脫。 */
export function esc(s: string): string {
return s
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
/** 同意頁需要 round-trip 回 POST /authorize 的隱藏欄位。 */
export interface ConsentParams {
client_id: string;
redirect_uri: string;
state: string;
code_challenge: string;
code_challenge_method: string;
scope: string;
resource: string;
}
function hidden(name: string, value: string): string {
return `<input type="hidden" name="${esc(name)}" value="${esc(value)}">`;
}
/**
* 同意頁 HTML。`error` 有值時(如祕密錯誤)顯示紅字,但仍保留隱藏欄位讓 owner 重試。
*/
export function consentPage(p: ConsentParams, error?: string): string {
const fields = [
hidden("client_id", p.client_id),
hidden("redirect_uri", p.redirect_uri),
hidden("state", p.state),
hidden("code_challenge", p.code_challenge),
hidden("code_challenge_method", p.code_challenge_method),
hidden("scope", p.scope),
hidden("resource", p.resource),
].join("\n ");
const errBlock = error
? `<p class="err" role="alert">${esc(error)}</p>`
: "";
return `<!doctype html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Arcrun MCP 授權</title>
<style>
:root { color-scheme: light dark; }
body { font-family: -apple-system, "Segoe UI", system-ui, sans-serif; max-width: 26rem;
margin: 4rem auto; padding: 0 1.25rem; line-height: 1.6; }
h1 { font-size: 1.25rem; }
p.desc { color: #666; font-size: .95rem; }
code { background: rgba(127,127,127,.15); padding: .1rem .35rem; border-radius: .25rem;
font-size: .85rem; word-break: break-all; }
label { display: block; margin: 1.25rem 0 .35rem; font-weight: 600; }
input[type=password], input[type=email] { width: 100%; padding: .6rem .7rem; font-size: 1rem;
border: 1px solid #8888; border-radius: .5rem; box-sizing: border-box; }
button { margin-top: 1.25rem; width: 100%; padding: .7rem; font-size: 1rem; font-weight: 600;
border: 0; border-radius: .5rem; background: #8a5f1e; color: #fff; cursor: pointer; }
button:hover { background: #6f4c18; }
p.err { color: #c0392b; font-weight: 600; }
p.foot { color: #999; font-size: .8rem; margin-top: 2rem; }
</style>
</head>
<body>
<h1>Arcrun MCP 授權</h1>
<p class="desc">應用程式 <code>${esc(p.client_id)}</code> 想連上你的 Arcrun MCP
這會讓它能<strong>讀寫你的 KBDB 全部資料</strong>。</p>
${errBlock}
<form method="POST" action="/authorize">
${fields}
<label for="email">你的 Portal 帳號(email</label>
<input id="email" name="email" type="email" autocomplete="username"
autofocus required placeholder="你登入知識庫用的 email">
<label for="password">Portal 密碼</label>
<input id="password" name="password" type="password" autocomplete="current-password"
required placeholder="你登入知識庫用的密碼">
<button type="submit">授權連線</button>
</form>
<p class="foot">用你登入知識庫(Portal)的同一組帳密,**不需要另外的祕密**。
帳密不正確不會發出授權碼。此頁不儲存你的輸入。</p>
</body>
</html>`;
}