openvibers/openvibe.network Commits

Sign-in goes straight through for a first-party site when one account is saved in this browser (no chooser to click); the chooser stays for third-party apps, several accounts and prompt=select_account (#324)

OP OpenVibers committed 19 days ago · e647a5d5d4 · 3 files, +72 −0

Changes

ddiff --git a/public/js/login.js b/public/js/login.js
iindex 346ee70a..3654e263 100644
---- a/public/js/login.js
++++ b/public/js/login.js
@@@ -21,6 +21,7 @@ if (isOAuth) {
clientHost = info.third_party ? info.redirect_host || '' : '';
renderChooserSubtitle();
renderConsent(info);
++ maybeContinue(info);
})
.catch(() => {});
}
@@@ -228,6 +229,23 @@ function showResetForm(token) {
setActivePanel('panel-reset', { showTabs: false, showExtras: false });
}
++// ── Straight through ────────────────────────────────────────
++// A first-party site's sign-in with one account saved in this browser (the one signed in) continues as it: nothing to
++// click, the same as the silent sign-in sites try first. The chooser stays for a third-party app (its consent), for
++// several saved accounts, and when the site asks for it (prompt=select_account or login: "use another account").
++let autoContinued = false;
++function maybeContinue(info) {
++ if (autoContinued || !isOAuth || !info || info.third_party) return;
++ if (['select_account', 'login'].includes(params.get('prompt') || '')) return;
++ const accounts = getSavedAccounts();
++ const activeId = localStorage.getItem('openvibe_active_account');
++ if (accounts.length !== 1 || accounts[0].is_anon || (activeId && String(accounts[0].id) !== String(activeId))) return;
++ autoContinued = true;
++ const subtitle = document.getElementById('chooser-subtitle');
++ if (subtitle) subtitle.textContent = `Continuing to ${info.name} as ${accounts[0].display_name || accounts[0].username}…`;
++ selectAccount(accounts[0]);
++}
++
// ── Render Account Chooser ──────────────────────────────────
function renderChooserSubtitle() {
const subtitle = document.getElementById('chooser-subtitle');
ddiff --git a/server/auth/oauth-routes.js b/server/auth/oauth-routes.js
iindex d718e665..9458fd10 100644
---- a/server/auth/oauth-routes.js
++++ b/server/auth/oauth-routes.js
@@@ -121,6 +121,8 @@ router.get('/authorize', async (req, res) => {
});
if (pkce.challenge) { loginParams.set('code_challenge', pkce.challenge); loginParams.set('code_challenge_method', pkce.method); }
if (nonce) loginParams.set('nonce', nonce);
++ // The site asked for the chooser ("use another account"): the page shows it even with one account saved.
++ if (['select_account', 'login'].includes(String(req.query.prompt || ''))) loginParams.set('prompt', String(req.query.prompt));
res.redirect(`${getConfig(req).loginUrl}/login?${loginParams.toString()}`);
});
ddiff --git a/test/login-auto-continue.test.js b/test/login-auto-continue.test.js
nnew file mode 100644
iindex 00000000..1bd89b64
---- /dev/null
++++ b/test/login-auto-continue.test.js
@@@ -0,0 +1,52 @@
++'use strict';
++// Straight through on /login (public/js/login.js maybeContinue): a first-party site's sign-in with one account saved in
++// this browser continues as it with nothing to click; a third-party app, several accounts, another active account, an
++// anonymous one, or a site asking for the chooser (prompt=select_account | login) still shows it. And /oauth/authorize
++// passes that prompt on to the page. Runs the function itself with stand-ins for the page.
++const assert = require('assert');
++const fs = require('fs');
++const path = require('path');
++const vm = require('vm');
++
++const js = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'login.js'), 'utf8');
++const start = js.indexOf('let autoContinued = false;');
++const fnStart = js.indexOf('function maybeContinue(info) {', start);
++assert.ok(start > 0 && fnStart > start, 'maybeContinue exists');
++let depth = 0, end = fnStart;
++for (let i = js.indexOf('{', fnStart); i < js.length; i++) {
++ if (js[i] === '{') depth++;
++ else if (js[i] === '}' && --depth === 0) { end = i + 1; break; }
++}
++const source = js.slice(start, end);
++assert.match(js, /renderConsent\(info\);\n\s*maybeContinue\(info\);/, 'it runs once the Network names the app');
++
++const me = { id: 7, username: 'goosely', display_name: 'goosely', token: 't' };
++function run({ info = { name: 'OpenVibe.Agents', third_party: false }, accounts = [me], active = '7', query = 'client_id=actor&redirect_uri=x' } = {}) {
++ const picked = [];
++ const subtitle = { textContent: '' };
++ const params = new URLSearchParams(query);
++ vm.runInNewContext(`${source}; maybeContinue(info); maybeContinue(info);`, {
++ info, params, isOAuth: params.has('client_id') && params.has('redirect_uri'),
++ getSavedAccounts: () => accounts, selectAccount: (a) => picked.push(a.id),
++ localStorage: { getItem: (k) => (k === 'openvibe_active_account' ? active : null) },
++ document: { getElementById: (id) => (id === 'chooser-subtitle' ? subtitle : null) },
++ });
++ return { picked, subtitle: subtitle.textContent };
++}
++
++let r = run();
++assert.deepStrictEqual(r.picked, [7], 'one saved account, a first-party site: continues once, as it');
++assert.strictEqual(r.subtitle, 'Continuing to OpenVibe.Agents as goosely…');
++assert.deepStrictEqual(run({ active: null }).picked, [7], 'no active account recorded: the only one');
++assert.deepStrictEqual(run({ info: { name: 'ClipTool', third_party: true } }).picked, [], 'a third-party app: the person chooses (and sees its consent)');
++assert.deepStrictEqual(run({ accounts: [me, { id: 8, username: 'alt', token: 'u' }] }).picked, [], 'several accounts: the chooser');
++assert.deepStrictEqual(run({ active: '9' }).picked, [], 'another account is the active one: the chooser');
++assert.deepStrictEqual(run({ accounts: [{ ...me, is_anon: true }] }).picked, [], 'an anonymous account is never assumed');
++assert.deepStrictEqual(run({ query: 'client_id=actor&redirect_uri=x&prompt=select_account' }).picked, [], 'asked for the chooser');
++assert.deepStrictEqual(run({ query: 'client_id=actor&redirect_uri=x&prompt=login' }).picked, []);
++assert.deepStrictEqual(run({ query: 'next=/my' }).picked, [], 'not an app sign-in');
++
++const routes = fs.readFileSync(path.join(__dirname, '..', 'server', 'auth', 'oauth-routes.js'), 'utf8');
++assert.match(routes, /\['select_account', 'login'\]\.includes\(String\(req\.query\.prompt \|\| ''\)\)\) loginParams\.set\('prompt'/, '/oauth/authorize passes the prompt to the page');
++
++console.log('login auto-continue: all checks passed');