fix(auth): harden WeChat login and explicit identity switching
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// Public HTTPS preflight. Uses only deliberately invalid OAuth codes; creates no
|
||||
// user/report, makes no paid Face++ call, and never simulates a production OpenID.
|
||||
// H5_BASE_URL=https://1me.xajuzi.com/state/ node h5/test/wechat-production.mjs
|
||||
import assert from 'node:assert/strict';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
assert(process.env.H5_BASE_URL, 'Set H5_BASE_URL explicitly for the production preflight');
|
||||
const base = new URL(process.env.H5_BASE_URL);
|
||||
assert.equal(base.protocol, 'https:');
|
||||
assert(base.pathname.endsWith('/'), 'H5_BASE_URL must have a trailing slash');
|
||||
const expectedAppId = process.env.WECHAT_EXPECTED_APP_ID || 'wx645d9a7573f1fe9a';
|
||||
async function api(route, options = {}) {
|
||||
return fetch(new URL('api/' + route, base), { ...options, redirect: 'manual', signal: AbortSignal.timeout(20000) });
|
||||
}
|
||||
const config = await api('auth/config');
|
||||
assert.equal(config.status, 200);
|
||||
assert.equal((await config.json()).wechat, true);
|
||||
for (const [data, origin, status] of [
|
||||
[{ consent: false }, base.origin, 400],
|
||||
[{ consent: true }, 'https://invalid.example', 403]
|
||||
]) {
|
||||
const r = await api('auth/wechat', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: origin }, body: JSON.stringify(data) });
|
||||
assert.equal(r.status, status);
|
||||
}
|
||||
async function start() {
|
||||
const r = await api('auth/wechat', { method: 'POST', headers: { 'Content-Type': 'application/json', Origin: base.origin }, body: JSON.stringify({ consent: true }) });
|
||||
assert.equal(r.status, 200);
|
||||
const cookies = r.headers.getSetCookie();
|
||||
const binding = cookies.find(c => c.startsWith('juzi_oauth='));
|
||||
assert(binding && binding.includes('Secure') && binding.includes('HttpOnly') && binding.includes('SameSite=Lax'));
|
||||
assert(binding.includes('Path=' + base.pathname + ';'));
|
||||
assert(!cookies.some(c => c.startsWith('juzi_session=')));
|
||||
const auth = new URL((await r.json()).url);
|
||||
assert.equal(auth.origin + auth.pathname, 'https://open.weixin.qq.com/connect/oauth2/authorize');
|
||||
assert.equal(auth.searchParams.get('appid'), expectedAppId);
|
||||
assert.equal(auth.searchParams.get('scope'), 'snsapi_base');
|
||||
assert.equal(auth.searchParams.get('response_type'), 'code');
|
||||
assert.equal(auth.searchParams.get('redirect_uri'), new URL('api/auth/wechat/callback', base).href);
|
||||
assert.equal(auth.hash, '#wechat_redirect');
|
||||
assert(!auth.searchParams.has('secret'));
|
||||
const state = auth.searchParams.get('state');
|
||||
assert(/^[a-f0-9]{64}$/.test(state));
|
||||
return { cookie: binding.split(';')[0], state };
|
||||
}
|
||||
async function callback(row, cookie, expectedError) {
|
||||
const query = new URLSearchParams({ code: 'codex-invalid-' + randomUUID(), state: row.state });
|
||||
const r = await api('auth/wechat/callback?' + query, { headers: cookie ? { Cookie: cookie } : {} });
|
||||
assert.equal(r.status, 302);
|
||||
assert.equal(r.headers.get('location'), base.pathname + '?login_error=' + expectedError);
|
||||
assert(!r.headers.getSetCookie().some(c => c.startsWith('juzi_session=')), 'invalid callback must not issue a session');
|
||||
}
|
||||
const missingBinding = await start();
|
||||
await callback(missingBinding, '', 'state');
|
||||
const invalidCode = await start();
|
||||
await callback(invalidCode, invalidCode.cookie, 'wechat');
|
||||
await callback(invalidCode, invalidCode.cookie, 'state');
|
||||
assert.equal((await api('auth/session')).status, 200);
|
||||
assert.equal((await (await api('auth/session')).json()).user, null);
|
||||
assert.equal((await api('reports')).status, 401);
|
||||
assert.equal((await api('admin/records')).status, 401);
|
||||
assert.equal((await fetch(new URL('admin.html', base))).status, 401);
|
||||
assert.equal((await api('health')).status, 200);
|
||||
assert.equal((await fetch(new URL('/MP_verify_kpx234XimQGtFPo6.txt', base))).status, 200);
|
||||
console.log('PASS: public HTTPS WeChat config, consent/CSRF, correct authorization URL, Secure scoped cookies, missing binding, real invalid-code rejection, state replay rejection, anonymous report denial, admin protection and domain file; REAL client authorization remains untested');
|
||||
Reference in New Issue
Block a user