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
Changes
@@@ -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 });@@@ -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); });
@@ -12,7 +12,7 @@
@@ -12,7 +12,7 @@
// GET /auth/callback → server-side code exchange, set cookies
// GET /auth/callback → server-side code exchange, set cookies
// POST /auth/fedcm → the shared navbar's FedCM assertion → tokens (jwt-bearer grant)
// 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/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
// POST /auth/refresh → rotate tokens via refresh_token grant
//
//
// Cookies (host-only for openvibe.blog):
// Cookies (host-only for openvibe.blog):
@@ -379,7 +379,10 @@ function createAuthRoutes(config, auth) {
@@ -379,7 +379,10 @@ function createAuthRoutes(config, auth) {
res.vary('Cookie');
res.vary('Cookie');
res.vary('Authorization');
res.vary('Authorization');
const token = extractToken(req);
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);
const claims = await auth.verify(token);
if (!claims) return res.status(401).json({ error: 'Invalid or expired 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 });
res.json({ user: claimsToUser(claims), expires_at: claims.exp ? claims.exp * 1000 : null });
@@ -61,6 +61,15 @@ const LONG = Array.from({ length: 100 }, (_, i) => `w${i}`).join(' ');
@@ -61,6 +61,15 @@ const LONG = Array.from({ length: 100 }, (_, i) => `w${i}`).join(' ');
assert.strictEqual(r.headers.get('cache-control'), 'private, no-store');
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();
await t.close();
done();
done();
})().catch((e) => { console.error(e); process.exit(1); });
})().catch((e) => { console.error(e); process.exit(1); });