Merge pull request #118 from havfo/lti1

LTI 1.1 integration
master
Mészáros Mihály 2020-02-10 09:04:07 +01:00 committed by GitHub
commit 209653dcc0
7 changed files with 238 additions and 96 deletions

View File

@ -13,6 +13,7 @@
"bowser": "^2.7.0", "bowser": "^2.7.0",
"dompurify": "^2.0.7", "dompurify": "^2.0.7",
"domready": "^1.0.8", "domready": "^1.0.8",
"end-of-stream": "1.4.0",
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"hark": "^1.2.3", "hark": "^1.2.3",
"is-electron": "^2.2.0", "is-electron": "^2.2.0",
@ -27,7 +28,7 @@
"react-intl": "^3.4.0", "react-intl": "^3.4.0",
"react-redux": "^7.1.1", "react-redux": "^7.1.1",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-scripts": "3.2.0", "react-scripts": "^3.3.0",
"redux": "^4.0.4", "redux": "^4.0.4",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
@ -36,7 +37,6 @@
"riek": "^1.1.0", "riek": "^1.1.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",
"source-map-explorer": "^2.1.0", "source-map-explorer": "^2.1.0",
"end-of-stream": "1.4.0",
"webtorrent": "^0.107.16" "webtorrent": "^0.107.16"
}, },
"scripts": { "scripts": {

View File

@ -106,7 +106,7 @@ export default class RoomClient
} }
constructor( constructor(
{ peerId, accessCode, device, useSimulcast, produce, forceTcp } = {}) { peerId, accessCode, device, useSimulcast, produce, forceTcp, displayName } = {})
{ {
if (!peerId) if (!peerId)
throw new Error('Missing peerId'); throw new Error('Missing peerId');
@ -114,8 +114,8 @@ export default class RoomClient
throw new Error('Missing device'); throw new Error('Missing device');
logger.debug( logger.debug(
'constructor() [peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", forceTcp: "%s"]', 'constructor() [peerId: "%s", device: "%s", useSimulcast: "%s", produce: "%s", forceTcp: "%s", displayName ""]',
peerId, device.flag, useSimulcast, produce, forceTcp); peerId, device.flag, useSimulcast, produce, forceTcp, displayName);
this._signalingUrl = null; this._signalingUrl = null;
@ -128,6 +128,9 @@ export default class RoomClient
// Wheter we force TCP // Wheter we force TCP
this._forceTcp = forceTcp; this._forceTcp = forceTcp;
// Use displayName
store.dispatch(settingsActions.setDisplayName(displayName));
// Torrent support // Torrent support
this._torrentSupport = null; this._torrentSupport = null;

View File

@ -102,6 +102,7 @@ function run()
const produce = parameters.get('produce') !== 'false'; const produce = parameters.get('produce') !== 'false';
const useSimulcast = parameters.get('simulcast') === 'true'; const useSimulcast = parameters.get('simulcast') === 'true';
const forceTcp = parameters.get('forceTcp') === 'true'; const forceTcp = parameters.get('forceTcp') === 'true';
const displayName = parameters.get('displayName');
// Get current device. // Get current device.
const device = deviceInfo(); const device = deviceInfo();
@ -114,7 +115,7 @@ function run()
); );
roomClient = new RoomClient( roomClient = new RoomClient(
{ peerId, accessCode, device, useSimulcast, produce, forceTcp }); { peerId, accessCode, device, useSimulcast, produce, forceTcp, displayName });
global.CLIENT = roomClient; global.CLIENT = roomClient;

View File

@ -2,25 +2,37 @@ const os = require('os');
module.exports = module.exports =
{ {
// oAuth2 conf
/* auth :
{
// Auth conf
/*
auth :
{
lti :
{
consumerKey : 'key',
consumerSecret : 'secret'
},
oidc:
{
// The issuer URL for OpenID Connect discovery // The issuer URL for OpenID Connect discovery
// The OpenID Provider Configuration Document // The OpenID Provider Configuration Document
// could be discovered on: // could be discovered on:
// issuerURL + '/.well-known/openid-configuration' // issuerURL + '/.well-known/openid-configuration'
// issuerURL : 'https://example.com', issuerURL : 'https://example.com',
// clientOptions : clientOptions :
// { {
client_id : '', client_id : '',
client_secret : '', client_secret : '',
scope : 'openid email profile', 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' redirect_uri : 'https://client.example.com/auth/callback'
} }
},*/
}
},
*/
redisOptions: {} redisOptions: {}
// session cookie secret // session cookie secret
cookieSecret : 'T0P-S3cR3t_cook!e', cookieSecret : 'T0P-S3cR3t_cook!e',

View File

@ -405,6 +405,27 @@ class Room extends EventEmitter
case 'join': 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. // Ensure the Peer is not already joined.
if (peer.joined) if (peer.joined)
throw new Error('Peer already joined'); throw new Error('Peer already joined');

View File

@ -19,9 +19,11 @@
"express-session": "^1.17.0", "express-session": "^1.17.0",
"express-socket.io-session": "^1.3.5", "express-socket.io-session": "^1.3.5",
"helmet": "^3.21.2", "helmet": "^3.21.2",
"ims-lti": "^3.0.2",
"mediasoup": "^3.0.12", "mediasoup": "^3.0.12",
"openid-client": "^3.7.3", "openid-client": "^3.7.3",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-lti": "0.0.7",
"redis": "^2.8.0", "redis": "^2.8.0",
"socket.io": "^2.3.0", "socket.io": "^2.3.0",
"spdy": "^4.0.1" "spdy": "^4.0.1"

View File

@ -17,14 +17,17 @@ const Room = require('./lib/Room');
const Peer = require('./lib/Peer'); const Peer = require('./lib/Peer');
const base64 = require('base-64'); const base64 = require('base-64');
const helmet = require('helmet'); const helmet = require('helmet');
const { const {
loginHelper, loginHelper,
logoutHelper logoutHelper
} = require('./httpHelper'); } = require('./httpHelper');
// auth // auth
const passport = require('passport'); const passport = require('passport');
const LTIStrategy = require('passport-lti');
const imsLti = require('ims-lti');
const redis = require('redis'); const redis = require('redis');
const client = redis.createClient(config.redisOptions); const redisClient = redis.createClient(config.redisOptions);
const { Issuer, Strategy } = require('openid-client'); const { Issuer, Strategy } = require('openid-client');
const expressSession = require('express-session'); const expressSession = require('express-session');
const RedisStore = require('connect-redis')(expressSession); const RedisStore = require('connect-redis')(expressSession);
@ -87,7 +90,7 @@ const session = expressSession({
name : config.cookieName, name : config.cookieName,
resave : true, resave : true,
saveUninitialized : true, saveUninitialized : true,
store : new RedisStore({ client }), store : new RedisStore({ client: redisClient }),
cookie : { cookie : {
secure : true, secure : true,
httpOnly : true, httpOnly : true,
@ -112,49 +115,26 @@ let io;
let oidcClient; let oidcClient;
let oidcStrategy; let oidcStrategy;
const auth = config.auth;
async function run() async function run()
{ {
if ( if (typeof(config.auth) === 'undefined')
typeof(auth) !== 'undefined' &&
typeof(auth.issuerURL) !== 'undefined' &&
typeof(auth.clientOptions) !== 'undefined'
)
{ {
Issuer.discover(auth.issuerURL).then(async (oidcIssuer) => logger.warn('Auth is not configured properly!');
{
// 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);
});
} }
else else
{ {
logger.error('Auth is not configure properly!'); await setupAuth();
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
} }
// Run a mediasoup Worker.
await runMediasoupWorkers();
// Run HTTPS server.
await runHttpsServer();
// Run WebSocketServer.
await runWebSocketServer();
// Log rooms status every 30 seconds. // Log rooms status every 30 seconds.
setInterval(() => setInterval(() =>
{ {
@ -174,16 +154,67 @@ async function run()
}, 10000); }, 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 // ... any authorization request parameters go here
// client_id defaults to client.client_id // client_id defaults to client.client_id
// redirect_uri defaults to client.redirect_uris[0] // redirect_uri defaults to client.redirect_uris[0]
// response type defaults to client.response_types[0], then 'code' // response type defaults to client.response_types[0], then 'code'
// scope defaults to 'openid' // 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 // optional, defaults to false, when true req is passed as a first
// argument to verify fn // argument to verify fn
@ -257,6 +288,31 @@ async function setupAuth(oidcIssuer)
passport.use('oidc', oidcStrategy); 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.initialize());
app.use(passport.session()); app.use(passport.session());
@ -270,6 +326,15 @@ async function setupAuth(oidcIssuer)
})(req, res, next); })(req, res, next);
}); });
// lti launch
app.post('/auth/lti',
passport.authenticate('lti', { failureRedirect: '/' }),
function(req, res)
{
res.redirect(`/${req.user.room}`);
}
);
// logout // logout
app.get('/auth/logout', (req, res) => app.get('/auth/logout', (req, res) =>
{ {
@ -321,14 +386,31 @@ async function runHttpsServer()
app.use('/.well-known/acme-challenge', express.static('public/.well-known/acme-challenge')); 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) if (req.secure)
{ {
return next(); const ltiURL = new URL(`${req.protocol }://${ req.get('host') }${req.originalUrl}`);
}
if (
req.isAuthenticated &&
req.user &&
req.user.displayName &&
!ltiURL.searchParams.get('displayName') &&
!isPathAlreadyTaken(req.url)
)
{
ltiURL.searchParams.append('displayName', req.user.displayName);
res.redirect(ltiURL);
}
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. // Serve all files in the public folder as static files.
@ -345,6 +427,27 @@ async function runHttpsServer()
httpServer.listen(config.listeningRedirectPort); httpServer.listen(config.listeningRedirectPort);
} }
function isPathAlreadyTaken(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. * Create a WebSocketServer to allow WebSocket connections from browsers.
*/ */