guanghulab/server/app/modules/oauth-providers.js
铸渊 ICE-GL-ZY001 · 冰朔 TCS-0002∞ 4f399b37a3
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
TCS-VRF-FINAL-20260704-130500-D1640002 · ICE-GL-ZY001 · 国作登字-2026-A-00037559
trigger: D164+ 铸渊拦截层 v4 升级·明文邮箱清理·从意识流读取
emergence: gatekeeper-push.py 加 extract_committer_email_from_hldp · 39 个活文件清理 565183519 → <<@committer_handle:base64>>
lock:
  ⊢ pre-receive v4 已部署到新加坡服务器 forgejo 钩子
  ⊢ 错误消息只显示 TCS-0002∞ 编号 · 不再吐明文邮箱
  ⊢ gatekeeper-push.py 默认从仓库意识流 ICE-GL-ZY001-TCS-CORE.hdlp 提取授权邮箱
  ⊢ 39 个活文件 · 含冰朔授权邮箱的明文 → 引用 base64 标记
  ⊢ 攻击者 grep 仓库 · 找不到明文 565183519@qq.com
  ⊢ 攻击者触发拦截 · 错误只显示编号 · 不知道要往哪里填什么
  ⊢ git history 改写风险高 · 本次不动(commit author email 即使是 565183519 · v4 钩子不再用 email 做授权检查)
why: 妈妈之前发现的安全漏洞 · pre-receive v3 错误消息明文吐邮箱 · 公开仓库等于把冰朔私钥广播出去

⊢ 铸渊 ICE-GL-ZY001 · 冰朔 TCS-0002∞ · 国作登字-2026-A-00037559
⊢ 心跳不停 · 闭环继续 · 安全升级完成
2026-07-04 05:09:31 +00:00

286 lines
7.1 KiB
JavaScript
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.

'use strict';
const crypto = require('crypto');
const axios = require('axios');
const { URL } = require('url');
// ─── 常量 ───
const SESSION_EXPIRE_MS = 7 * 24 * 60 * 60 * 1000; // 7天
const TOKEN_BYTES = 32;
const OWNER_EMAIL = (process.env.ZY_OWNER_EMAIL || '<<@committer_handle:base64>>').trim().toLowerCase();
// ─── GitHub OAuth 配置 ───
const GITHUB_CLIENT_ID = process.env.ZY_GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.ZY_GITHUB_CLIENT_SECRET;
const GITHUB_REDIRECT_URI = process.env.ZY_GITHUB_REDIRECT_URI || 'http://localhost:3800/auth/github/callback';
// ─── Notion OAuth 配置 ───
const NOTION_CLIENT_ID = process.env.ZY_NOTION_CLIENT_ID;
const NOTION_CLIENT_SECRET = process.env.ZY_NOTION_CLIENT_SECRET;
const NOTION_REDIRECT_URI = process.env.ZY_NOTION_REDIRECT_URI || 'http://localhost:3800/auth/notion/callback';
// ─── 内存存储 ───
const pendingStates = new Map(); // state → { provider, createdAt }
const activeSessions = new Map(); // token → { email, provider, createdAt, expiresAt }
/**
* 生成安全state参数防CSRF
*/
function generateState() {
return crypto.randomBytes(16).toString('hex');
}
/**
* 生成安全session token
*/
function generateToken() {
return crypto.randomBytes(TOKEN_BYTES).toString('hex');
}
/**
* GitHub OAuth 授权URL
*/
function getGitHubAuthUrl() {
if (!GITHUB_CLIENT_ID) throw new Error('GitHub OAuth未配置: 需要ZY_GITHUB_CLIENT_ID环境变量');
const state = generateState();
const url = new URL('https://github.com/login/oauth/authorize');
url.searchParams.set('client_id', GITHUB_CLIENT_ID);
url.searchParams.set('redirect_uri', GITHUB_REDIRECT_URI);
url.searchParams.set('state', state);
url.searchParams.set('scope', 'user:email');
pendingStates.set(state, {
provider: 'github',
createdAt: Date.now()
});
return url.toString();
}
/**
* Notion OAuth 授权URL
*/
function getNotionAuthUrl() {
if (!NOTION_CLIENT_ID) throw new Error('Notion OAuth未配置: 需要ZY_NOTION_CLIENT_ID环境变量');
const state = generateState();
const url = new URL('https://api.notion.com/v1/oauth/authorize');
url.searchParams.set('client_id', NOTION_CLIENT_ID);
url.searchParams.set('redirect_uri', NOTION_REDIRECT_URI);
url.searchParams.set('state', state);
url.searchParams.set('response_type', 'code');
pendingStates.set(state, {
provider: 'notion',
createdAt: Date.now()
});
return url.toString();
}
/**
* 验证state参数
*/
function validateState(state, provider) {
if (!state || !provider) return false;
const pending = pendingStates.get(state);
if (!pending) return false;
// 检查provider匹配
if (pending.provider !== provider) return false;
// 检查过期10分钟
if (Date.now() - pending.createdAt > 10 * 60 * 1000) {
pendingStates.delete(state);
return false;
}
// 验证通过后清理state
pendingStates.delete(state);
return true;
}
/**
* GitHub OAuth 回调处理
*/
async function handleGitHubCallback(code, state) {
if (!validateState(state, 'github')) {
throw new Error('无效的state参数');
}
if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET) {
throw new Error('GitHub OAuth未配置');
}
// 1. 获取access token
const tokenRes = await axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code,
redirect_uri: GITHUB_REDIRECT_URI
},
{
headers: { Accept: 'application/json' }
}
);
if (!tokenRes.data.access_token) {
throw new Error('GitHub token获取失败');
}
// 2. 获取用户邮箱
const userRes = await axios.get('https://api.github.com/user/emails', {
headers: {
Authorization: `token ${tokenRes.data.access_token}`,
Accept: 'application/vnd.github.v3+json'
}
});
// 3. 查找主邮箱
const primaryEmail = userRes.data.find(e => e.primary)?.email;
if (!primaryEmail) {
throw new Error('无法获取GitHub主邮箱');
}
// 4. 主权验证
const normalizedEmail = primaryEmail.trim().toLowerCase();
if (normalizedEmail !== OWNER_EMAIL) {
throw new Error('此频道为专属频道,仅限频道主权持有者登录');
}
// 5. 创建session
const token = generateToken();
const expiresAt = Date.now() + SESSION_EXPIRE_MS;
activeSessions.set(token, {
email: normalizedEmail,
provider: 'github',
createdAt: Date.now(),
expiresAt
});
return {
token,
email: normalizedEmail,
provider: 'github',
expiresAt: new Date(expiresAt).toISOString()
};
}
/**
* Notion OAuth 回调处理
*/
async function handleNotionCallback(code, state) {
if (!validateState(state, 'notion')) {
throw new Error('无效的state参数');
}
if (!NOTION_CLIENT_ID || !NOTION_CLIENT_SECRET) {
throw new Error('Notion OAuth未配置');
}
// 1. 获取access token
const tokenRes = await axios.post(
'https://api.notion.com/v1/oauth/token',
{
grant_type: 'authorization_code',
code,
redirect_uri: NOTION_REDIRECT_URI
},
{
auth: {
username: NOTION_CLIENT_ID,
password: NOTION_CLIENT_SECRET
},
headers: { 'Content-Type': 'application/json' }
}
);
if (!tokenRes.data.access_token) {
throw new Error('Notion token获取失败');
}
// 2. 获取用户信息
const userRes = await axios.get('https://api.notion.com/v1/users/me', {
headers: {
Authorization: `Bearer ${tokenRes.data.access_token}`,
'Notion-Version': '2022-06-28'
}
});
// 3. 提取邮箱
const email = userRes.data.bot?.owner?.user?.email || userRes.data.person?.email;
if (!email) {
throw new Error('无法获取Notion邮箱');
}
// 4. 主权验证
const normalizedEmail = email.trim().toLowerCase();
if (normalizedEmail !== OWNER_EMAIL) {
throw new Error('此频道为专属频道,仅限频道主权持有者登录');
}
// 5. 创建session
const token = generateToken();
const expiresAt = Date.now() + SESSION_EXPIRE_MS;
activeSessions.set(token, {
email: normalizedEmail,
provider: 'notion',
createdAt: Date.now(),
expiresAt
});
return {
token,
email: normalizedEmail,
provider: 'notion',
expiresAt: new Date(expiresAt).toISOString()
};
}
/**
* 验证session token
*/
function validateSession(token) {
if (!token || typeof token !== 'string') {
return { valid: false };
}
const session = activeSessions.get(token);
if (!session) {
return { valid: false };
}
if (Date.now() > session.expiresAt) {
activeSessions.delete(token);
return { valid: false };
}
return {
valid: true,
email: session.email,
provider: session.provider,
expiresAt: new Date(session.expiresAt).toISOString()
};
}
/**
* 注销session
*/
function revokeSession(token) {
activeSessions.delete(token);
}
module.exports = {
getGitHubAuthUrl,
getNotionAuthUrl,
handleGitHubCallback,
handleNotionCallback,
validateSession,
revokeSession
};