openvibers/openvibe.blog Commits

A guest's session probe answers 200, not 401 (browser check, WS-Q task 3): GET /auth/me with no session credential at all answers { user: null }, which the shared navbar reads as signed out. It asks on every page view, and the 401 logged a console error on each. A credential that is present but invalid or expired still answers 401. Test: security.test.js

OP OpenVibers committed 26 Sept 2026 · 897822f061 · 2 files, +14 −2


Changes

ddiff --git a/server/auth/sso.js b/server/auth/sso.js
iindex 7e9f93e..f2d8783 100644
---- a/server/auth/sso.js
++++ b/server/auth/sso.js
@@@ -12,7 +12,7 @@
// GET /auth/callback → server-side code exchange, set cookies
// POST /auth/fedcm → the shared navbar's FedCM assertion → tokens (jwt-bearer grant)
// GET /auth/logout → clear cookies (+ best-effort refresh revoke), hint=guest
--// GET /auth/me → offline-verify ov_token, return profile
++// GET /auth/me → offline-verify ov_token, return profile ({ user: null } for a guest)
// POST /auth/refresh → rotate tokens via refresh_token grant
//
// Cookies (host-only for openvibe.blog):
@@@ -379,7 +379,10 @@ function createAuthRoutes(config, auth) {
res.vary('Cookie');
res.vary('Authorization');
const token = extractToken(req);
-- if (!token) return res.status(401).json({ error: 'Not authenticated' });
++ // No credential at all (a guest) is signed out, not an error: the shared navbar asks this on every
++ // page view, and a 401 logged a console error on each (browser check, OpenVibe.Host). A credential
++ // that is present but invalid or expired still answers 401.
++ if (!token) return res.json({ user: null });
const claims = await auth.verify(token);
if (!claims) return res.status(401).json({ error: 'Invalid or expired token' });
res.json({ user: claimsToUser(claims), expires_at: claims.exp ? claims.exp * 1000 : null });
ddiff --git a/test/security.test.js b/test/security.test.js
iindex f3caab6..9afe317 100644
---- a/test/security.test.js
++++ b/test/security.test.js
@@@ -61,6 +61,15 @@ const LONG = Array.from({ length: 100 }, (_, i) => `w${i}`).join(' ');
assert.strictEqual(r.headers.get('cache-control'), 'private, no-store');
});
++ await check('/auth/me: a guest is signed out (200 { user: null }), a bad credential is 401', async () => {
++ const guest = await t.get('/auth/me');
++ assert.strictEqual(guest.status, 200, 'no cookie or token at all: not an error');
++ assert.deepStrictEqual(guest.json(), { user: null });
++ assert.strictEqual(guest.headers.get('cache-control'), 'private, no-store');
++ assert.strictEqual((await t.get('/auth/me', { headers: { cookie: 'ov_token=expired.or.forged' } })).status, 401, 'a present but invalid cookie');
++ assert.strictEqual((await t.get('/auth/me', { as: 'garbage' })).status, 401, 'a present but invalid bearer token');
++ });
++
await t.close();
done();
})().catch((e) => { console.error(e); process.exit(1); });