openvibers/openvibe.vip Commits
Auth: Network tokens through openvibe-sdk/auth (no hand-rolled JWKS) (#35)
Changes
@@@ -13,14 +13,14 @@"dotenv": "^16.4.7","express": "^4.21.2","iovalkey": "^0.4.0",-- "jsonwebtoken": "^9.0.2","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-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0","pg": "^8.23.0"},"devDependencies": {-- "@electric-sql/pglite": "^0.5.8"++ "@electric-sql/pglite": "^0.5.8",++ "jsonwebtoken": "^9.0.2"},"engines": {"node": ">=22.0.0"@@@ -30,7 +30,6 @@"dotenv": "^16.4.7","express": "^4.21.2","iovalkey": "^0.4.0",-- "jsonwebtoken": "^9.0.2","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-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v3.0.0",@@@ -40,6 +39,7 @@"node": ">=22.0.0"},"devDependencies": {-- "@electric-sql/pglite": "^0.5.8"++ "@electric-sql/pglite": "^0.5.8",++ "jsonwebtoken": "^9.0.2"}}@@@ -19,7 +19,8 @@* A request that presents a token is judged on that token alone: a bad one is refused, never* downgraded to anonymous.*/--const { serviceAuth, capabilities, http, ids, staff: staffMap } = require('openvibe-contracts');++const { capabilities, http, ids, staff: staffMap } = require('openvibe-contracts');++const { verifyService } = require('../network');const PRINCIPAL_SUB = /^(svc|app|mod):/;const ANON = Object.freeze({ kind: 'anonymous' });@@@ -44,27 +45,28 @@ function userPrincipal(claims) {}function createApiAuth({ config, keys, userAuth }) {-- function resolve(req) {++ async function resolve(req) {const header = String(req.headers.authorization || '');if (!header.startsWith('Bearer ')) return { principal: ANON };const token = header.slice(7).trim();-- const publicKey = keys.get();-- if (!publicKey) return { error: [503, 'identity.unavailable', 'the Network signing key is not loaded yet'] };++ if (!keys.ready()) return { error: [503, 'identity.unavailable', 'the Network signing key is not loaded yet'] };const payload = decodePayload(token);if (payload && typeof payload.sub === 'string' && PRINCIPAL_SUB.test(payload.sub)) {-- const r = serviceAuth.verifyServiceToken(token, { publicKey, issuer: config.network.issuer, audience: config.audience });++ let r;++ try { r = await verifyService(config, keys, token); } catch { return { error: [503, 'identity.unavailable', 'the Network signing keys could not be read'] }; }if (!r.ok) return { error: [401, r.code, r.reason] };return { principal: { kind: 'service', sub: r.claims.sub, cap: r.claims.cap || [], jti: r.claims.jti } };}-- const claims = userAuth.verify(token);++ const claims = await userAuth.verify(token);if (!claims) return { error: [401, 'token.invalid', 'the user token is invalid or expired'] };const p = userPrincipal(claims);if (!p) return { error: [403, 'identity.no_subject', 'this account has no canonical subject yet; sign in again'] };return { principal: p };}-- function middleware(req, res, next) {-- const r = resolve(req);++ async function middleware(req, res, next) {++ let r;++ try { r = await resolve(req); } catch (err) { return next(err); }if (r.error) return http.sendProblem(res, r.error[0], r.error[1], { detail: r.error[2], ctx: req.ov });req.principal = r.principal;return next();@@@ -1,44 +1,37 @@'use strict';/**-- * OpenVibe.Network client (the same shape as OpenVibe.Tips and OpenVibe.Community).++ * OpenVibe.Network client.*-- * keys the Network's RS256 public key (OV_NETWORK_PUBLIC_KEY, else GET /api/.well-known/jwks,-- * refreshed every 6 h and retried every 30 s until it loads). It verifies service tokens-- * (audience openvibe.vip) and the browser's Network user JWT, both offline.++ * keys the Network's RS256 signing keys through openvibe-sdk/auth: one process-wide JWKS client per URL keeps++ * them fresh, serves the last good keys through a Network outage, backs off, and refetches at once when a++ * token names a key it does not have (a rotation). OV_NETWORK_PUBLIC_KEY pins one key instead. Service++ * tokens (audience openvibe.vip) and the browser's Network user JWT are both verified offline.*/const crypto = require('crypto');--const jwt = require('jsonwebtoken');++const contracts = require('openvibe-contracts');++const { jwksClient, verifyUserToken, verifyServiceToken } = require('openvibe-sdk/auth');const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');function createKeyProvider(config, { fetchImpl = globalThis.fetch, log = console } = {}) {-- let pem = config.network.publicKey ? crypto.createPublicKey(config.network.publicKey).export({ type: 'spki', format: 'pem' }) : null;-- let timer = null;-- async function load() {-- const url = `${config.network.internalUrl}/api/.well-known/jwks`;-- try {-- const res = await fetchImpl(url, { signal: AbortSignal.timeout(8000) });-- if (!res.ok) throw new Error(`JWKS ${res.status}`);-- const body = await res.json();-- const jwk = (body.keys || []).find((k) => k.kty === 'RSA');-- if (jwk) pem = crypto.createPublicKey({ key: jwk, format: 'jwk' }).export({ type: 'spki', format: 'pem' });-- else if (typeof body.public_key === 'string') pem = crypto.createPublicKey(body.public_key).export({ type: 'spki', format: 'pem' });-- else throw new Error('JWKS contained no keys');-- return pem;-- } catch (e) {-- log.warn(`[VIP] Network key not loaded from ${url}: ${e.message}`);-- return null;-- }-- }-- function start() {-- if (config.network.publicKey || timer) return;-- const retry = () => load().then((k) => { if (!k) setTimeout(retry, 30_000).unref(); });-- retry();-- timer = setInterval(async () => { await load(); }, 6 * 60 * 60 * 1000);-- timer.unref();-- }-- function stop() { if (timer) clearInterval(timer); timer = null; }-- return { get: () => pem, load, start, stop };++ const url = `${config.network.internalUrl}/api/.well-known/jwks`;++ const pinned = config.network.publicKey ? crypto.createPublicKey(config.network.publicKey) : null;++ const client = pinned ? null : jwksClient(url, { fetch: fetchImpl, log });++ /** Where a verification takes its keys from (openvibe-sdk/auth options). */++ const source = pinned ? { publicKey: pinned } : { jwks: url, fetch: fetchImpl };++ return {++ source,++ ready: () => (pinned ? true : client.status().ready),++ /** Fetch the keys now (boot, tests); true when there is a key to verify with. */++ async load() { if (client) await client.refresh().catch(() => {}); return pinned ? true : client.status().ready; },++ start() { if (client) client.start(); },++ stop() { if (client) client.stop(); },++ };++}++++/** A service principal's token (audience openvibe.vip): contracts' result { ok, code, reason, claims }. */++async function verifyService(config, keys, token) {++ return await verifyServiceToken(token, { ...keys.source, issuer: config.network.issuer, audience: config.audience, contracts });}/**@@@ -54,19 +47,15 @@ function createUserAuth(config, keys) {authBase: config.network.url,internalBase: config.network.internalUrl,});-- /** Claims of a valid user token, or null. Service tokens are never user tokens. */-- function verify(token) {++ /**++ * Claims of a valid user token, or null. openvibe-sdk/auth refuses service, app and mod principals and typed++ * tokens (a realtime ticket, an export token): none of them is a session.++ */++ async function verify(token) {if (!token) return null;-- const key = keys.get();-- if (!key) return null;-- let claims;-- try { claims = jwt.verify(token, key, { algorithms: ['RS256'], issuer: config.network.issuer }); } catch { return null; }-- if (!claims || typeof claims !== 'object') return null;-- if (typeof claims.sub === 'string' && /^(svc|app|mod):/.test(claims.sub)) return null;-- if (claims.actor_type === 'service') return null;-- return claims;++ try { return await verifyUserToken(token, { ...keys.source, issuer: config.network.issuer }); } catch { return null; }}return { client, verify };}--module.exports = { createKeyProvider, createUserAuth };++module.exports = { createKeyProvider, createUserAuth, verifyService };@@@ -33,7 +33,7 @@ function createVipReadiness({ db, keys, config, outbox, now, release = null, fet{ name: 'valkey', required: false, check: async () => (valkey ? valkey.ready() : { skipped: 'VALKEY_URL not set: per-actor limits count in this process only' }) },{name: 'network_jwks', required: false,-- check: () => (keys.get() ? true : 'Network signing key not loaded yet: tokens cannot be verified'),++ check: () => (keys.ready() ? true : 'Network signing key not loaded yet: tokens cannot be verified'),},{name: 'billing', required: false, cacheMs: 15_000, timeoutMs: 2500,@@@ -63,9 +63,9 @@ function withParam(target, key, value) {/** Page middleware: req.viewer = the signed-in person (principal shape) or null. Never blocks. */function viewerMiddleware(userAuth) {-- return (req, _res, next) => {++ return async (req, _res, next) => {const token = req.cookies && req.cookies[ACCESS_COOKIE];-- const claims = token ? userAuth.verify(token) : null;++ const claims = token ? await userAuth.verify(token) : null;req.viewer = claims ? userPrincipal(claims) : null;req.viewerClaims = claims ? claimsToUser(claims) : null;next();@@@ -116,9 +116,9 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }throw lastErr || new Error('Network unreachable');}-- router.get('/login', (req, res) => {++ router.get('/login', async (req, res) => {const silent = !!req.query.silent && req.query.silent !== '0';-- if (silent && userAuth.verify(req.cookies && req.cookies[ACCESS_COOKIE])) { clearFlow(res); return res.redirect(sanitizeNext(req.query.next, config)); }++ if (silent && await userAuth.verify(req.cookies && req.cookies[ACCESS_COOKIE])) { clearFlow(res); return res.redirect(sanitizeNext(req.query.next, config)); }const { url, state } = userAuth.client.getAuthorizationUrl(config.oauth.scope);let target = url;if (silent) { const u = new URL(url); u.searchParams.set('prompt', 'none'); target = u.toString(); }@@@ -167,7 +167,7 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }const data = await tokenGrant({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: token });if (!data.access_token) throw Object.assign(new Error('no token'), { status: 401, error: 'invalid_grant' });setSession(res, data.access_token, data.refresh_token);-- return res.json({ ok: true, user: data.user || claimsToUser(userAuth.verify(data.access_token)) });++ return res.json({ ok: true, user: data.user || claimsToUser(await userAuth.verify(data.access_token)) });} catch (err) {if (err.status && err.status < 500) return res.status(401).json({ error: err.error || 'invalid_grant' });return res.status(502).json({ error: 'server_error', error_description: 'Could not reach OpenVibe.Network' });@@@ -190,9 +190,9 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }res.redirect(sanitizeNext(req.query.next, config));});-- router.get('/me', (req, res) => {++ router.get('/me', async (req, res) => {const token = (String(req.headers.authorization || '').startsWith('Bearer ') ? req.headers.authorization.slice(7) : null) || (req.cookies && req.cookies[ACCESS_COOKIE]);-- const claims = userAuth.verify(token);++ const claims = await userAuth.verify(token);if (!claims) return res.status(401).json({ error: 'Not authenticated' });res.json({ user: claimsToUser(claims), expires_at: claims.exp ? claims.exp * 1000 : null });});@@@ -203,7 +203,7 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }try {const data = await tokenGrant({ grant_type: 'refresh_token', refresh_token: refresh });setSession(res, data.access_token, data.refresh_token);-- return res.json({ token: data.access_token, user: claimsToUser(userAuth.verify(data.access_token)) });++ return res.json({ token: data.access_token, user: claimsToUser(await userAuth.verify(data.access_token)) });} catch (err) {if (err.status && err.status < 500) { clearSession(res); return res.status(401).json({ error: 'Refresh token rejected — please sign in again' }); }return res.status(502).json({ error: 'Could not reach OpenVibe.Network' });
@@ -13,14 +13,14 @@
@@ -13,14 +13,14 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "jsonwebtoken": "^9.0.2",
"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.38.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"
},
},
"devDependencies": {
"devDependencies": {
- "@electric-sql/pglite": "^0.5.8"
+ "@electric-sql/pglite": "^0.5.8",
+ "jsonwebtoken": "^9.0.2"
},
},
"engines": {
"engines": {
"node": ">=22.0.0"
"node": ">=22.0.0"
@@ -30,7 +30,6 @@
@@ -30,7 +30,6 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "jsonwebtoken": "^9.0.2",
"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.38.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",
@@ -40,6 +39,7 @@
@@ -40,6 +39,7 @@
"node": ">=22.0.0"
"node": ">=22.0.0"
},
},
"devDependencies": {
"devDependencies": {
- "@electric-sql/pglite": "^0.5.8"
+ "@electric-sql/pglite": "^0.5.8",
+ "jsonwebtoken": "^9.0.2"
}
}
}
}
@@ -19,7 +19,8 @@
@@ -19,7 +19,8 @@
* A request that presents a token is judged on that token alone: a bad one is refused, never
* A request that presents a token is judged on that token alone: a bad one is refused, never
* downgraded to anonymous.
* downgraded to anonymous.
*/
*/
-const { serviceAuth, capabilities, http, ids, staff: staffMap } = require('openvibe-contracts');
+const { capabilities, http, ids, staff: staffMap } = require('openvibe-contracts');
+const { verifyService } = require('../network');
const PRINCIPAL_SUB = /^(svc|app|mod):/;
const PRINCIPAL_SUB = /^(svc|app|mod):/;
const ANON = Object.freeze({ kind: 'anonymous' });
const ANON = Object.freeze({ kind: 'anonymous' });
@@ -44,27 +45,28 @@ function userPrincipal(claims) {
@@ -44,27 +45,28 @@ function userPrincipal(claims) {
}
}
function createApiAuth({ config, keys, userAuth }) {
function createApiAuth({ config, keys, userAuth }) {
- function resolve(req) {
+ async function resolve(req) {
const header = String(req.headers.authorization || '');
const header = String(req.headers.authorization || '');
if (!header.startsWith('Bearer ')) return { principal: ANON };
if (!header.startsWith('Bearer ')) return { principal: ANON };
const token = header.slice(7).trim();
const token = header.slice(7).trim();
- const publicKey = keys.get();
- if (!publicKey) return { error: [503, 'identity.unavailable', 'the Network signing key is not loaded yet'] };
+ if (!keys.ready()) return { error: [503, 'identity.unavailable', 'the Network signing key is not loaded yet'] };
const payload = decodePayload(token);
const payload = decodePayload(token);
if (payload && typeof payload.sub === 'string' && PRINCIPAL_SUB.test(payload.sub)) {
if (payload && typeof payload.sub === 'string' && PRINCIPAL_SUB.test(payload.sub)) {
- const r = serviceAuth.verifyServiceToken(token, { publicKey, issuer: config.network.issuer, audience: config.audience });
+ let r;
+ try { r = await verifyService(config, keys, token); } catch { return { error: [503, 'identity.unavailable', 'the Network signing keys could not be read'] }; }
if (!r.ok) return { error: [401, r.code, r.reason] };
if (!r.ok) return { error: [401, r.code, r.reason] };
return { principal: { kind: 'service', sub: r.claims.sub, cap: r.claims.cap || [], jti: r.claims.jti } };
return { principal: { kind: 'service', sub: r.claims.sub, cap: r.claims.cap || [], jti: r.claims.jti } };
}
}
- const claims = userAuth.verify(token);
+ const claims = await userAuth.verify(token);
if (!claims) return { error: [401, 'token.invalid', 'the user token is invalid or expired'] };
if (!claims) return { error: [401, 'token.invalid', 'the user token is invalid or expired'] };
const p = userPrincipal(claims);
const p = userPrincipal(claims);
if (!p) return { error: [403, 'identity.no_subject', 'this account has no canonical subject yet; sign in again'] };
if (!p) return { error: [403, 'identity.no_subject', 'this account has no canonical subject yet; sign in again'] };
return { principal: p };
return { principal: p };
}
}
- function middleware(req, res, next) {
- const r = resolve(req);
+ async function middleware(req, res, next) {
+ let r;
+ try { r = await resolve(req); } catch (err) { return next(err); }
if (r.error) return http.sendProblem(res, r.error[0], r.error[1], { detail: r.error[2], ctx: req.ov });
if (r.error) return http.sendProblem(res, r.error[0], r.error[1], { detail: r.error[2], ctx: req.ov });
req.principal = r.principal;
req.principal = r.principal;
return next();
return next();
@@ -1,44 +1,37 @@
@@ -1,44 +1,37 @@
'use strict';
'use strict';
/**
/**
- * OpenVibe.Network client (the same shape as OpenVibe.Tips and OpenVibe.Community).
+ * OpenVibe.Network client.
*
*
- * keys the Network's RS256 public key (OV_NETWORK_PUBLIC_KEY, else GET /api/.well-known/jwks,
- * refreshed every 6 h and retried every 30 s until it loads). It verifies service tokens
- * (audience openvibe.vip) and the browser's Network user JWT, both offline.
+ * keys the Network's RS256 signing keys through openvibe-sdk/auth: one process-wide JWKS client per URL keeps
+ * them fresh, serves the last good keys through a Network outage, backs off, and refetches at once when a
+ * token names a key it does not have (a rotation). OV_NETWORK_PUBLIC_KEY pins one key instead. Service
+ * tokens (audience openvibe.vip) and the browser's Network user JWT are both verified offline.
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
-const jwt = require('jsonwebtoken');
+const contracts = require('openvibe-contracts');
+const { jwksClient, verifyUserToken, verifyServiceToken } = require('openvibe-sdk/auth');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
function createKeyProvider(config, { fetchImpl = globalThis.fetch, log = console } = {}) {
function createKeyProvider(config, { fetchImpl = globalThis.fetch, log = console } = {}) {
- let pem = config.network.publicKey ? crypto.createPublicKey(config.network.publicKey).export({ type: 'spki', format: 'pem' }) : null;
- let timer = null;
- async function load() {
- const url = `${config.network.internalUrl}/api/.well-known/jwks`;
- try {
- const res = await fetchImpl(url, { signal: AbortSignal.timeout(8000) });
- if (!res.ok) throw new Error(`JWKS ${res.status}`);
- const body = await res.json();
- const jwk = (body.keys || []).find((k) => k.kty === 'RSA');
- if (jwk) pem = crypto.createPublicKey({ key: jwk, format: 'jwk' }).export({ type: 'spki', format: 'pem' });
- else if (typeof body.public_key === 'string') pem = crypto.createPublicKey(body.public_key).export({ type: 'spki', format: 'pem' });
- else throw new Error('JWKS contained no keys');
- return pem;
- } catch (e) {
- log.warn(`[VIP] Network key not loaded from ${url}: ${e.message}`);
- return null;
- }
- }
- function start() {
- if (config.network.publicKey || timer) return;
- const retry = () => load().then((k) => { if (!k) setTimeout(retry, 30_000).unref(); });
- retry();
- timer = setInterval(async () => { await load(); }, 6 * 60 * 60 * 1000);
- timer.unref();
- }
- function stop() { if (timer) clearInterval(timer); timer = null; }
- return { get: () => pem, load, start, stop };
+ const url = `${config.network.internalUrl}/api/.well-known/jwks`;
+ const pinned = config.network.publicKey ? crypto.createPublicKey(config.network.publicKey) : null;
+ const client = pinned ? null : jwksClient(url, { fetch: fetchImpl, log });
+ /** Where a verification takes its keys from (openvibe-sdk/auth options). */
+ const source = pinned ? { publicKey: pinned } : { jwks: url, fetch: fetchImpl };
+ return {
+ source,
+ ready: () => (pinned ? true : client.status().ready),
+ /** Fetch the keys now (boot, tests); true when there is a key to verify with. */
+ async load() { if (client) await client.refresh().catch(() => {}); return pinned ? true : client.status().ready; },
+ start() { if (client) client.start(); },
+ stop() { if (client) client.stop(); },
+ };
+}
+
+/** A service principal's token (audience openvibe.vip): contracts' result { ok, code, reason, claims }. */
+async function verifyService(config, keys, token) {
+ return await verifyServiceToken(token, { ...keys.source, issuer: config.network.issuer, audience: config.audience, contracts });
}
}
/**
/**
@@ -54,19 +47,15 @@ function createUserAuth(config, keys) {
@@ -54,19 +47,15 @@ function createUserAuth(config, keys) {
authBase: config.network.url,
authBase: config.network.url,
internalBase: config.network.internalUrl,
internalBase: config.network.internalUrl,
});
});
- /** Claims of a valid user token, or null. Service tokens are never user tokens. */
- function verify(token) {
+ /**
+ * Claims of a valid user token, or null. openvibe-sdk/auth refuses service, app and mod principals and typed
+ * tokens (a realtime ticket, an export token): none of them is a session.
+ */
+ async function verify(token) {
if (!token) return null;
if (!token) return null;
- const key = keys.get();
- if (!key) return null;
- let claims;
- try { claims = jwt.verify(token, key, { algorithms: ['RS256'], issuer: config.network.issuer }); } catch { return null; }
- if (!claims || typeof claims !== 'object') return null;
- if (typeof claims.sub === 'string' && /^(svc|app|mod):/.test(claims.sub)) return null;
- if (claims.actor_type === 'service') return null;
- return claims;
+ try { return await verifyUserToken(token, { ...keys.source, issuer: config.network.issuer }); } catch { return null; }
}
}
return { client, verify };
return { client, verify };
}
}
-module.exports = { createKeyProvider, createUserAuth };
+module.exports = { createKeyProvider, createUserAuth, verifyService };
@@ -33,7 +33,7 @@ function createVipReadiness({ db, keys, config, outbox, now, release = null, fet
@@ -33,7 +33,7 @@ function createVipReadiness({ db, keys, config, outbox, now, release = null, fet
{ name: 'valkey', required: false, check: async () => (valkey ? valkey.ready() : { skipped: 'VALKEY_URL not set: per-actor limits count in this process only' }) },
{ name: 'valkey', required: false, check: async () => (valkey ? valkey.ready() : { skipped: 'VALKEY_URL not set: per-actor limits count in this process only' }) },
{
{
name: 'network_jwks', required: false,
name: 'network_jwks', required: false,
- check: () => (keys.get() ? true : 'Network signing key not loaded yet: tokens cannot be verified'),
+ check: () => (keys.ready() ? true : 'Network signing key not loaded yet: tokens cannot be verified'),
},
},
{
{
name: 'billing', required: false, cacheMs: 15_000, timeoutMs: 2500,
name: 'billing', required: false, cacheMs: 15_000, timeoutMs: 2500,
@@ -63,9 +63,9 @@ function withParam(target, key, value) {
@@ -63,9 +63,9 @@ function withParam(target, key, value) {
/** Page middleware: req.viewer = the signed-in person (principal shape) or null. Never blocks. */
/** Page middleware: req.viewer = the signed-in person (principal shape) or null. Never blocks. */
function viewerMiddleware(userAuth) {
function viewerMiddleware(userAuth) {
- return (req, _res, next) => {
+ return async (req, _res, next) => {
const token = req.cookies && req.cookies[ACCESS_COOKIE];
const token = req.cookies && req.cookies[ACCESS_COOKIE];
- const claims = token ? userAuth.verify(token) : null;
+ const claims = token ? await userAuth.verify(token) : null;
req.viewer = claims ? userPrincipal(claims) : null;
req.viewer = claims ? userPrincipal(claims) : null;
req.viewerClaims = claims ? claimsToUser(claims) : null;
req.viewerClaims = claims ? claimsToUser(claims) : null;
next();
next();
@@ -116,9 +116,9 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }
@@ -116,9 +116,9 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }
throw lastErr || new Error('Network unreachable');
throw lastErr || new Error('Network unreachable');
}
}
- router.get('/login', (req, res) => {
+ router.get('/login', async (req, res) => {
const silent = !!req.query.silent && req.query.silent !== '0';
const silent = !!req.query.silent && req.query.silent !== '0';
- if (silent && userAuth.verify(req.cookies && req.cookies[ACCESS_COOKIE])) { clearFlow(res); return res.redirect(sanitizeNext(req.query.next, config)); }
+ if (silent && await userAuth.verify(req.cookies && req.cookies[ACCESS_COOKIE])) { clearFlow(res); return res.redirect(sanitizeNext(req.query.next, config)); }
const { url, state } = userAuth.client.getAuthorizationUrl(config.oauth.scope);
const { url, state } = userAuth.client.getAuthorizationUrl(config.oauth.scope);
let target = url;
let target = url;
if (silent) { const u = new URL(url); u.searchParams.set('prompt', 'none'); target = u.toString(); }
if (silent) { const u = new URL(url); u.searchParams.set('prompt', 'none'); target = u.toString(); }
@@ -167,7 +167,7 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }
@@ -167,7 +167,7 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }
const data = await tokenGrant({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: token });
const data = await tokenGrant({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: token });
if (!data.access_token) throw Object.assign(new Error('no token'), { status: 401, error: 'invalid_grant' });
if (!data.access_token) throw Object.assign(new Error('no token'), { status: 401, error: 'invalid_grant' });
setSession(res, data.access_token, data.refresh_token);
setSession(res, data.access_token, data.refresh_token);
- return res.json({ ok: true, user: data.user || claimsToUser(userAuth.verify(data.access_token)) });
+ return res.json({ ok: true, user: data.user || claimsToUser(await userAuth.verify(data.access_token)) });
} catch (err) {
} catch (err) {
if (err.status && err.status < 500) return res.status(401).json({ error: err.error || 'invalid_grant' });
if (err.status && err.status < 500) return res.status(401).json({ error: err.error || 'invalid_grant' });
return res.status(502).json({ error: 'server_error', error_description: 'Could not reach OpenVibe.Network' });
return res.status(502).json({ error: 'server_error', error_description: 'Could not reach OpenVibe.Network' });
@@ -190,9 +190,9 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }
@@ -190,9 +190,9 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }
res.redirect(sanitizeNext(req.query.next, config));
res.redirect(sanitizeNext(req.query.next, config));
});
});
- router.get('/me', (req, res) => {
+ router.get('/me', async (req, res) => {
const token = (String(req.headers.authorization || '').startsWith('Bearer ') ? req.headers.authorization.slice(7) : null) || (req.cookies && req.cookies[ACCESS_COOKIE]);
const token = (String(req.headers.authorization || '').startsWith('Bearer ') ? req.headers.authorization.slice(7) : null) || (req.cookies && req.cookies[ACCESS_COOKIE]);
- const claims = userAuth.verify(token);
+ const claims = await userAuth.verify(token);
if (!claims) return res.status(401).json({ error: 'Not authenticated' });
if (!claims) return res.status(401).json({ error: 'Not authenticated' });
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 });
});
});
@@ -203,7 +203,7 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }
@@ -203,7 +203,7 @@ function createSessionRoutes(config, userAuth, { fetchImpl = globalThis.fetch }
try {
try {
const data = await tokenGrant({ grant_type: 'refresh_token', refresh_token: refresh });
const data = await tokenGrant({ grant_type: 'refresh_token', refresh_token: refresh });
setSession(res, data.access_token, data.refresh_token);
setSession(res, data.access_token, data.refresh_token);
- return res.json({ token: data.access_token, user: claimsToUser(userAuth.verify(data.access_token)) });
+ return res.json({ token: data.access_token, user: claimsToUser(await userAuth.verify(data.access_token)) });
} catch (err) {
} catch (err) {
if (err.status && err.status < 500) { clearSession(res); return res.status(401).json({ error: 'Refresh token rejected — please sign in again' }); }
if (err.status && err.status < 500) { clearSession(res); return res.status(401).json({ error: 'Refresh token rejected — please sign in again' }); }
return res.status(502).json({ error: 'Could not reach OpenVibe.Network' });
return res.status(502).json({ error: 'Could not reach OpenVibe.Network' });