fix(auth): harden WeChat login and explicit identity switching
This commit is contained in:
+15
-6
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user