guanghulab/server/app/modules/oauth-providers.js

286 lines
7.1 KiB
JavaScript
Raw Normal View History

'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
};