[GLADA] GLADA-CAB-20260415-001 step2 1. 创建oauth-providers.js模块,实现GitHub和Notion的OAuth流程_2. 在server.js中添加对应的API路由_3. 更新package.json的scripts部分以支持OAuth测试_4. 所有OAuth流程严格验证主权者邮箱(565183519@qq.com)_5. 使用与验证码登录相同的session token机制保持一致性
This commit is contained in:
parent
5cc48c9f74
commit
ec89bb0b5d
148
server/app/modules/oauth-providers.js
Normal file
148
server/app/modules/oauth-providers.js
Normal file
@ -0,0 +1,148 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fetch = require('node-fetch');
|
||||
const { URLSearchParams } = require('url');
|
||||
|
||||
// ─── 常量 ───
|
||||
const OWNER_EMAIL = (process.env.ZY_OWNER_EMAIL || '565183519@qq.com').trim().toLowerCase();
|
||||
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
|
||||
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
|
||||
const GITHUB_REDIRECT_URI = process.env.GITHUB_REDIRECT_URI || 'http://localhost:3800/api/oauth/github/callback';
|
||||
const NOTION_CLIENT_ID = process.env.NOTION_CLIENT_ID;
|
||||
const NOTION_CLIENT_SECRET = process.env.NOTION_CLIENT_SECRET;
|
||||
const NOTION_REDIRECT_URI = process.env.NOTION_REDIRECT_URI || 'http://localhost:3800/api/oauth/notion/callback';
|
||||
const STATE_SECRET = process.env.OAUTH_STATE_SECRET || 'default-secret-change-me';
|
||||
|
||||
// ─── 状态令牌生成 ───
|
||||
function generateStateToken(email) {
|
||||
const hash = crypto.createHmac('sha256', STATE_SECRET)
|
||||
.update(email)
|
||||
.digest('hex');
|
||||
return `${hash}:${Date.now()}`;
|
||||
}
|
||||
|
||||
function verifyStateToken(token, email) {
|
||||
const [hash, timestamp] = token.split(':');
|
||||
if (!hash || !timestamp) return false;
|
||||
|
||||
// 状态令牌10分钟过期
|
||||
if (Date.now() - Number(timestamp) > 10 * 60 * 1000) return false;
|
||||
|
||||
const expectedHash = crypto.createHmac('sha256', STATE_SECRET)
|
||||
.update(email)
|
||||
.digest('hex');
|
||||
|
||||
return hash === expectedHash;
|
||||
}
|
||||
|
||||
// ─── GitHub OAuth ───
|
||||
async function getGitHubOAuthUrl(email) {
|
||||
if (email.toLowerCase() !== OWNER_EMAIL) {
|
||||
throw new Error('仅限主权者邮箱使用');
|
||||
}
|
||||
|
||||
const state = generateStateToken(email);
|
||||
const params = new URLSearchParams({
|
||||
client_id: GITHUB_CLIENT_ID,
|
||||
redirect_uri: GITHUB_REDIRECT_URI,
|
||||
scope: 'user:email',
|
||||
state
|
||||
});
|
||||
|
||||
return `https://github.com/login/oauth/authorize?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function handleGitHubCallback(code, state) {
|
||||
if (!verifyStateToken(state, OWNER_EMAIL)) {
|
||||
throw new Error('无效的状态令牌');
|
||||
}
|
||||
|
||||
// 交换access token
|
||||
const tokenParams = new URLSearchParams({
|
||||
client_id: GITHUB_CLIENT_ID,
|
||||
client_secret: GITHUB_CLIENT_SECRET,
|
||||
code,
|
||||
redirect_uri: GITHUB_REDIRECT_URI
|
||||
});
|
||||
|
||||
const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: tokenParams
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
if (tokenData.error) {
|
||||
throw new Error(`GitHub OAuth错误: ${tokenData.error_description || tokenData.error}`);
|
||||
}
|
||||
|
||||
// 返回与验证码登录相同的session token结构
|
||||
return {
|
||||
token: crypto.randomBytes(32).toString('hex'),
|
||||
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7天
|
||||
provider: 'github',
|
||||
email: OWNER_EMAIL
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Notion OAuth ───
|
||||
async function getNotionOAuthUrl(email) {
|
||||
if (email.toLowerCase() !== OWNER_EMAIL) {
|
||||
throw new Error('仅限主权者邮箱使用');
|
||||
}
|
||||
|
||||
const state = generateStateToken(email);
|
||||
const params = new URLSearchParams({
|
||||
client_id: NOTION_CLIENT_ID,
|
||||
redirect_uri: NOTION_REDIRECT_URI,
|
||||
response_type: 'code',
|
||||
owner: 'user',
|
||||
state
|
||||
});
|
||||
|
||||
return `https://api.notion.com/v1/oauth/authorize?${params.toString()}`;
|
||||
}
|
||||
|
||||
async function handleNotionCallback(code, state) {
|
||||
if (!verifyStateToken(state, OWNER_EMAIL)) {
|
||||
throw new Error('无效的状态令牌');
|
||||
}
|
||||
|
||||
// 交换access token
|
||||
const tokenRes = await fetch('https://api.notion.com/v1/oauth/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Basic ${Buffer.from(`${NOTION_CLIENT_ID}:${NOTION_CLIENT_SECRET}`).toString('base64')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: NOTION_REDIRECT_URI
|
||||
})
|
||||
});
|
||||
|
||||
const tokenData = await tokenRes.json();
|
||||
if (tokenData.error) {
|
||||
throw new Error(`Notion OAuth错误: ${tokenData.error_description || tokenData.error}`);
|
||||
}
|
||||
|
||||
// 返回与验证码登录相同的session token结构
|
||||
return {
|
||||
token: crypto.randomBytes(32).toString('hex'),
|
||||
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000, // 7天
|
||||
provider: 'notion',
|
||||
email: OWNER_EMAIL
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGitHubOAuthUrl,
|
||||
handleGitHubCallback,
|
||||
getNotionOAuthUrl,
|
||||
handleNotionCallback
|
||||
};
|
||||
@ -6,7 +6,16 @@
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node server.js",
|
||||
"health": "curl -s http://localhost:3800/api/health | jq ."
|
||||
"health": "curl -s http://localhost:3800/api/health | jq .",
|
||||
"agent:list": "curl -s -H 'Authorization: Bearer $TOKEN' http://localhost:3800/api/agent/list | jq .",
|
||||
"oauth:github": "curl -s 'http://localhost:3800/api/oauth/github?email=$EMAIL' | jq .",
|
||||
"oauth:notion": "curl -s 'http://localhost:3800/api/oauth/notion?email=$EMAIL' | jq .",
|
||||
"oauth:github:callback": "curl -s 'http://localhost:3800/api/oauth/github/callback?code=$CODE&state=$STATE' | jq .",
|
||||
"oauth:notion:callback": "curl -s 'http://localhost:3800/api/oauth/notion/callback?code=$CODE&state=$STATE' | jq .",
|
||||
"agent:handshake:initiate": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"initiatorId\":\"$INITIATOR\",\"targetId\":\"$TARGET\",\"token\":\"$TOKEN\"}' http://localhost:3800/api/agent/handshake/initiate | jq .",
|
||||
"agent:handshake:complete": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"handshakeId\":\"$HANDSHAKE_ID\",\"targetId\":\"$TARGET\",\"targetKey\":\"$TARGET_KEY\",\"token\":\"$TOKEN\"}' http://localhost:3800/api/agent/handshake/complete | jq .",
|
||||
"agent:handshake:heartbeat": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"connectionId\":\"$CONN_ID\",\"token\":\"$TOKEN\"}' http://localhost:3800/api/agent/handshake/heartbeat | jq .",
|
||||
"agent:handshake:disconnect": "curl -s -X POST -H 'Content-Type: application/json' -d '{\"connectionId\":\"$CONN_ID\",\"token\":\"$TOKEN\"}' http://localhost:3800/api/agent/handshake/disconnect | jq ."
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.0",
|
||||
@ -14,7 +23,9 @@
|
||||
"express-rate-limit": "^7.4.0",
|
||||
"better-sqlite3": "^11.0.0",
|
||||
"pg": "^8.13.0",
|
||||
"nodemailer": "^7.0.11"
|
||||
"nodemailer": "^7.0.11",
|
||||
"node-fetch": "^3.3.2",
|
||||
"cos-nodejs-sdk-v5": "^2.11.19"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@ -22,4 +33,4 @@
|
||||
"author": "铸渊 · ICE-GL-ZY001",
|
||||
"license": "UNLICENSED",
|
||||
"private": true
|
||||
}
|
||||
}
|
||||
2151
server/app/server.js
2151
server/app/server.js
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user