openvibers/openrestream Commits

Enable silent first-visit sign-in on OpenRestream (#236)

OP OpenVibers committed 5 days ago · 57b48bdac2 · 3 files, +38 −2

Changes

ddiff --git a/server/auth/sso.js b/server/auth/sso.js
iindex 19fa344..300cf04 100644
---- a/server/auth/sso.js
++++ b/server/auth/sso.js
@@@ -13,7 +13,7 @@
*/
const crypto = require('crypto');
const express = require('express');
--const { createSsoClient, failurePage } = require('openvibe-sdk/sso');
++const { createSsoClient, failurePage, withParam } = require('openvibe-sdk/sso');
const ACCESS = 'ov_token';
const REFRESH = 'ov_refresh';
@@@ -72,7 +72,9 @@ function createSsoRoutes({ config, auth, fetchImpl = globalThis.fetch }) {
}
router.get('/login', (req, res) => {
-- const started = sdkSso.authorizeUrl();
++ const silent = !!req.query.silent && req.query.silent !== '0';
++ const started = sdkSso.authorizeUrl({ silent });
++ if (silent) res.cookie('ov_oauth_silent', '1', flow); else res.clearCookie('ov_oauth_silent', { path: '/auth' });
res.cookie(STATE, started.state, flow);
res.cookie(VERIFIER, started.verifier, flow);
res.cookie(NEXT, sanitizeNext(req.query.next, config), flow);
@@@ -87,6 +89,9 @@ function createSsoRoutes({ config, auth, fetchImpl = globalThis.fetch }) {
res.clearCookie(STATE, { path: '/auth' });
res.clearCookie(VERIFIER, { path: '/auth' });
res.clearCookie(NEXT, { path: '/auth' });
++ const silent = req.cookies && req.cookies.ov_oauth_silent === '1';
++ res.clearCookie('ov_oauth_silent', { path: '/auth' });
++ if (error && silent && sameString(state, expected)) return res.redirect(withParam(next, 'sso', 'none'));
if (error) return res.redirect(`/?auth_error=${encodeURIComponent(String(error))}`);
const retryHref = `/auth/login${next !== '/' ? `?next=${encodeURIComponent(next)}` : ''}`;
const failed = (status, title, text) => res.status(status).type('html').send(failurePage({
ddiff --git a/server/ui/layout.js b/server/ui/layout.js
iindex 2cf5cef..845a730 100644
---- a/server/ui/layout.js
++++ b/server/ui/layout.js
@@@ -147,6 +147,7 @@ function renderPage({ title, body, user, canonicalPath = '/', robots = 'noindex,
{ label: 'Sessions', href: '/sessions', icon: 'fa-clock-rotate-left' },
],
sessionUrl: '/auth/me',
++ silentLogin: '/auth/login?silent=1&next={url}',
loginUrl: `/auth/login?next=${encodeURIComponent(canonicalPath)}`,
logoutUrl: '/auth/logout?next={path}', // Sign out in the shared navbar ends this site's session too
notificationsRealtime: true, // the bell hears new notifications over OpenVibe.Events (Shared 1.22.0)
ddiff --git a/test/silent-signin.test.js b/test/silent-signin.test.js
nnew file mode 100644
iindex 0000000..276b708
---- /dev/null
++++ b/test/silent-signin.test.js
@@@ -0,0 +1,30 @@
++'use strict';
++const assert = require('node:assert/strict');
++const express = require('express');
++const { createSsoRoutes } = require('../server/auth/sso');
++(async () => {
++ const app = express(); app.use(require('cookie-parser')());
++ app.use('/auth', createSsoRoutes({ config: {
++ baseUrl: 'https://openre.stream', networkUrl: 'https://network.test', networkInternalUrl: 'https://network.test',
++ oauth: { clientId: 'openre', clientSecret: 'test-secret', redirectUri: 'https://openre.stream/auth/callback', scope: 'profile' }, cookies: { secure: true },
++ }, auth: { verifyUser: async () => null }, fetchImpl: () => { throw Error('no token exchange for a guest'); } }));
++ const server = app.listen(0); await new Promise(resolve => server.once('listening', resolve));
++ const get = (path, cookie) => fetch(`http://127.0.0.1:${server.address().port}${path}`, { redirect: 'manual', headers: cookie ? { Cookie: cookie } : {} });
++ try {
++ const start = await get('/auth/login?silent=1&next=%2Fsessions%3Ftab%3Dmine%23latest');
++ const u = new URL(start.headers.get('location'));
++ assert.equal(u.searchParams.get('prompt'), 'none');
++ assert.equal(u.searchParams.get('code_challenge_method'), 'S256');
++ const cookie = start.headers.getSetCookie().map(c => c.split(';')[0]).join('; ');
++ const guest = await get('/auth/callback?error=login_required&state='+u.searchParams.get('state'), cookie);
++ assert.equal(guest.status, 302);
++ assert.equal(guest.headers.get('location'), '/sessions?tab=mine&sso=none#latest');
++ assert.ok(!guest.headers.getSetCookie().some(c => c.startsWith('ov_token=')));
++ const bad = await get('/auth/callback?code=unissued&state=wrong', cookie);
++ assert.equal(bad.status, 400);
++ const normal = await get('/auth/login?silent=0');
++ assert.equal(new URL(normal.headers.get('location')).searchParams.get('prompt'), null);
++ assert.ok(normal.headers.getSetCookie().some(c => c.startsWith('ov_oauth_silent=;')));
++ console.log('silent signin: prompt, PKCE, guest return and state checks passed');
++ } finally { await new Promise(resolve => server.close(resolve)); }
++})().catch(err => { console.error(err); process.exitCode = 1; });