Use passport authentication to standardize authentication in this application

This commit is contained in:
Evert Prants 2019-10-25 15:23:11 +03:00
parent 2e5e6412b9
commit c2cbd60f96
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
5 changed files with 115 additions and 130 deletions

192
app.js
View File

@ -1,19 +1,20 @@
const connectSession = require('connect-redis') const connectSession = require('connect-redis')
const session = require('express-session') const session = require('express-session')
const bodyParser = require('body-parser') const bodyParser = require('body-parser')
const nunjucks = require('nunjucks')
const passport = require('passport')
const express = require('express') const express = require('express')
const request = require('request') const request = require('request')
const nunjucks = require('nunjucks')
const sqlite = require('sqlite') const sqlite = require('sqlite')
const xml2js = require('xml2js') const xml2js = require('xml2js')
const path = require('path')
const toml = require('toml')
const http = require('http')
const fs = require('fs')
const WebSocket = require('ws') const WebSocket = require('ws')
const uuid = require('uuid/v4') const uuid = require('uuid/v4')
const redis = require('redis') const redis = require('redis')
const path = require('path')
const toml = require('toml')
const http = require('http')
const URL = require('url') const URL = require('url')
const fs = require('fs')
require('express-async-errors') require('express-async-errors')
@ -40,32 +41,26 @@ try {
config = Object.assign({ config = Object.assign({
'Streaming': { 'Streaming': {
'Port': '9322', 'port': '9322',
'Database': 'streaming.db', 'database': 'streaming.db',
'StreamServer': 'https://tv.icynet.eu/live/', 'streamServer': 'https://tv.icynet.eu/live/',
'ServerHost': 'icynet.eu', 'serverHost': 'icynet.eu',
'PublishAddress': 'rtmp://{host}:1935/hls-live/{streamer}', 'publishAddress': 'rtmp://{host}:1935/hls-live/{streamer}',
'Secret': 'changeme' 'secret': 'changeme'
}, },
'Auth': { 'Auth': {
'Server': 'http://localhost:8282', 'strategy': 'passport-oauth2',
'Redirect': 'http://localhost:5000/auth/_callback/' 'callbackURL': 'http://localhost:5000/auth/_callback/',
}, 'clientID': '1',
'OAuth2': { 'clientSecret': 'changeme'
'ClientID': '1',
'ClientSecret': 'changeme'
} }
}, config) }, config)
// Constants // Constants
const oauthAuth = '{server}/oauth2/authorize?response_type=code&state={state}&redirect_uri={redirect}&client_id={client}&scope=image' const oauthAuth = '{server}/oauth2/authorize?response_type=code&state={state}&redirect_uri={redirect}&client_id={client}&scope=image'
const port = parseInt(config['Streaming']['Port']) const port = parseInt(config['Streaming']['port'])
const streamServer = config['Streaming']['StreamServer'] const streamServer = config['Streaming']['streamServer']
const streamServerHost = config['Streaming']['ServerHost'] const streamServerHost = config['Streaming']['serverHost']
const authServer = config['Auth']['Server']
const oauthRedirect = config['Auth']['Redirect']
const oauthId = config['OAuth2']['ClientID'].toString()
const oauthSecret = config['OAuth2']['ClientSecret']
const streamAppName = streamServer.match(/\/([\w-_]+)\/$/)[1] const streamAppName = streamServer.match(/\/([\w-_]+)\/$/)[1]
function teval (str, obj) { function teval (str, obj) {
@ -79,7 +74,7 @@ function teval (str, obj) {
// Database // Database
const dbPromise = Promise.resolve() const dbPromise = Promise.resolve()
.then(() => sqlite.open(path.join(process.cwd(), config['Streaming']['Database']), { Promise, cache: true })) .then(() => sqlite.open(path.join(process.cwd(), config['Streaming']['database']), { Promise, cache: true }))
.then(db => db.migrate()) .then(db => db.migrate())
// Setup server // Setup server
@ -87,6 +82,24 @@ const app = express()
const server = http.createServer(app) const server = http.createServer(app)
const wss = new WebSocket.Server({ clientTracking: false, noServer: true }) const wss = new WebSocket.Server({ clientTracking: false, noServer: true })
// Authentication
const Strategy = require(config['Auth']['strategy'])
const strategyConfig = Object.assign({}, config['Auth'])
if (!strategyConfig.provider) strategyConfig.provider = strategyConfig.strategy.replace('passport-', '')
passport.use(new Strategy(strategyConfig, function (accessToken, refreshToken, profile, done) {
process.nextTick(function() {
return done(null, profile)
})
}))
passport.serializeUser(function (user, done) {
done(null, user)
})
passport.deserializeUser(function (user, done) {
done(null, user)
})
app.enable('trust proxy', 1) app.enable('trust proxy', 1)
app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.urlencoded({ extended: false }))
@ -101,7 +114,7 @@ nunjucks.configure('templates', {
const sessionParser = session({ const sessionParser = session({
key: 'Streamserver Session', key: 'Streamserver Session',
secret: config['Streaming']['Secret'], secret: config['Streaming']['secret'],
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
store: new SessionStore({ client: redis.createClient() }), store: new SessionStore({ client: redis.createClient() }),
@ -113,6 +126,9 @@ const sessionParser = session({
app.use(sessionParser) app.use(sessionParser)
app.use(passport.initialize())
app.use(passport.session())
// Parse stream metrics from the stat.xml file // Parse stream metrics from the stat.xml file
async function pullMetrics (uuid) { async function pullMetrics (uuid) {
let statPath = streamServer + 'stat' let statPath = streamServer + 'stat'
@ -188,7 +204,7 @@ app.post('/publish', async (req, res) => {
console.log('=> Streamer %s has started streaming!', streamer.name) console.log('=> Streamer %s has started streaming!', streamer.name)
// Generate real publish address for the server // Generate real publish address for the server
let publishAddress = config['Streaming']['PublishAddress'] let publishAddress = config['Streaming']['publishAddress']
.replace('{streamer}', streamer.name) .replace('{streamer}', streamer.name)
.replace('{host}', '127.0.0.1') .replace('{host}', '127.0.0.1')
@ -219,97 +235,50 @@ app.post('/publish_done', async (req, res) => {
// Front-end server // Front-end server
// OAuth2 authenticator // OAuth2 authenticator
app.get('/login', async (req, res) => { app.get('/login', passport.authenticate(strategyConfig.provider, Object.assign({}, strategyConfig.authOptions || {})))
if (req.session.user) return res.redirect('/')
req.session.state = uuid()
res.redirect(teval(oauthAuth, { state: req.session.state, redirect: oauthRedirect, client: oauthId, server: authServer }))
})
app.get('/auth/_callback', async (req, res) => {
let state = req.session.state
if (!state) throw new Error('Something went wrong!')
let code = req.query.code
let provState = req.query.state
if (!code || state !== provState) throw new Error('Something went wrong!')
delete req.session.state
// Aquire token
let { body } = await post(authServer + '/oauth2/token', {
form: {
grant_type: 'authorization_code',
code: code,
redirect_uri: oauthRedirect,
client_id: oauthId,
client_secret: oauthSecret
},
auth: {
user: oauthId,
pass: oauthSecret
}
})
if (!body) throw new Error('Could not obtain access token!')
try {
body = JSON.parse(body)
} catch (e) {
console.error(e, body)
throw new Error('Authorization server gave us an invalid response!')
}
if (body['error']) {
throw new Error(body['error'] + ': ' + body['error_description'])
}
let token = body.access_token
// Get user information
let { body: bodyNew } = await get(authServer + '/oauth2/user', { auth: { bearer: token } })
try {
bodyNew = JSON.parse(bodyNew)
} catch (e) {
console.error(e, bodyNew)
throw new Error('Authorization server gave us an invalid response for user!')
}
app.get('/auth/_callback', passport.authenticate(strategyConfig.provider, { failureRedirect: '/' }), async (req, res) => {
dev && console.log(req.user.username, 'logged in')
// Get user from database // Get user from database
let db = await dbPromise let db = await dbPromise
let user = await db.get('SELECT * FROM signed_users WHERE uuid=?', bodyNew.uuid) let user = await db.get('SELECT * FROM signed_users WHERE uuid=?', req.user.uuid)
if (!user) { if (!user) {
await db.run('INSERT INTO signed_users (uuid,name) VALUES (?,?)', bodyNew.uuid, bodyNew.username) await db.run('INSERT INTO signed_users (uuid,name) VALUES (?,?)', req.user.uuid, req.user.username)
} }
req.session.login = bodyNew.uuid
req.session.username = bodyNew.username
// Lets see if this user is a streamer // Lets see if this user is a streamer
let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', bodyNew.uuid) let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.user.uuid)
if (streamer) cache.streamers[bodyNew.uuid] = streamer if (streamer) cache.streamers[req.user.uuid] = streamer
res.redirect('/') res.redirect('/')
}) })
app.get('/logout', (req, res) => { app.get('/logout', (req, res) => {
req.session.destroy() req.logout()
res.redirect('/') res.redirect('/')
}) })
function authed (req, res, next) {
if (req.isAuthenticated() && req.isStreamer) return next()
res.jsonp({ error: 'Unauthorized' })
}
// Views // Views
app.use('/dist', express.static(path.join(__dirname, 'dist'), { maxAge: dev ? 0 : 2678400000 })) app.use('/dist', express.static(path.join(__dirname, 'dist'), { maxAge: dev ? 0 : 2678400000 }))
app.use(async function (req, res, next) { app.use(async function (req, res, next) {
req.isStreamer = false req.isStreamer = false
if (!req.session.login) return next() if (!req.isAuthenticated()) return next()
res.locals.session = { uuid: req.session.login, username: req.session.username } res.locals.user = req.user
if (!cache.streamers[req.session.login]) { if (!cache.streamers[req.user.uuid]) {
let db = await dbPromise let db = await dbPromise
let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.session.login) let streamer = await db.get('SELECT * FROM channels WHERE user_uuid = ?', req.user.uuid)
if (streamer) cache.streamers[req.session.login] = streamer if (streamer) cache.streamers[req.user.uuid] = streamer
} }
if (cache.streamers[req.session.login]) { if (cache.streamers[req.user.uuid]) {
req.isStreamer = true req.isStreamer = true
return next() return next()
} }
@ -323,16 +292,14 @@ app.get('/', (req, res) => {
}) })
// Dashboard // Dashboard
app.get('/dashboard', (req, res, next) => { app.get('/dashboard', authed, (req, res) => {
if (!req.isStreamer) return next(new Error('Unauthorized')) let stream = cache.streamers[req.user.uuid]
let stream = cache.streamers[req.session.login]
res.render('dashboard.html', { stream: stream.key, server: 'rtmp://' + streamServerHost + '/live/' }) res.render('dashboard.html', { stream: stream.key, server: 'rtmp://' + streamServerHost + '/live/' })
}) })
// Stats // Stats
app.get('/dashboard/stats', async (req, res) => { app.get('/dashboard/stats', authed, async (req, res) => {
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) let stream = cache.streamers[req.user.uuid]
let stream = cache.streamers[req.session.login]
let data let data
try { try {
@ -346,9 +313,8 @@ app.get('/dashboard/stats', async (req, res) => {
}) })
// Data // Data
app.get('/dashboard/data', async (req, res) => { app.get('/dashboard/data', authed, async (req, res) => {
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) let stream = cache.streamers[req.user.uuid]
let stream = cache.streamers[req.session.login]
let data let data
let db = await dbPromise let db = await dbPromise
@ -364,7 +330,7 @@ app.get('/dashboard/data', async (req, res) => {
res.jsonp({ res.jsonp({
'name': data.name, 'name': data.name,
'key': stream.key, 'key': stream.key,
'uuid': req.session.login, 'uuid': req.user.uuid,
'live': data.live_at != null, 'live': data.live_at != null,
'live_at': new Date(parseInt(data.live_at)), 'live_at': new Date(parseInt(data.live_at)),
'last_stream': new Date(parseInt(data.last_stream)) 'last_stream': new Date(parseInt(data.last_stream))
@ -372,9 +338,8 @@ app.get('/dashboard/data', async (req, res) => {
}) })
// Get links // Get links
app.get('/dashboard/link', async (req, res) => { app.get('/dashboard/link', authed, async (req, res) => {
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) let user = req.user.uuid
let user = req.session.login
let db = await dbPromise let db = await dbPromise
let links = await db.all('SELECT * FROM link WHERE uuid = ?', user) let links = await db.all('SELECT * FROM link WHERE uuid = ?', user)
@ -383,9 +348,8 @@ app.get('/dashboard/link', async (req, res) => {
}) })
// Add link URL // Add link URL
app.post('/dashboard/link', async (req, res) => { app.post('/dashboard/link', authed, async (req, res) => {
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) let user = req.user.uuid
let user = req.session.login
let name = req.body.name let name = req.body.name
let url = req.body.url let url = req.body.url
@ -413,9 +377,8 @@ app.post('/dashboard/link', async (req, res) => {
}) })
// Remove link URL // Remove link URL
app.post('/dashboard/link/delete', async (req, res) => { app.post('/dashboard/link/delete', authed, async (req, res) => {
if (!req.isStreamer) return res.jsonp({ error: 'Unauthorized' }) let user = req.user.uuid
let user = req.session.login
if (req.body.name == null && req.body.url == null) return res.jsonp({ error: 'Missing parameters!' }) if (req.body.name == null && req.body.url == null) return res.jsonp({ error: 'Missing parameters!' })
@ -469,8 +432,8 @@ app.use((error, req, res, next) => {
// Socket Server // Socket Server
wss.on('connection', (ws, request, client) => { wss.on('connection', (ws, request, client) => {
const userId = request.session.login || request.session.id const userId = request.user.uuid || request.session.id
const username = request.session.username const username = request.user.username
let myChannels = [] let myChannels = []
dev && console.log(userId, 'connected') dev && console.log(userId, 'connected')
@ -538,4 +501,5 @@ server.listen(port, host, () => {
})().catch(e => console.error(e.stack)) })().catch(e => console.error(e.stack))
console.log('Listening on %s:%d', host, port) console.log('Listening on %s:%d', host, port)
console.log('Authentication module: %s (%s)', strategyConfig.strategy, strategyConfig.provider)
}) })

View File

@ -1,15 +1,16 @@
[Streaming] [Streaming]
Port = 5000 port = 5000
Database = "streaming.db" database = "streaming.db"
StreamServer = "http://localhost:5000/live/" streamServer = "http://localhost:5000/live/"
ServerHost = "localhost:1935" serverHost = "localhost:1935"
PublishAddress = "rtmp://{host}:1935/hls-live/{streamer}" publishAddress = "rtmp://{host}:1935/hls-live/{streamer}"
Secret = "changeme" secret = "changeme"
[Auth] [Auth]
Server = "http://localhost:8282" provider = "oauth2"
Redirect = "http://localhost:5000/auth/_callback/" strategy = "passport-oauth2"
callbackURL = "http://localhost:5000/auth/_callback/"
[OAuth2] authorizationURL = "http://localhost/oauth2/authorize"
ClientID = 1 tokenURL = "http://localhost/oauth2/token"
ClientSecret = "hackme" clientID = 1
clientSecret = "hackme"

21
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "icytv", "name": "icytv",
"version": "2.0.0", "version": "2.1.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -4570,6 +4570,20 @@
"resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz",
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ="
}, },
"passport": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz",
"integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
},
"path-browserify": { "path-browserify": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz",
@ -4624,6 +4638,11 @@
"pify": "^3.0.0" "pify": "^3.0.0"
} }
}, },
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
},
"pbkdf2": { "pbkdf2": {
"version": "3.0.16", "version": "3.0.16",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.16.tgz",

View File

@ -29,6 +29,7 @@
"express-async-errors": "^3.1.1", "express-async-errors": "^3.1.1",
"express-session": "^1.16.1", "express-session": "^1.16.1",
"nunjucks": "^3.2.0", "nunjucks": "^3.2.0",
"passport": "^0.4.0",
"redis": "^2.8.0", "redis": "^2.8.0",
"request": "^2.88.0", "request": "^2.88.0",
"sqlite": "^3.0.3", "sqlite": "^3.0.3",

View File

@ -13,8 +13,8 @@
<h3 class="masthead-brand">IcyTV</h3> <h3 class="masthead-brand">IcyTV</h3>
<nav class="nav nav-masthead justify-content-center"> <nav class="nav nav-masthead justify-content-center">
<a class="nav-link active" href="/">Home</a> <a class="nav-link active" href="/">Home</a>
{% if session.uuid %} {% if user.uuid %}
<a class="nav-link" href="/dashboard">{{ session.username }}</a> <a class="nav-link" href="/dashboard">{{ user.username }}</a>
{% else %} {% else %}
<a class="nav-link" href="/login">Login</a> <a class="nav-link" href="/login">Login</a>
{% endif %} {% endif %}
@ -26,7 +26,7 @@
<h1 class="cover-heading">IcyTV</h1> <h1 class="cover-heading">IcyTV</h1>
<p class="lead">This is a private livestreaming server for Icy Network members.</p> <p class="lead">This is a private livestreaming server for Icy Network members.</p>
{% if streamer %} {% if streamer %}
<p class="lead">Welcome back, {{ session.username }}!</p> <p class="lead">Welcome back, {{ user.username }}!</p>
<p class="lead"> <p class="lead">
<a href="/dashboard" class="btn btn-lg btn-secondary">My Dashboard</a> <a href="/dashboard" class="btn btn-lg btn-secondary">My Dashboard</a>
</p> </p>