openvibers/openvibe.live Commits
Sign-in: Switch account opens the Network's chooser without signing out; the navbar's accounts and Add another account come back to the page; /api/auth/sso/login passes prompt and login_hint (#366)
Changes
@@@ -659,13 +659,11 @@ function _checkNetworkSession() {timer = setTimeout(done, 4000);}/** "Switch account": drop this site's session, keep openvibe.network's, open the account chooser. */++/** Switch account: OpenVibe.Network's chooser (every account this browser holds), back here as the one chosen. Nothing++ * is signed out first, so closing the chooser keeps this session. */function switchAccount() {-- fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {}).finally(() => {-- localStorage.removeItem('token'); localStorage.removeItem('ov_token');-- document.cookie = 'token=;Max-Age=0;path=/'; document.cookie = 'ov_token=;Max-Age=0;path=/';-- _setSsoHint('account');-- location.href = '/api/auth/sso/login';-- });++ _setSsoHint('account');++ location.href = '/api/auth/sso/login?prompt=select_account&next=' + encodeURIComponent(location.pathname + location.search);}/** "Browse as guest": signed out here only — openvibe.network still remembers you, so "Sign in" is one tap. */function browseAsGuest() { logout(); try { toast('Browsing as a guest — Sign in brings you straight back.', 'info'); } catch { /* */ } }@@@ -69,7 +69,9 @@return {service: 'live', mount: '#navbar-mount', className: 'navbar', auth: 'external', apiBase: 'https://openvibe.network',user: user || null, token: (function () { try { return localStorage.getItem('token'); } catch (e) { return null; } })(),-- loginUrl: '/api/auth/sso/login', accounts: false, networkLinks: 0, history: false, silentLogin: false, fedcm: false, recent: false,++ // The menu's other accounts and "Add another account" go through the Network's chooser and come back through++ // this sign-in (Shared 4.13.0), which returns to the page they were on.++ loginUrl: '/api/auth/sso/login?next={path}', networkLinks: 0, history: false, silentLogin: false, fedcm: false, recent: false,notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)links: links(user), menu: menu(user),chips: [@@@ -3,7 +3,7 @@* All authentication is via OpenVibe.Tools OAuth2 SSO.* No local login/register — users sign in at openvibe.network.*-- * GET /api/auth/sso/login — Redirect to openvibe.network OAuth++ * GET /api/auth/sso/login — Redirect to openvibe.network OAuth (?silent=1, ?prompt=select_account|login, ?login_hint, ?next)* GET /api/auth/callback — OAuth callback* GET /api/auth/me — Current user* PUT /api/auth/profile — Update profile@@@ -319,6 +319,11 @@ router.get('/sso/login', async (req, res) => {// ?silent=1: continue as the account openvibe.network already knows (no chooser); if the// network has no session either, it bounces back with error=login_required and we stay quiet.if (req.query.silent) params.set('prompt', 'none');++ // "Switch account" (the chooser) and "use another account" (the sign-in form); login_hint names the account to++ // continue as when this browser has it (Network docs/sign-in.md).++ else if (['select_account', 'login'].includes(String(req.query.prompt || ''))) params.set('prompt', String(req.query.prompt));++ const hint = String(req.query.login_hint || '').trim();++ if (/^@?[A-Za-z0-9_.-]{1,64}$/.test(hint)) params.set('login_hint', hint);res.redirect(`${getNetworkBase()}/oauth/authorize?${params.toString()}`);});nnew file mode 100644@@@ -0,0 +1,53 @@++'use strict';++// Live's sign-in (GET /api/auth/sso/login) passes "switch account" and "use another account" on to OpenVibe.Network:++// ?prompt=select_account shows the chooser, ?prompt=login the sign-in form, ?login_hint=<username> names the account the++// chooser continues as (owner, 2026-10-26: sign-in "like the Google sign-in does"; Network docs/sign-in.md). ?silent=1++// stays prompt=none, and anything else is dropped. The menu's "Switch account" opens the chooser without signing out.++//++// node test/sso-login-prompt.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++process.env.NODE_ENV = 'test';++const quiet = console.log;++console.log = (...a) => { if (!/^\[/.test(String(a[0]))) quiet(...a); };++console.warn = () => {};++++(async () => {++ const express = require('express');++ const cookieParser = require('cookie-parser');++ const app = express();++ app.use(cookieParser());++ app.use('/api/auth', require('../server/auth/routes'));++ const server = require('http').createServer(app).listen(0);++ if (!server.listening) await new Promise((r) => server.once('listening', r));++ const go = async (q) => {++ const res = await fetch(`http://127.0.0.1:${server.address().port}/api/auth/sso/login${q}`, { redirect: 'manual' });++ assert.strictEqual(res.status, 302, q);++ return new URL(res.headers.get('location'));++ };++++ let u = await go('?prompt=select_account&login_hint=%40alt&next=%2Fdashboard');++ assert.strictEqual(u.pathname, '/oauth/authorize');++ assert.deepStrictEqual([u.searchParams.get('prompt'), u.searchParams.get('login_hint')], ['select_account', '@alt']);++ u = await go('?prompt=login');++ assert.strictEqual(u.searchParams.get('prompt'), 'login');++ u = await go('?prompt=consent&login_hint=%3Cb%3E');++ assert.deepStrictEqual([u.searchParams.get('prompt'), u.searchParams.get('login_hint')], [null, null], 'anything else is dropped');++ u = await go('?silent=1&prompt=login');++ assert.strictEqual(u.searchParams.get('prompt'), 'none', 'silent stays silent');++ u = await go('');++ assert.strictEqual(u.searchParams.get('prompt'), null, 'a plain sign-in: the Network shows its chooser');++ server.close();++++ // The menu's Switch account keeps this session until another account is chosen.++ const appJs = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');++ const sw = appJs.slice(appJs.indexOf('function switchAccount()'), appJs.indexOf('function browseAsGuest()'));++ assert.ok(/prompt=select_account&next=/.test(sw) && !/logout|removeItem/.test(sw), 'Switch account opens the chooser, signing nothing out');++ const nav = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'ov-navbar-live.js'), 'utf8');++ assert.ok(/loginUrl: '\/api\/auth\/sso\/login\?next=\{path\}'/.test(nav) && !/accounts: false/.test(nav), "the navbar's accounts come back through this sign-in, to the page they were on");++++ quiet('sso login prompt: all checks passed');++ process.exit(0);++})().catch((err) => { quiet(err); process.exit(1); });
@@ -659,13 +659,11 @@ function _checkNetworkSession() {
@@ -659,13 +659,11 @@ function _checkNetworkSession() {
timer = setTimeout(done, 4000);
timer = setTimeout(done, 4000);
}
}
/** "Switch account": drop this site's session, keep openvibe.network's, open the account chooser. */
/** "Switch account": drop this site's session, keep openvibe.network's, open the account chooser. */
+/** Switch account: OpenVibe.Network's chooser (every account this browser holds), back here as the one chosen. Nothing
+ * is signed out first, so closing the chooser keeps this session. */
function switchAccount() {
function switchAccount() {
- fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }).catch(() => {}).finally(() => {
- localStorage.removeItem('token'); localStorage.removeItem('ov_token');
- document.cookie = 'token=;Max-Age=0;path=/'; document.cookie = 'ov_token=;Max-Age=0;path=/';
- _setSsoHint('account');
- location.href = '/api/auth/sso/login';
- });
+ _setSsoHint('account');
+ location.href = '/api/auth/sso/login?prompt=select_account&next=' + encodeURIComponent(location.pathname + location.search);
}
}
/** "Browse as guest": signed out here only — openvibe.network still remembers you, so "Sign in" is one tap. */
/** "Browse as guest": signed out here only — openvibe.network still remembers you, so "Sign in" is one tap. */
function browseAsGuest() { logout(); try { toast('Browsing as a guest — Sign in brings you straight back.', 'info'); } catch { /* */ } }
function browseAsGuest() { logout(); try { toast('Browsing as a guest — Sign in brings you straight back.', 'info'); } catch { /* */ } }
@@ -69,7 +69,9 @@
@@ -69,7 +69,9 @@
return {
return {
service: 'live', mount: '#navbar-mount', className: 'navbar', auth: 'external', apiBase: 'https://openvibe.network',
service: 'live', mount: '#navbar-mount', className: 'navbar', auth: 'external', apiBase: 'https://openvibe.network',
user: user || null, token: (function () { try { return localStorage.getItem('token'); } catch (e) { return null; } })(),
user: user || null, token: (function () { try { return localStorage.getItem('token'); } catch (e) { return null; } })(),
- loginUrl: '/api/auth/sso/login', accounts: false, networkLinks: 0, history: false, silentLogin: false, fedcm: false, recent: false,
+ // The menu's other accounts and "Add another account" go through the Network's chooser and come back through
+ // this sign-in (Shared 4.13.0), which returns to the page they were on.
+ loginUrl: '/api/auth/sso/login?next={path}', networkLinks: 0, history: false, silentLogin: false, fedcm: false, recent: false,
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
links: links(user), menu: menu(user),
links: links(user), menu: menu(user),
chips: [
chips: [
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
* All authentication is via OpenVibe.Tools OAuth2 SSO.
* All authentication is via OpenVibe.Tools OAuth2 SSO.
* No local login/register — users sign in at openvibe.network.
* No local login/register — users sign in at openvibe.network.
*
*
- * GET /api/auth/sso/login — Redirect to openvibe.network OAuth
+ * GET /api/auth/sso/login — Redirect to openvibe.network OAuth (?silent=1, ?prompt=select_account|login, ?login_hint, ?next)
* GET /api/auth/callback — OAuth callback
* GET /api/auth/callback — OAuth callback
* GET /api/auth/me — Current user
* GET /api/auth/me — Current user
* PUT /api/auth/profile — Update profile
* PUT /api/auth/profile — Update profile
@@ -319,6 +319,11 @@ router.get('/sso/login', async (req, res) => {
@@ -319,6 +319,11 @@ router.get('/sso/login', async (req, res) => {
// ?silent=1: continue as the account openvibe.network already knows (no chooser); if the
// ?silent=1: continue as the account openvibe.network already knows (no chooser); if the
// network has no session either, it bounces back with error=login_required and we stay quiet.
// network has no session either, it bounces back with error=login_required and we stay quiet.
if (req.query.silent) params.set('prompt', 'none');
if (req.query.silent) params.set('prompt', 'none');
+ // "Switch account" (the chooser) and "use another account" (the sign-in form); login_hint names the account to
+ // continue as when this browser has it (Network docs/sign-in.md).
+ else if (['select_account', 'login'].includes(String(req.query.prompt || ''))) params.set('prompt', String(req.query.prompt));
+ const hint = String(req.query.login_hint || '').trim();
+ if (/^@?[A-Za-z0-9_.-]{1,64}$/.test(hint)) params.set('login_hint', hint);
res.redirect(`${getNetworkBase()}/oauth/authorize?${params.toString()}`);
res.redirect(`${getNetworkBase()}/oauth/authorize?${params.toString()}`);
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
+'use strict';
+// Live's sign-in (GET /api/auth/sso/login) passes "switch account" and "use another account" on to OpenVibe.Network:
+// ?prompt=select_account shows the chooser, ?prompt=login the sign-in form, ?login_hint=<username> names the account the
+// chooser continues as (owner, 2026-10-26: sign-in "like the Google sign-in does"; Network docs/sign-in.md). ?silent=1
+// stays prompt=none, and anything else is dropped. The menu's "Switch account" opens the chooser without signing out.
+//
+// node test/sso-login-prompt.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+process.env.NODE_ENV = 'test';
+const quiet = console.log;
+console.log = (...a) => { if (!/^\[/.test(String(a[0]))) quiet(...a); };
+console.warn = () => {};
+
+(async () => {
+ const express = require('express');
+ const cookieParser = require('cookie-parser');
+ const app = express();
+ app.use(cookieParser());
+ app.use('/api/auth', require('../server/auth/routes'));
+ const server = require('http').createServer(app).listen(0);
+ if (!server.listening) await new Promise((r) => server.once('listening', r));
+ const go = async (q) => {
+ const res = await fetch(`http://127.0.0.1:${server.address().port}/api/auth/sso/login${q}`, { redirect: 'manual' });
+ assert.strictEqual(res.status, 302, q);
+ return new URL(res.headers.get('location'));
+ };
+
+ let u = await go('?prompt=select_account&login_hint=%40alt&next=%2Fdashboard');
+ assert.strictEqual(u.pathname, '/oauth/authorize');
+ assert.deepStrictEqual([u.searchParams.get('prompt'), u.searchParams.get('login_hint')], ['select_account', '@alt']);
+ u = await go('?prompt=login');
+ assert.strictEqual(u.searchParams.get('prompt'), 'login');
+ u = await go('?prompt=consent&login_hint=%3Cb%3E');
+ assert.deepStrictEqual([u.searchParams.get('prompt'), u.searchParams.get('login_hint')], [null, null], 'anything else is dropped');
+ u = await go('?silent=1&prompt=login');
+ assert.strictEqual(u.searchParams.get('prompt'), 'none', 'silent stays silent');
+ u = await go('');
+ assert.strictEqual(u.searchParams.get('prompt'), null, 'a plain sign-in: the Network shows its chooser');
+ server.close();
+
+ // The menu's Switch account keeps this session until another account is chosen.
+ const appJs = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'app.js'), 'utf8');
+ const sw = appJs.slice(appJs.indexOf('function switchAccount()'), appJs.indexOf('function browseAsGuest()'));
+ assert.ok(/prompt=select_account&next=/.test(sw) && !/logout|removeItem/.test(sw), 'Switch account opens the chooser, signing nothing out');
+ const nav = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'ov-navbar-live.js'), 'utf8');
+ assert.ok(/loginUrl: '\/api\/auth\/sso\/login\?next=\{path\}'/.test(nav) && !/accounts: false/.test(nav), "the navbar's accounts come back through this sign-in, to the page they were on");
+
+ quiet('sso login prompt: all checks passed');
+ process.exit(0);
+})().catch((err) => { quiet(err); process.exit(1); });