From e8731a37e5738f88e9b88f4a82b440e187aed09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Mon, 16 Dec 2019 14:30:46 +0100 Subject: [PATCH 1/5] Add LTI support, and tidy OIDC code --- server/config/config.example.js | 36 +++++--- server/package.json | 2 + server/server.js | 154 +++++++++++++++++++++++--------- 3 files changed, 138 insertions(+), 54 deletions(-) diff --git a/server/config/config.example.js b/server/config/config.example.js index fee7927..1a55c32 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -2,25 +2,37 @@ const os = require('os'); module.exports = { - // oAuth2 conf - /* auth : + + // Auth conf + /* + auth : { - - // The issuer URL for OpenID Connect discovery - // The OpenID Provider Configuration Document - // could be discovered on: + lti : + { + consumerKey : 'key', + consumerSecret : 'secret' + }, + oidc: + { + // The issuer URL for OpenID Connect discovery + // The OpenID Provider Configuration Document + // could be discovered on: // issuerURL + '/.well-known/openid-configuration' - - // issuerURL : 'https://example.com', - // clientOptions : - // { + + issuerURL : 'https://example.com', + clientOptions : + { client_id : '', client_secret : '', scope : 'openid email profile', - // where client.example.com is your multiparty meeting server + // where client.example.com is your multiparty meeting server redirect_uri : 'https://client.example.com/auth/callback' } - },*/ + + } + }, + */ + redisOptions: {} // session cookie secret cookieSecret : 'T0P-S3cR3t_cook!e', diff --git a/server/package.json b/server/package.json index dbeebd7..a61f15e 100644 --- a/server/package.json +++ b/server/package.json @@ -19,9 +19,11 @@ "express-session": "^1.17.0", "express-socket.io-session": "^1.3.5", "helmet": "^3.21.2", + "ims-lti": "^3.0.2", "mediasoup": "^3.0.12", "openid-client": "^3.7.3", "passport": "^0.4.0", + "passport-lti": "0.0.7", "redis": "^2.8.0", "socket.io": "^2.3.0", "spdy": "^4.0.1" diff --git a/server/server.js b/server/server.js index d8557d4..ff073d6 100755 --- a/server/server.js +++ b/server/server.js @@ -17,14 +17,17 @@ const Room = require('./lib/Room'); const Peer = require('./lib/Peer'); const base64 = require('base-64'); const helmet = require('helmet'); + const { loginHelper, logoutHelper } = require('./httpHelper'); // auth const passport = require('passport'); +const LTIStrategy = require('passport-lti'); +const imsLti = require('ims-lti'); const redis = require('redis'); -const client = redis.createClient(config.redisOptions); +const redisClient = redis.createClient(config.redisOptions); const { Issuer, Strategy } = require('openid-client'); const expressSession = require('express-session'); const RedisStore = require('connect-redis')(expressSession); @@ -87,7 +90,7 @@ const session = expressSession({ name : config.cookieName, resave : true, saveUninitialized : true, - store : new RedisStore({ client }), + store : new RedisStore({ client: redisClient }), cookie : { secure : true, httpOnly : true, @@ -112,49 +115,26 @@ let io; let oidcClient; let oidcStrategy; -const auth = config.auth; - async function run() { - if ( - typeof(auth) !== 'undefined' && - typeof(auth.issuerURL) !== 'undefined' && - typeof(auth.clientOptions) !== 'undefined' - ) + if ( typeof(config.auth) === 'undefined' ) { - Issuer.discover(auth.issuerURL).then(async (oidcIssuer) => - { - // Setup authentication - await setupAuth(oidcIssuer); - - // Run a mediasoup Worker. - await runMediasoupWorkers(); - - // Run HTTPS server. - await runHttpsServer(); - - // Run WebSocketServer. - await runWebSocketServer(); - }) - .catch((err) => - { - logger.error(err); - }); + logger.warn('Auth is not configured properly!'); } else { - logger.error('Auth is not configure properly!'); - - // Run a mediasoup Worker. - await runMediasoupWorkers(); - - // Run HTTPS server. - await runHttpsServer(); - - // Run WebSocketServer. - await runWebSocketServer(); + await setupAuth(); } + // Run a mediasoup Worker. + await runMediasoupWorkers(); + + // Run HTTPS server. + await runHttpsServer(); + + // Run WebSocketServer. + await runWebSocketServer(); + // Log rooms status every 30 seconds. setInterval(() => { @@ -174,21 +154,72 @@ async function run() }, 10000); } -async function setupAuth(oidcIssuer) +function setupLTI(ltiConfig) { - oidcClient = new oidcIssuer.Client(auth.clientOptions); + + // Add redis nonce store + ltiConfig.nonceStore = new imsLti.Stores.RedisStore(ltiConfig.consumerKey, redisClient); + ltiConfig.passReqToCallback= true; + + const ltiStrategy = new LTIStrategy( + ltiConfig, + function(req, lti, done) + { + // LTI launch parameters + if (lti) + { + const user = {}; + + if (lti.user_id && lti.custom_room) + { + user.id = lti.user_id; + user._lti = lti; + } + + if (lti.custom_room) + { + user.room = lti.custom_room; + } + else + { + user.room = ''; + } + if (lti.lis_person_name_full) + { + user.displayName=lti.lis_person_name_full; + } + + // Perform local authentication if necessary + return done(null, user); + + } + else + { + return done('LTI error'); + } + + } + ); + + passport.use('lti', ltiStrategy); +} + +function setupOIDC(oidcIssuer) +{ + + oidcClient = new oidcIssuer.Client(config.auth.oidc.clientOptions); // ... any authorization request parameters go here // client_id defaults to client.client_id // redirect_uri defaults to client.redirect_uris[0] // response type defaults to client.response_types[0], then 'code' // scope defaults to 'openid' - const params = auth.clientOptions; - + const params = config.auth.oidc.clientOptions; + // optional, defaults to false, when true req is passed as a first // argument to verify fn const passReqToCallback = false; - + // optional, defaults to false, when true the code_challenge_method will be // resolved from the issuer configuration, instead of true you may provide // any of the supported values directly, i.e. "S256" (recommended) or "plain" @@ -257,6 +288,31 @@ async function setupAuth(oidcIssuer) passport.use('oidc', oidcStrategy); +} + +async function setupAuth() +{ + // LTI + if ( + typeof(config.auth.lti) !== 'undefined' && + typeof(config.auth.lti.consumerKey) !== 'undefined' && + typeof(config.auth.lti.consumerSecret) !== 'undefined' + ) setupLTI(config.auth.lti); + + // OIDC + if ( + typeof(config.auth.oidc) !== 'undefined' && + typeof(config.auth.oidc.issuerURL) !== 'undefined' && + typeof(config.auth.oidc.clientOptions) !== 'undefined' + ) + { + const oidcIssuer = await Issuer.discover(config.auth.oidc.issuerURL); + + // Setup authentication + setupOIDC(oidcIssuer); + + } + app.use(passport.initialize()); app.use(passport.session()); @@ -270,6 +326,15 @@ async function setupAuth(oidcIssuer) })(req, res, next); }); + // lti launch + app.post('/auth/lti', + passport.authenticate('lti', { failureRedirect: '/' }), + function(req, res) + { + res.redirect(`/${req.user.room}`); + } + ); + // logout app.get('/auth/logout', (req, res) => { @@ -325,6 +390,11 @@ async function runHttpsServer() { if (req.secure) { + if (req.isAuthenticated && req.user && req.user._lti) + { + logger.error(req.user._lti); + } + return next(); } From 78fd6e1b78626dbe88e5d763299fd6c243849621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Thu, 2 Jan 2020 09:48:10 +0100 Subject: [PATCH 2/5] Init displayName according LTI --- app/src/RoomClient.js | 71 ++++++++++++++++++++++--------------------- app/src/index.js | 5 +-- server/lib/Room.js | 21 +++++++++++++ server/server.js | 9 ++++-- 4 files changed, 68 insertions(+), 38 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 682a74b..09b60a6 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -106,7 +106,7 @@ export default class RoomClient } constructor( - { peerId, accessCode, device, useSimulcast, produce, forceTcp } = {}) + { peerId, accessCode, device, useSimulcast, produce, forceTcp, displayName } = {}) { if (!peerId) throw new Error('Missing peerId'); @@ -114,8 +114,8 @@ export default class RoomClient throw new Error('Missing device'); logger.debug( - 'constructor() [peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", forceTcp: "%s"]', - peerId, device.flag, useSimulcast, produce, forceTcp); + 'constructor() [peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", forceTcp: "%s", displayName ""]', + peerId, device.flag, useSimulcast, produce, forceTcp, displayName); this._signalingUrl = null; @@ -128,6 +128,9 @@ export default class RoomClient // Wheter we force TCP this._forceTcp = forceTcp; + // Use displayName + store.dispatch(settingsActions.setDisplayName(displayName)); + // Torrent support this._torrentSupport = null; @@ -493,7 +496,7 @@ export default class RoomClient store.dispatch( meActions.setDisplayNameInProgress(true)); - + try { await this.sendRequest('changeDisplayName', { displayName }); @@ -811,7 +814,7 @@ export default class RoomClient catch (error) { logger.error('unmuteMic() | failed: %o', error); - + store.dispatch(requestActions.notify( { type : 'error', @@ -1533,7 +1536,7 @@ export default class RoomClient case 'enteredLobby': { store.dispatch(roomActions.setInLobby(true)); - + const { displayName } = store.getState().settings; const { picture } = store.getState().me; @@ -1702,7 +1705,7 @@ export default class RoomClient store.dispatch( roomActions.setJoinByAccessCode(joinByAccessCode)); - if (joinByAccessCode) + if (joinByAccessCode) { store.dispatch(requestActions.notify( { @@ -1742,10 +1745,10 @@ export default class RoomClient case 'changeDisplayName': { const { peerId, displayName, oldDisplayName } = notification.data; - + store.dispatch( peerActions.setPeerDisplayName(displayName, peerId)); - + store.dispatch(requestActions.notify( { text : intl.formatMessage({ @@ -1756,26 +1759,26 @@ export default class RoomClient displayName }) })); - + break; } - + case 'changePicture': { const { peerId, picture } = notification.data; - + store.dispatch(peerActions.setPeerPicture(peerId, picture)); - + break; } - + case 'chatMessage': { const { peerId, chatMessage } = notification.data; - + store.dispatch( chatActions.addResponseMessage({ ...chatMessage, peerId })); - + if ( !store.getState().toolarea.toolAreaOpen || (store.getState().toolarea.toolAreaOpen && @@ -1786,16 +1789,16 @@ export default class RoomClient roomActions.setToolbarsVisible(true)); this._soundNotification(); } - + break; } - + case 'sendFile': { const { peerId, magnetUri } = notification.data; - + store.dispatch(fileActions.addFile(peerId, magnetUri)); - + store.dispatch(requestActions.notify( { text : intl.formatMessage({ @@ -1803,7 +1806,7 @@ export default class RoomClient defaultMessage : 'New file available' }) })); - + if ( !store.getState().toolarea.toolAreaOpen || (store.getState().toolarea.toolAreaOpen && @@ -1814,27 +1817,27 @@ export default class RoomClient roomActions.setToolbarsVisible(true)); this._soundNotification(); } - + break; } - + case 'producerScore': { const { producerId, score } = notification.data; - + store.dispatch( producerActions.setProducerScore(producerId, score)); - + break; } - + case 'newPeer': { const { id, displayName, picture, device } = notification.data; - + store.dispatch( peerActions.addPeer({ id, displayName, picture, device, consumers: [] })); - + store.dispatch(requestActions.notify( { text : intl.formatMessage({ @@ -1844,20 +1847,20 @@ export default class RoomClient displayName }) })); - + break; } - + case 'peerClosed': { const { peerId } = notification.data; - + store.dispatch( peerActions.removePeer(peerId)); - + break; } - + case 'consumerClosed': { const { consumerId } = notification.data; @@ -1891,7 +1894,7 @@ export default class RoomClient store.dispatch( consumerActions.setConsumerPaused(consumerId, 'remote')); - + break; } diff --git a/app/src/index.js b/app/src/index.js index 4d0e604..441e842 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -100,7 +100,8 @@ function run() const produce = parameters.get('produce') !== 'false'; const useSimulcast = parameters.get('simulcast') === 'true'; const forceTcp = parameters.get('forceTcp') === 'true'; - + const displayName = parameters.get('displayName'); + // Get current device. const device = deviceInfo(); @@ -112,7 +113,7 @@ function run() ); roomClient = new RoomClient( - { peerId, accessCode, device, useSimulcast, produce, forceTcp }); + { peerId, accessCode, device, useSimulcast, produce, forceTcp, displayName }); global.CLIENT = roomClient; diff --git a/server/lib/Room.js b/server/lib/Room.js index c544768..ebb31c4 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -405,6 +405,27 @@ class Room extends EventEmitter case 'join': { + + try + { + if (peer.socket.handshake.session.passport.user.displayName) + { + this._notification( + peer.socket, + 'changeDisplayname', + { + peerId : peer.id, + displayName : peer.socket.handshake.session.passport.user.displayName, + oldDisplayName : '' + }, + true + ); + } + } + catch (error) + { + logger.error(error); + } // Ensure the Peer is not already joined. if (peer.joined) throw new Error('Peer already joined'); diff --git a/server/server.js b/server/server.js index ff073d6..723d7d2 100755 --- a/server/server.js +++ b/server/server.js @@ -390,9 +390,14 @@ async function runHttpsServer() { if (req.secure) { - if (req.isAuthenticated && req.user && req.user._lti) + const ltiURL = new URL(req.protocol + '://' + req.get('host') + req.originalUrl); + + if (req.isAuthenticated && req.user && req.user.displayName && !ltiURL.searchParams.get('displayName')) { - logger.error(req.user._lti); + + + ltiURL.searchParams.append('displayName', req.user.displayName); + res.redirect(ltiURL); } return next(); From c0b6ff6be46bac8f58c1c1339a8c5809d3a82ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Thu, 2 Jan 2020 09:50:44 +0100 Subject: [PATCH 3/5] Upgrade react-scripts --- app/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/package.json b/app/package.json index 445251d..a1e84e1 100644 --- a/app/package.json +++ b/app/package.json @@ -13,6 +13,7 @@ "bowser": "^2.7.0", "dompurify": "^2.0.7", "domready": "^1.0.8", + "end-of-stream": "1.4.0", "file-saver": "^2.0.2", "hark": "^1.2.3", "is-electron": "^2.2.0", @@ -27,7 +28,7 @@ "react-intl": "^3.4.0", "react-redux": "^7.1.1", "react-router-dom": "^5.1.2", - "react-scripts": "3.2.0", + "react-scripts": "^3.3.0", "redux": "^4.0.4", "redux-logger": "^3.0.6", "redux-persist": "^6.0.0", @@ -36,7 +37,6 @@ "riek": "^1.1.0", "socket.io-client": "^2.3.0", "source-map-explorer": "^2.1.0", - "end-of-stream": "1.4.0", "webtorrent": "^0.107.16" }, "scripts": { From 6e7f6b4a0d11dec307de619f680f0ff624c121e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Thu, 2 Jan 2020 15:49:32 +0100 Subject: [PATCH 4/5] Fix: warning * Fix: missing else, * move out to function already taken path comparison --- server/server.js | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/server/server.js b/server/server.js index 723d7d2..fa136de 100755 --- a/server/server.js +++ b/server/server.js @@ -386,24 +386,31 @@ async function runHttpsServer() app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge')); - app.all('*', (req, res, next) => + app.all('*', async (req, res, next) => { if (req.secure) { - const ltiURL = new URL(req.protocol + '://' + req.get('host') + req.originalUrl); + const ltiURL = new URL(`${req.protocol }://${ req.get('host') }${req.originalUrl}`); - if (req.isAuthenticated && req.user && req.user.displayName && !ltiURL.searchParams.get('displayName')) + if ( + req.isAuthenticated && + req.user && + req.user.displayName && + !ltiURL.searchParams.get('displayName') && + !is_path_already_taken(req.url) + ) { - ltiURL.searchParams.append('displayName', req.user.displayName); + res.redirect(ltiURL); } - - return next(); + else + return next(); } + else + res.redirect(`https://${req.hostname}${req.url}`); - res.redirect(`https://${req.hostname}${req.url}`); }); // Serve all files in the public folder as static files. @@ -420,6 +427,25 @@ async function runHttpsServer() httpServer.listen(config.listeningRedirectPort); } +function is_path_already_taken(url) { + const alreadyTakenPath = + [ + '/config/', + '/static/', + '/images/', + '/sounds/', + '/favicon.', + '/auth/' + ]; + + alreadyTakenPath.forEach((path) => { + if (url.toString().startsWith(path)) + return true; + }); + + return false; +} + /** * Create a WebSocketServer to allow WebSocket connections from browsers. */ From 4e0283981c9e72a5272e469d487db5980c560ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Thu, 2 Jan 2020 15:58:31 +0100 Subject: [PATCH 5/5] Tidy: change function name to camelCase --- server/server.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/server.js b/server/server.js index fa136de..cf486b6 100755 --- a/server/server.js +++ b/server/server.js @@ -117,7 +117,7 @@ let oidcStrategy; async function run() { - if ( typeof(config.auth) === 'undefined' ) + if (typeof(config.auth) === 'undefined') { logger.warn('Auth is not configured properly!'); } @@ -397,7 +397,7 @@ async function runHttpsServer() req.user && req.user.displayName && !ltiURL.searchParams.get('displayName') && - !is_path_already_taken(req.url) + !isPathAlreadyTaken(req.url) ) { @@ -427,7 +427,8 @@ async function runHttpsServer() httpServer.listen(config.listeningRedirectPort); } -function is_path_already_taken(url) { +function isPathAlreadyTaken(url) +{ const alreadyTakenPath = [ '/config/', @@ -438,8 +439,9 @@ function is_path_already_taken(url) { '/auth/' ]; - alreadyTakenPath.forEach((path) => { - if (url.toString().startsWith(path)) + alreadyTakenPath.forEach((path) => + { + if (url.toString.startsWith(path)) return true; });