fix(auth): harden WeChat login and explicit identity switching

This commit is contained in:
xsl
2026-10-12 10:07:54 +08:00
parent 0ec5e5dc32
commit d68f841f0e
16 changed files with 1484 additions and 735 deletions
+15 -6
View File
@@ -80,6 +80,8 @@ module.exports = function createAccounts(root, env = process.env) {
limits.set(key, value); return ++value.count <= 60;
}
function redirect(res, suffix) { res.writeHead(302, { Location: prefix + suffix, 'Cache-Control': 'no-store', 'Referrer-Policy': 'no-referrer' }); res.end(); }
// Operational events only: never log code/state, cookies, OpenID, secrets or tokens.
function wechatEvent(event, fields = {}) { console.log('[auth/wechat] ' + event + ' ' + JSON.stringify(fields)); }
async function route(req, res, pathname) {
if (!pathname.startsWith('/api/auth/') && !pathname.startsWith('/api/reports')) return false;
try {
@@ -123,17 +125,24 @@ module.exports = function createAccounts(root, env = process.env) {
const state = url.searchParams.get('state') || '', code = url.searchParams.get('code');
const row = db.states[hash(state)], binding = cookies(req).juzi_oauth || '';
delete db.states[hash(state)]; save(); cookie(res, 'juzi_oauth', '', 0);
if (!available || !code || !row || row.expires < Date.now() || row.binding !== hash(binding)) { redirect(res, '?login_error=state'); return true; }
if (!available || !code || !row || row.expires < Date.now() || row.binding !== hash(binding)) {
wechatEvent('callback_rejected', { reason: 'state' }); redirect(res, '?login_error=state'); return true;
}
const tokenUrl = new URL('https://api.weixin.qq.com/sns/oauth2/access_token');
tokenUrl.search = new URLSearchParams({ appid: appId, secret, code, grant_type: 'authorization_code' }).toString();
let data;
try { const r = await fetch(tokenUrl, { signal: AbortSignal.timeout(10000) }); data = await r.json(); }
catch (_) { redirect(res, '?login_error=network'); return true; }
if (!data.openid || data.errcode) { redirect(res, '?login_error=wechat'); return true; }
let data, upstreamStatus;
try { const r = await fetch(tokenUrl, { signal: AbortSignal.timeout(10000) }); upstreamStatus = r.status; data = await r.json(); }
catch (_) { wechatEvent('exchange_failed', { reason: 'network' }); redirect(res, '?login_error=network'); return true; }
if (!data || typeof data.openid !== 'string' || !data.openid || data.errcode || (upstreamStatus && upstreamStatus !== 200)) {
wechatEvent('exchange_failed', { reason: 'wechat', errcode: data && Number.isInteger(data.errcode) ? data.errcode : null,
httpStatus: upstreamStatus || null });
redirect(res, '?login_error=wechat'); return true;
}
const identity = hash(appId + ':' + data.openid);
let user = db.users.find(u => u.kind === 'wechat' && u.identity === identity);
const newAccount = !user;
if (!user) { user = { id: crypto.randomUUID(), kind: 'wechat', identity, consent: new Date().toISOString(), reports: [] }; db.users.push(user); }
login(res, user); redirect(res, '?login=success');
login(res, user); wechatEvent('login_success', { newAccount }); redirect(res, '?login=success');
} else if (pathname.startsWith('/api/reports') && req.method === 'GET') {
const user = session(req);
if (!user) { json(res, 401, { ok: false, message: '请先登录' }); return true; }