fix(auth): harden WeChat login and explicit identity switching
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
<p class="identity-note" id="identity-note"></p>
|
||||
<div id="history-list" aria-live="polite"></div>
|
||||
<button class="history-retry" data-action="history-retry">刷新列表</button>
|
||||
<button class="history-retry" data-action="switch-login">切换登录方式</button>
|
||||
<small class="font-credit">本应用使用 MiSans 字体</small>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* Browser identity is a random credential, never a fingerprint or a report. */
|
||||
window.AccountClient = (function () {
|
||||
'use strict';
|
||||
var key = 'juzi.identity.v1', user = null;
|
||||
function read() { try { return localStorage.getItem(key); } catch (_) { return null; } }
|
||||
var key = 'juzi.identity.v1', kindKey = 'juzi.auth.kind.v1', user = null;
|
||||
function read(name) { try { return localStorage.getItem(name || key); } catch (_) { return null; } }
|
||||
function rememberKind() { try { if (user) localStorage.setItem(kindKey, user.kind); } catch (_) {} }
|
||||
function request(url, data) {
|
||||
return fetch(url, { credentials: 'same-origin', method: data ? 'POST' : 'GET',
|
||||
headers: data ? { 'Content-Type': 'application/json' } : {},
|
||||
@@ -13,13 +14,16 @@ window.AccountClient = (function () {
|
||||
function browser(consent) {
|
||||
return request('api/auth/browser', { consent: consent, token: read() }).then(function (v) {
|
||||
try { localStorage.setItem(key, v.token); } catch (_) { /* session cookie still works */ }
|
||||
user = v.user; return v;
|
||||
user = v.user; rememberKind(); return v;
|
||||
});
|
||||
}
|
||||
function init() {
|
||||
function init(options) {
|
||||
return request('api/auth/session').then(function (v) {
|
||||
user = v.user;
|
||||
if (!user && read()) return browser(true).catch(function (e) {
|
||||
rememberKind();
|
||||
// A missing WeChat session is not permission to switch to a browser account.
|
||||
// OAuth returns must also stay explicit when its cookie was not retained.
|
||||
if (!user && !(options && options.restoreBrowser === false) && read(kindKey) !== 'wechat' && read()) return browser(true).catch(function (e) {
|
||||
// Do not replace an existing credential on a transient server/network error.
|
||||
if (/身份已失效/.test(e.message)) { try { localStorage.removeItem(key); } catch (_) {} }
|
||||
throw e;
|
||||
|
||||
+30
-7
@@ -1304,7 +1304,13 @@
|
||||
startCamera(); // 复用启停与预热(camSeqCam 防竞态)
|
||||
},
|
||||
'history': function () { openHistory(); },
|
||||
'history-retry': function () { renderHistoryList(); }
|
||||
'history-retry': function () { renderHistoryList(); },
|
||||
'switch-login': function () {
|
||||
historySeq++;
|
||||
$('#login-consent').checked = false;
|
||||
$('#login-message').textContent = '微信与本机浏览器为独立账号,报告不会自动合并。请选择登录方式;返回可保留当前登录。';
|
||||
showScreen('s-login', 'push');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
@@ -1427,14 +1433,31 @@
|
||||
history.replaceState({ screen: 's-login' }, ''); // 刷新后从启动页开始,历史栈的屏标记与之对齐
|
||||
renderCamStep();
|
||||
if (!(/[?&]mock=1/.test(location.search) && /[?&]screen=report/.test(location.search))) {
|
||||
var loginError = /[?&]login_error=([^&#]*)/.exec(location.search);
|
||||
var loginSuccess = /[?&]login=success(?:&|$)/.test(location.search);
|
||||
var oauthReturn = !!loginError || loginSuccess;
|
||||
loginBusy = true;
|
||||
AccountClient.init().then(function () {
|
||||
if (AccountClient.user()) showScreen('s-splash', 'replace');
|
||||
else $('#login-message').textContent = /[?&]login_error=/.test(location.search)
|
||||
? '微信登录未完成,可重试或使用本机浏览器登录。'
|
||||
: '本机登录仅限当前浏览器,不支持换手机找回。';
|
||||
AccountClient.init({ restoreBrowser: !oauthReturn }).then(function () {
|
||||
if (loginError) {
|
||||
var code = loginError[1];
|
||||
$('#login-message').textContent = code === 'state'
|
||||
? '登录链接已过期或会话丢失,请在同一微信窗口重新点击微信登录。'
|
||||
: code === 'network' ? '暂时无法连接微信服务,请稍后重新登录。'
|
||||
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择本机浏览器登录。';
|
||||
} else if (loginSuccess && (!AccountClient.user() || AccountClient.user().kind !== 'wechat')) {
|
||||
$('#login-message').textContent = '微信返回后登录会话未能保存,请在同一微信窗口重新登录。';
|
||||
} else if (AccountClient.user()) showScreen('s-splash', 'replace');
|
||||
else $('#login-message').textContent = '微信登录可跨手机查看报告;本机登录仅限当前浏览器。';
|
||||
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
||||
.finally(function () { loginBusy = false; });
|
||||
.finally(function () {
|
||||
loginBusy = false;
|
||||
if (oauthReturn) {
|
||||
var clean = new URL(location.href);
|
||||
clean.searchParams.delete('login'); clean.searchParams.delete('login_error');
|
||||
if (clean.hash === '#wechat_redirect') clean.hash = '';
|
||||
history.replaceState(history.state, '', clean.pathname + clean.search + clean.hash);
|
||||
}
|
||||
});
|
||||
}
|
||||
// 仅调试用:?mock=1&screen=report 直渲 mock 报告页(供无头浏览器截图验证布局,非正式入口)
|
||||
// 有 MOCK_FIXTURE 时走 fixture(检测可视化同步出图),否则回退 SkinScore 内置样本
|
||||
|
||||
+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; }
|
||||
|
||||
@@ -76,6 +76,25 @@ const ReportUI = require('../js/report-ui');
|
||||
return (await request('/api/auth/session', null, callback.cookie)).data.user;
|
||||
}
|
||||
assert.deepEqual(await wechat(), await wechat());
|
||||
for (const scenario of [
|
||||
{ data: { errcode: 40029 }, error: 'wechat' },
|
||||
{ data: null, error: 'wechat' },
|
||||
{ data: { openid: {} }, error: 'wechat' },
|
||||
{ data: { openid: 'must-not-login' }, status: 500, error: 'wechat' },
|
||||
{ network: true, error: 'network' }
|
||||
]) {
|
||||
global.fetch = (url, options) => String(url).startsWith('https://api.weixin.qq.com/')
|
||||
? scenario.network ? Promise.reject(new Error('fixture network failure'))
|
||||
: Promise.resolve({ status: scenario.status || 200, json: async () => scenario.data })
|
||||
: realFetch(url, options);
|
||||
const started = await request('/api/auth/wechat', { consent: true });
|
||||
const state = new URL(started.data.url).searchParams.get('state');
|
||||
const failed = await request('/api/auth/wechat/callback?code=bad&state=' + state, null, started.cookie);
|
||||
assert.equal(failed.location, '/state/?login_error=' + scenario.error);
|
||||
assert(!failed.cookie.includes('juzi_session='));
|
||||
assert.equal((await request('/api/auth/session', null, failed.cookie)).data.user, null);
|
||||
assert((await request('/api/auth/wechat/callback?code=bad&state=' + state, null, started.cookie)).location.includes('login_error=state'));
|
||||
}
|
||||
} finally { global.fetch = realFetch; }
|
||||
accounts = create(root, { TRUST_LOOPBACK_PROXY: '1' });
|
||||
// Malformed credentials exercise the limiter without creating 60 users.
|
||||
|
||||
@@ -9,6 +9,8 @@ import { createRequire } from 'node:module';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
const require = createRequire(import.meta.url);
|
||||
const createAccounts = require('../lib/accounts');
|
||||
const SkinScore = require('../js/skin-score');
|
||||
const ReportUI = require('../js/report-ui');
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-wechat-test-'));
|
||||
const h5 = fileURLToPath(new URL('../', import.meta.url));
|
||||
@@ -35,12 +37,25 @@ try {
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const base = 'https://skin.example.test:' + server.address().port + '/state/';
|
||||
accounts = createAccounts(path.join(root, 'accounts'), { WECHAT_APP_ID: 'test-app', WECHAT_APP_SECRET: 'test-secret', PUBLIC_BASE_URL: base });
|
||||
let upstreamError = false, openid = 'test-same-wechat-user';
|
||||
global.fetch = async url => {
|
||||
assert.equal(new URL(url).origin, 'https://api.weixin.qq.com');
|
||||
return { ok: true, json: async () => ({ openid: 'test-same-wechat-user' }) };
|
||||
return { ok: true, status: 200, json: async () => upstreamError ? { errcode: 40029 } : { openid } };
|
||||
};
|
||||
browser = await chromium.launch({ headless: true, args: ['--no-proxy-server', '--host-resolver-rules=MAP skin.example.test 127.0.0.1'] });
|
||||
// Fetch in Chromium so its test-host resolver and cookie jar are both used.
|
||||
async function api(page, route, data) {
|
||||
return page.evaluate(async ({ route, data }) => {
|
||||
const response = await fetch('api/' + route, { method: data ? 'POST' : 'GET',
|
||||
headers: data ? { 'Content-Type': 'application/json' } : {}, body: data ? JSON.stringify(data) : undefined });
|
||||
return { status: response.status, data: await response.json() };
|
||||
}, { route, data });
|
||||
}
|
||||
let firstId;
|
||||
const vm = SkinScore.buildViewModel(SkinScore.MOCK_RESULT, SkinScore.MOCK_SIDE, { realAge: 30 });
|
||||
const saved = { id: 'RwechatCrossDevice', created_at: new Date().toISOString(), vm,
|
||||
presentation: ReportUI.build(vm, SkinScore.MOCK_RESULT), photos: [null, null, null],
|
||||
payload: { result: SkinScore.MOCK_RESULT, side: SkinScore.MOCK_SIDE } };
|
||||
for (let device = 0; device < 2; device++) {
|
||||
const context = await browser.newContext({ ignoreHTTPSErrors: true, userAgent: 'Mozilla/5.0 MicroMessenger/8.0', viewport: { width: 375, height: 726 } });
|
||||
const page = await context.newPage();
|
||||
@@ -56,22 +71,91 @@ try {
|
||||
});
|
||||
await page.goto(base);
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
let browserId, browserToken;
|
||||
if (device === 0) {
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
browserId = await page.evaluate(() => AccountClient.user().id);
|
||||
browserToken = await page.evaluate(() => localStorage.getItem('juzi.identity.v1'));
|
||||
accounts.storeReport(browserId, { ...saved, id: 'RbrowserOriginal' });
|
||||
await page.click('[data-action="history"]');
|
||||
await page.waitForSelector('[data-report-id="RbrowserOriginal"]');
|
||||
await page.click('[data-action="switch-login"]');
|
||||
assert.equal(await page.locator('#login-consent').isChecked(), false);
|
||||
assert((await page.locator('#login-message').textContent()).includes('报告不会自动合并'));
|
||||
upstreamError = true;
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-wechat"]');
|
||||
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('微信授权未完成'));
|
||||
assert.equal(await page.locator('#s-login.active').count(), 1, 'OAuth failure must not masquerade as browser login success');
|
||||
await page.waitForFunction(() => !location.search.includes('login_error'));
|
||||
upstreamError = false;
|
||||
}
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-wechat"]');
|
||||
// Redirects may preserve WeChat's fragment; it is not sent to the backend.
|
||||
await page.waitForURL(url => url.origin + url.pathname === base && url.searchParams.get('login') === 'success');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.waitForFunction(() => !location.search.includes('login=success'));
|
||||
const user = await page.evaluate(() => AccountClient.user());
|
||||
assert.equal(user.kind, 'wechat');
|
||||
if (firstId) assert.equal(user.id, firstId); else firstId = user.id;
|
||||
if (device === 0) accounts.storeReport(user.id, saved);
|
||||
const cookie = (await context.cookies()).find(c => c.name === 'juzi_session');
|
||||
assert(cookie.secure && cookie.httpOnly && cookie.path === '/state/' && cookie.sameSite === 'Lax');
|
||||
await page.click('[data-action="history"]');
|
||||
await page.waitForFunction(() => document.querySelector('#identity-note').textContent.includes('同一微信账号'));
|
||||
await page.waitForSelector('[data-report-id="RwechatCrossDevice"]');
|
||||
assert.equal(await page.locator('[data-report-id="RbrowserOriginal"]').count(), 0);
|
||||
assert.deepEqual((await api(page, 'reports/' + saved.id)).data.report, JSON.parse(JSON.stringify(saved)));
|
||||
if (device === 0) {
|
||||
assert.equal(await page.evaluate(() => localStorage.getItem('juzi.identity.v1')), browserToken);
|
||||
await context.clearCookies();
|
||||
await page.reload();
|
||||
await page.waitForFunction(() => !document.querySelector('#login-message').textContent.includes('检查'));
|
||||
assert.equal(await page.locator('#s-login.active').count(), 1, 'lost WeChat session must not silently restore the browser account');
|
||||
assert.equal((await api(page, 'auth/session')).data.user, null);
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-wechat"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
assert.equal(await page.evaluate(() => AccountClient.user().id), firstId);
|
||||
await page.click('[data-action="history"]');
|
||||
await page.click('[data-action="switch-login"]');
|
||||
await page.check('#login-consent');
|
||||
await page.click('[data-action="login-browser"]');
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
assert.equal(await page.evaluate(() => AccountClient.user().id), browserId);
|
||||
await page.click('[data-action="history"]');
|
||||
await page.waitForSelector('[data-report-id="RbrowserOriginal"]');
|
||||
assert.equal(await page.locator('[data-report-id="RwechatCrossDevice"]').count(), 0);
|
||||
assert.equal((await api(page, 'reports/' + saved.id)).status, 404);
|
||||
// An OAuth success URL with a lost cookie must not restore an unrelated browser credential.
|
||||
await context.clearCookies();
|
||||
await page.goto(base + '?login=success');
|
||||
await page.waitForFunction(() => document.querySelector('#login-message').textContent.includes('会话未能保存'));
|
||||
assert.equal((await api(page, 'auth/session')).data.user, null);
|
||||
await page.waitForFunction(() => !location.search.includes('login=success'));
|
||||
await page.reload(); // Ordinary browser restore is still supported.
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
assert.equal(await page.evaluate(() => AccountClient.user().id), browserId);
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
await context.close();
|
||||
}
|
||||
console.log('PASS: HTTPS /state/ login → simulated WeChat → callback with bound Secure cookie → homepage/history; two browser contexts share WeChat identity');
|
||||
// Different WeChat users must never see each other's reports.
|
||||
openid = 'test-different-wechat-user';
|
||||
const stranger = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||
const strangerPage = await stranger.newPage();
|
||||
await strangerPage.goto(base);
|
||||
const start = await api(strangerPage, 'auth/wechat', { consent: true });
|
||||
const auth = new URL(start.data.url), callback = new URL(auth.searchParams.get('redirect_uri'));
|
||||
callback.search = new URLSearchParams({ code: 'test-code', state: auth.searchParams.get('state') });
|
||||
await strangerPage.goto(callback.href);
|
||||
const strangerUser = (await api(strangerPage, 'auth/session')).data.user;
|
||||
assert(strangerUser && strangerUser.id !== firstId);
|
||||
assert.equal((await api(strangerPage, 'reports/' + saved.id)).status, 404);
|
||||
assert.equal((await api(strangerPage, 'reports')).data.total, 0);
|
||||
await stranger.close();
|
||||
console.log('PASS: HTTPS /state/ simulated WeChat login, Secure cookies, same identity/report across devices, different-user isolation, explicit account switching, preserved browser reports, OAuth failure/missing-cookie handling, no silent browser fallback');
|
||||
} finally {
|
||||
global.fetch = realFetch;
|
||||
if (browser) await browser.close();
|
||||
|
||||
@@ -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