openvibers/openvibe.bot Commits
Security: a typed Network token (realtime ticket, FedCM assertion) is never a session (#69)
Changes
@@@ -68,6 +68,9 @@ function createUserAuth(config, keys) {if (!claims || typeof claims !== 'object') return null;if (typeof claims.sub === 'string' && /^(svc|app|mod|node):/.test(claims.sub)) return null;if (claims.actor_type === 'service' || claims.actor_type === 'node') return null;++ // A typed token is never a session: a realtime ticket (typ realtime) or a FedCM assertion (typ fedcm, issued to++ // another site) is signed by the same key, and this check takes no audience (openvibe-sdk/auth's rule).++ if (claims.typ !== undefined || claims.purpose !== undefined) return null;return claims;}return { client, verify };@@@ -35,6 +35,14 @@ const { boot, check, done } = require('./helpers/app');assert.strictEqual(missing.headers.get('content-type'), 'application/problem+json');});++ await check('a typed Network token (realtime ticket, FedCM assertion) is never a session, even signed by the Network key', async () => {++ assert.strictEqual((await t.call('GET', '/api/v1/robots', { user: alex })).status, 200, 'a session token works');++ for (const extra of [{ typ: 'realtime', purpose: 'realtime' }, { typ: 'fedcm', aud: 'https://elsewhere.example' }]) {++ const r = await t.call('GET', '/api/v1/robots', { token: t.network.signUser(alex, extra) });++ assert.strictEqual(r.status, 401, `${extra.typ}: ${r.status}`);++ }++ });++await check('a stranger cannot read or manage someone else\'s robot; the owner can', async () => {const { robot, pairing } = await t.robot(alex);assert.match(pairing.code, /^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$/);@@@ -50,8 +50,8 @@ async function startNetwork() {const now = Math.floor(Date.now() / 1000);return serviceAuth.signServiceToken({ iss: issuer, sub, actor_type: 'service', aud, cap, iat: now, exp: now + expSec, jti: crypto.randomBytes(8).toString('hex') }, privatePem);}-- function signUser(u) {-- return jwt.sign({ sub: String(u.networkId || ++n), subject_id: u.subject, username: u.username, display_name: u.display_name || u.username, role: u.role || 'user' },++ function signUser(u, extra = {}) {++ return jwt.sign({ sub: String(u.networkId || ++n), subject_id: u.subject, username: u.username, display_name: u.display_name || u.username, role: u.role || 'user', ...extra },privatePem, { algorithm: 'RS256', issuer, expiresIn: '1h' });}// Node principals: the view Network's principalView answers (server/registry/node-principals.js).
@@ -68,6 +68,9 @@ function createUserAuth(config, keys) {
@@ -68,6 +68,9 @@ function createUserAuth(config, keys) {
if (!claims || typeof claims !== 'object') return null;
if (!claims || typeof claims !== 'object') return null;
if (typeof claims.sub === 'string' && /^(svc|app|mod|node):/.test(claims.sub)) return null;
if (typeof claims.sub === 'string' && /^(svc|app|mod|node):/.test(claims.sub)) return null;
if (claims.actor_type === 'service' || claims.actor_type === 'node') return null;
if (claims.actor_type === 'service' || claims.actor_type === 'node') return null;
+ // A typed token is never a session: a realtime ticket (typ realtime) or a FedCM assertion (typ fedcm, issued to
+ // another site) is signed by the same key, and this check takes no audience (openvibe-sdk/auth's rule).
+ if (claims.typ !== undefined || claims.purpose !== undefined) return null;
return claims;
return claims;
}
}
return { client, verify };
return { client, verify };
@@ -35,6 +35,14 @@ const { boot, check, done } = require('./helpers/app');
@@ -35,6 +35,14 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(missing.headers.get('content-type'), 'application/problem+json');
assert.strictEqual(missing.headers.get('content-type'), 'application/problem+json');
});
});
+ await check('a typed Network token (realtime ticket, FedCM assertion) is never a session, even signed by the Network key', async () => {
+ assert.strictEqual((await t.call('GET', '/api/v1/robots', { user: alex })).status, 200, 'a session token works');
+ for (const extra of [{ typ: 'realtime', purpose: 'realtime' }, { typ: 'fedcm', aud: 'https://elsewhere.example' }]) {
+ const r = await t.call('GET', '/api/v1/robots', { token: t.network.signUser(alex, extra) });
+ assert.strictEqual(r.status, 401, `${extra.typ}: ${r.status}`);
+ }
+ });
+
await check('a stranger cannot read or manage someone else\'s robot; the owner can', async () => {
await check('a stranger cannot read or manage someone else\'s robot; the owner can', async () => {
const { robot, pairing } = await t.robot(alex);
const { robot, pairing } = await t.robot(alex);
assert.match(pairing.code, /^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$/);
assert.match(pairing.code, /^[0-9A-HJKMNP-TV-Z]{4}-[0-9A-HJKMNP-TV-Z]{4}$/);
@@ -50,8 +50,8 @@ async function startNetwork() {
@@ -50,8 +50,8 @@ async function startNetwork() {
const now = Math.floor(Date.now() / 1000);
const now = Math.floor(Date.now() / 1000);
return serviceAuth.signServiceToken({ iss: issuer, sub, actor_type: 'service', aud, cap, iat: now, exp: now + expSec, jti: crypto.randomBytes(8).toString('hex') }, privatePem);
return serviceAuth.signServiceToken({ iss: issuer, sub, actor_type: 'service', aud, cap, iat: now, exp: now + expSec, jti: crypto.randomBytes(8).toString('hex') }, privatePem);
}
}
- function signUser(u) {
- return jwt.sign({ sub: String(u.networkId || ++n), subject_id: u.subject, username: u.username, display_name: u.display_name || u.username, role: u.role || 'user' },
+ function signUser(u, extra = {}) {
+ return jwt.sign({ sub: String(u.networkId || ++n), subject_id: u.subject, username: u.username, display_name: u.display_name || u.username, role: u.role || 'user', ...extra },
privatePem, { algorithm: 'RS256', issuer, expiresIn: '1h' });
privatePem, { algorithm: 'RS256', issuer, expiresIn: '1h' });
}
}
// Node principals: the view Network's principalView answers (server/registry/node-principals.js).
// Node principals: the view Network's principalView answers (server/registry/node-principals.js).