openvibers/openvibe.space Commits
Space: sessions verified by openvibe-sdk/auth (typed tokens refused) over createNetworkKeys (#28)
Changes
@@@ -66,7 +66,7 @@ migration.- OpenVibe.Network (SSO, JWKS, the `space` OAuth client)- OpenVibe.Community (where every forum URL redirects)--- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.38.0, `openvibe-shared` v3.0.0, pinned by release tarball++- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.40.0, `openvibe-shared` v3.0.0, pinned by release tarball## Capabilities@@@ -196,6 +196,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).<!-- versions:start -->- openvibe-contracts: v0.129.0--- openvibe-sdk: v0.38.0++- openvibe-sdk: v0.40.0- openvibe-shared: v3.0.0<!-- versions:end -->@@@ -7,7 +7,7 @@"port": 4940,"domain": "openvibe.space","contracts": "openvibe-contracts v0.129.0",-- "sdk": "openvibe-sdk v0.38.0",++ "sdk": "openvibe-sdk v0.40.0","shared": "openvibe-shared v3.0.0","capabilities": [],"capabilitiesRegistered": false,@@@ -15,7 +15,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0","pg": "^8.23.0"},@@@ -807,9 +807,9 @@}},"node_modules/openvibe-sdk": {-- "version": "0.38.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.0",-- "integrity": "sha512-/Z+1kB5l4Hff0E1O1Zo+DcXaZX93wrmISOPP75sB7NqVUZVeKuJgjYu6vBwG8ufJjlk0vD3ZJ0ntBLI/3IKdpg==",++ "version": "0.40.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0",++ "integrity": "sha512-fYX0zijJZDdwjXJGwroLDlb/wniY6NXVtgdPVJJHNgcdRmaAydpr6SKuxcLffmkTquyZQptwChsCvo+D6Wx+IA==","license": "MIT","engines": {"node": ">=20"@@@ -27,7 +27,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.0",++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0","pg": "^8.23.0"},@@@ -35,6 +35,7 @@const express = require('express');const crypto = require('crypto');const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');++const { createNetworkKeys, verifyUserToken } = require('openvibe-sdk/auth');const cache = require('openvibe-shared/cache-policy');const ACCESS_COOKIE = 'ov_token';@@@ -45,63 +46,46 @@ const NEXT_COOKIE = 'ov_oauth_next';const SILENT_COOKIE = 'ov_oauth_silent';/**-- * Create the auth client + JWKS fetcher shared by the whole app.-- * Verification is OFFLINE: we cache the Network's RS256 public key from-- * GET /api/.well-known/jwks and verify JWTs locally on every request.++ * Create the auth client shared by the whole app: OAuth (code, jwt-bearer and refresh grants) through++ * OpenVibeAuthClient, and OFFLINE session verification with openvibe-sdk/auth verifyUserToken against Network's keys++ * (createNetworkKeys: the JWKS, retried every 30 s until it loads, refreshed every 15 minutes, a rotation followed on++ * an unknown kid). A typed token (a FedCM assertion, a realtime ticket, an export token) or a service principal is++ * never a session here.++ *++ * auth.keys Network's keys (keys.verifyOptions for the other verifiers, keys.loaded() for readiness)++ * auth.ensureKey() the keys once loaded (fetching once if they are not), else null++ * auth.verify(token) claims | null*/function createAuthClient(config) {const client = new OpenVibeAuthClient({clientId: config.oauth.clientId,clientSecret: config.oauth.clientSecret,redirectUri: config.oauth.redirectUri,-- publicKey: null, // filled by ensureKey()++ publicKey: null, // sessions are verified below, never by this clientauthBase: config.networkUrl,internalBase: config.networkInternalUrl,});++ const keys = createNetworkKeys({ network: config.networkInternalUrl || config.networkUrl, log: console });-- let lastFetch = 0;-- let inflight = null;async function ensureKey() {-- if (client.publicKey) return client.publicKey;-- // A request that arrives while the key is being fetched waits for that fetch rather than-- // being treated as signed out.-- if (inflight) return inflight;-- // Don't hammer the Network if it's down — retry at most every 30s-- if (Date.now() - lastFetch < 30_000) return null;-- lastFetch = Date.now();-- inflight = fetchKey().finally(() => { inflight = null; });-- return inflight;-- }-- async function fetchKey() {-- for (const base of [config.networkInternalUrl, config.networkUrl]) {-- if (!base) continue;-- try {-- const res = await fetch(`${base}/api/.well-known/jwks`, { signal: AbortSignal.timeout(5000) });-- if (!res.ok) continue;-- const jwks = await res.json();-- if (jwks.public_key) {-- client.publicKey = jwks.public_key;-- console.log(`[Auth] Network public key loaded from ${base} (${jwks.algorithm || 'RS256'})`);-- return client.publicKey;-- }-- } catch (err) {-- console.warn(`[Auth] JWKS fetch failed from ${base}: ${err.message}`);-- }-- }-- return null;++ if (!keys.loaded()) await keys.refresh();++ return keys.loaded() ? keys : null;}/** Offline JWT verification. Returns decoded claims or null. */async function verify(token) {if (!token) return null;-- await ensureKey();-- return client.verifyToken(token);++ try {++ return await verifyUserToken(String(token), { ...keys.verifyOptions, issuer: config.networkUrl });++ } catch {++ return null;++ }}-- // Warm the key cache at boot (non-fatal if the Network is down)-- ensureKey().catch(() => {});++ // Load the keys now and keep them fresh (non-fatal if the Network is down; unref'd timers).++ keys.start().catch(() => {});-- return { client, ensureKey, verify };++ return { client, keys, ensureKey, verify };}/** Token from Authorization header or the ov_token cookie. */@@@ -26,9 +26,8 @@ function createSpaceReadiness({ db, auth, config, release = null } = {}) {{name: 'network_jwks', required: false,check: () => {-- if (auth.client.publicKey) return true;-- // Not loaded: ask again (ensureKey throttles itself to one fetch per 30 s) and report now.-- auth.ensureKey().catch(() => {});++ if (auth.keys.loaded()) return true;++ // Not loaded: createNetworkKeys retries every 30 s on its own; report now.return 'Network signing key not loaded yet: sign-in is unavailable';},},@@@ -68,6 +68,15 @@ const { boot, check, done } = require('./helpers/app');assert.match(login.headers.get('location'), /^https?:\/\/127\.0\.0\.1:\d+\/oauth\/authorize/);});++ await check('a session token signs in; a typed Network token (a FedCM assertion, a realtime ticket, an export token) never does', async () => {++ const me = (token) => fetch(`${t.base}/auth/me`, { headers: { cookie: `ov_token=${token}` } });++ const claims = { id: 7, username: 'alex', subject_id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPR' };++ assert.strictEqual((await me(t.network.sign(claims))).status, 200);++ for (const extra of [{ typ: 'fedcm', aud: 'space' }, { typ: 'realtime', purpose: 'realtime' }, { purpose: 'export' }]) {++ assert.strictEqual((await me(t.network.sign({ ...claims, ...extra }))).status, 401, JSON.stringify(extra));++ }++ });++await check('there is no forum here: an unknown path is an honest 404 page', async () => {const r = await t.get('/nope');assert.strictEqual(r.status, 404);
@@ -66,7 +66,7 @@ migration.
@@ -66,7 +66,7 @@ migration.
- OpenVibe.Network (SSO, JWKS, the `space` OAuth client)
- OpenVibe.Network (SSO, JWKS, the `space` OAuth client)
- OpenVibe.Community (where every forum URL redirects)
- OpenVibe.Community (where every forum URL redirects)
-- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.38.0, `openvibe-shared` v3.0.0, pinned by release tarball
+- `openvibe-contracts` v0.112.0, `openvibe-sdk` v0.40.0, `openvibe-shared` v3.0.0, pinned by release tarball
## Capabilities
## Capabilities
@@ -196,6 +196,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).
@@ -196,6 +196,6 @@ AGPL-3.0-only. See [LICENSE](LICENSE).
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.129.0
- openvibe-contracts: v0.129.0
-- openvibe-sdk: v0.38.0
+- openvibe-sdk: v0.40.0
- openvibe-shared: v3.0.0
- openvibe-shared: v3.0.0
<!-- versions:end -->
<!-- versions:end -->
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
"port": 4940,
"port": 4940,
"domain": "openvibe.space",
"domain": "openvibe.space",
"contracts": "openvibe-contracts v0.129.0",
"contracts": "openvibe-contracts v0.129.0",
- "sdk": "openvibe-sdk v0.38.0",
+ "sdk": "openvibe-sdk v0.40.0",
"shared": "openvibe-shared v3.0.0",
"shared": "openvibe-shared v3.0.0",
"capabilities": [],
"capabilities": [],
"capabilitiesRegistered": false,
"capabilitiesRegistered": false,
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -807,9 +807,9 @@
@@ -807,9 +807,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"node_modules/openvibe-sdk": {
- "version": "0.38.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.0",
- "integrity": "sha512-/Z+1kB5l4Hff0E1O1Zo+DcXaZX93wrmISOPP75sB7NqVUZVeKuJgjYu6vBwG8ufJjlk0vD3ZJ0ntBLI/3IKdpg==",
+ "version": "0.40.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0",
+ "integrity": "sha512-fYX0zijJZDdwjXJGwroLDlb/wniY6NXVtgdPVJJHNgcdRmaAydpr6SKuxcLffmkTquyZQptwChsCvo+D6Wx+IA==",
"license": "MIT",
"license": "MIT",
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
@@ -27,7 +27,7 @@
@@ -27,7 +27,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.129.0",
- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.38.0",
+ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.40.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
@@ -35,6 +35,7 @@
@@ -35,6 +35,7 @@
const express = require('express');
const express = require('express');
const crypto = require('crypto');
const crypto = require('crypto');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
+const { createNetworkKeys, verifyUserToken } = require('openvibe-sdk/auth');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const ACCESS_COOKIE = 'ov_token';
const ACCESS_COOKIE = 'ov_token';
@@ -45,63 +46,46 @@ const NEXT_COOKIE = 'ov_oauth_next';
@@ -45,63 +46,46 @@ const NEXT_COOKIE = 'ov_oauth_next';
const SILENT_COOKIE = 'ov_oauth_silent';
const SILENT_COOKIE = 'ov_oauth_silent';
/**
/**
- * Create the auth client + JWKS fetcher shared by the whole app.
- * Verification is OFFLINE: we cache the Network's RS256 public key from
- * GET /api/.well-known/jwks and verify JWTs locally on every request.
+ * Create the auth client shared by the whole app: OAuth (code, jwt-bearer and refresh grants) through
+ * OpenVibeAuthClient, and OFFLINE session verification with openvibe-sdk/auth verifyUserToken against Network's keys
+ * (createNetworkKeys: the JWKS, retried every 30 s until it loads, refreshed every 15 minutes, a rotation followed on
+ * an unknown kid). A typed token (a FedCM assertion, a realtime ticket, an export token) or a service principal is
+ * never a session here.
+ *
+ * auth.keys Network's keys (keys.verifyOptions for the other verifiers, keys.loaded() for readiness)
+ * auth.ensureKey() the keys once loaded (fetching once if they are not), else null
+ * auth.verify(token) claims | null
*/
*/
function createAuthClient(config) {
function createAuthClient(config) {
const client = new OpenVibeAuthClient({
const client = new OpenVibeAuthClient({
clientId: config.oauth.clientId,
clientId: config.oauth.clientId,
clientSecret: config.oauth.clientSecret,
clientSecret: config.oauth.clientSecret,
redirectUri: config.oauth.redirectUri,
redirectUri: config.oauth.redirectUri,
- publicKey: null, // filled by ensureKey()
+ publicKey: null, // sessions are verified below, never by this client
authBase: config.networkUrl,
authBase: config.networkUrl,
internalBase: config.networkInternalUrl,
internalBase: config.networkInternalUrl,
});
});
+ const keys = createNetworkKeys({ network: config.networkInternalUrl || config.networkUrl, log: console });
- let lastFetch = 0;
- let inflight = null;
async function ensureKey() {
async function ensureKey() {
- if (client.publicKey) return client.publicKey;
- // A request that arrives while the key is being fetched waits for that fetch rather than
- // being treated as signed out.
- if (inflight) return inflight;
- // Don't hammer the Network if it's down — retry at most every 30s
- if (Date.now() - lastFetch < 30_000) return null;
- lastFetch = Date.now();
- inflight = fetchKey().finally(() => { inflight = null; });
- return inflight;
- }
- async function fetchKey() {
- for (const base of [config.networkInternalUrl, config.networkUrl]) {
- if (!base) continue;
- try {
- const res = await fetch(`${base}/api/.well-known/jwks`, { signal: AbortSignal.timeout(5000) });
- if (!res.ok) continue;
- const jwks = await res.json();
- if (jwks.public_key) {
- client.publicKey = jwks.public_key;
- console.log(`[Auth] Network public key loaded from ${base} (${jwks.algorithm || 'RS256'})`);
- return client.publicKey;
- }
- } catch (err) {
- console.warn(`[Auth] JWKS fetch failed from ${base}: ${err.message}`);
- }
- }
- return null;
+ if (!keys.loaded()) await keys.refresh();
+ return keys.loaded() ? keys : null;
}
}
/** Offline JWT verification. Returns decoded claims or null. */
/** Offline JWT verification. Returns decoded claims or null. */
async function verify(token) {
async function verify(token) {
if (!token) return null;
if (!token) return null;
- await ensureKey();
- return client.verifyToken(token);
+ try {
+ return await verifyUserToken(String(token), { ...keys.verifyOptions, issuer: config.networkUrl });
+ } catch {
+ return null;
+ }
}
}
- // Warm the key cache at boot (non-fatal if the Network is down)
- ensureKey().catch(() => {});
+ // Load the keys now and keep them fresh (non-fatal if the Network is down; unref'd timers).
+ keys.start().catch(() => {});
- return { client, ensureKey, verify };
+ return { client, keys, ensureKey, verify };
}
}
/** Token from Authorization header or the ov_token cookie. */
/** Token from Authorization header or the ov_token cookie. */
@@ -26,9 +26,8 @@ function createSpaceReadiness({ db, auth, config, release = null } = {}) {
@@ -26,9 +26,8 @@ function createSpaceReadiness({ db, auth, config, release = null } = {}) {
{
{
name: 'network_jwks', required: false,
name: 'network_jwks', required: false,
check: () => {
check: () => {
- if (auth.client.publicKey) return true;
- // Not loaded: ask again (ensureKey throttles itself to one fetch per 30 s) and report now.
- auth.ensureKey().catch(() => {});
+ if (auth.keys.loaded()) return true;
+ // Not loaded: createNetworkKeys retries every 30 s on its own; report now.
return 'Network signing key not loaded yet: sign-in is unavailable';
return 'Network signing key not loaded yet: sign-in is unavailable';
},
},
},
},
@@ -68,6 +68,15 @@ const { boot, check, done } = require('./helpers/app');
@@ -68,6 +68,15 @@ const { boot, check, done } = require('./helpers/app');
assert.match(login.headers.get('location'), /^https?:\/\/127\.0\.0\.1:\d+\/oauth\/authorize/);
assert.match(login.headers.get('location'), /^https?:\/\/127\.0\.0\.1:\d+\/oauth\/authorize/);
});
});
+ await check('a session token signs in; a typed Network token (a FedCM assertion, a realtime ticket, an export token) never does', async () => {
+ const me = (token) => fetch(`${t.base}/auth/me`, { headers: { cookie: `ov_token=${token}` } });
+ const claims = { id: 7, username: 'alex', subject_id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPR' };
+ assert.strictEqual((await me(t.network.sign(claims))).status, 200);
+ for (const extra of [{ typ: 'fedcm', aud: 'space' }, { typ: 'realtime', purpose: 'realtime' }, { purpose: 'export' }]) {
+ assert.strictEqual((await me(t.network.sign({ ...claims, ...extra }))).status, 401, JSON.stringify(extra));
+ }
+ });
+
await check('there is no forum here: an unknown path is an honest 404 page', async () => {
await check('there is no forum here: an unknown path is an honest 404 page', async () => {
const r = await t.get('/nope');
const r = await t.get('/nope');
assert.strictEqual(r.status, 404);
assert.strictEqual(r.status, 404);