diff --git a/.gitignore b/.gitignore index 935896d..1ba6f18 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,4 @@ backend/appdata/archives/blacklist_audio.txt backend/appdata/archives/blacklist_video.txt backend/appdata/logs/combined.log backend/appdata/logs/error.log +backend/appdata/users.json diff --git a/backend/app.js b/backend/app.js index a8a7c82..aa057be 100644 --- a/backend/app.js +++ b/backend/app.js @@ -1,6 +1,7 @@ var async = require('async'); const { uuid } = require('uuidv4'); var fs = require('fs-extra'); +var auth = require('./authentication/auth'); var winston = require('winston'); var path = require('path'); var youtubedl = require('youtube-dl'); @@ -146,6 +147,9 @@ var descriptors = {}; app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); +// use passport +app.use(auth.passport.initialize()); + // objects function File(id, title, thumbnailURL, isAudio, duration, url, uploader, size, path, upload_date) { @@ -1253,6 +1257,7 @@ app.use(function(req, res, next) { } else if (req.path.includes('/api/video/') || req.path.includes('/api/audio/')) { next(); } else { + logger.verbose(`Rejecting request - invalid API use for endpoint: ${req.path}. API key received: ${req.query.apiKey}`); req.socket.end(); } }); @@ -2303,6 +2308,17 @@ app.get('/api/audio/:id', function(req , res){ }) }); +// user authentication + +app.post('/api/auth/register' + , auth.registerUser); +app.post('/api/auth/login' +// , auth.passport.authenticate('basic',{session:false}) // causes challenge pop-up on 401 + , auth.authenticateViaPassport + , auth.generateJWT + , auth.returnAuthResponse +); + app.use(function(req, res, next) { //if the request is not html then move along var accept = req.accepts('html', 'json', 'xml'); diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js new file mode 100644 index 0000000..7d62196 --- /dev/null +++ b/backend/authentication/auth.js @@ -0,0 +1,172 @@ +const low = require('lowdb') +const FileSync = require('lowdb/adapters/FileSync'); +const adapter = new FileSync('./appdata/users.json'); +const db = low(adapter); +db.defaults( + { + users: [] + } +).write(); + +/************************* + * Authentication module + ************************/ +var bcrypt = require('bcrypt'); +const saltRounds = 10; + +var jwt = require('jsonwebtoken'); +const JWT_EXPIRATION = (60 * 60); // one hour + +const { uuid } = require('uuidv4'); +const SERVER_SECRET = uuid(); + +exports.passport = require('passport'); +var BasicStrategy = require('passport-http').BasicStrategy; + +/*************************************** + * Register user with hashed password + **************************************/ +exports.registerUser = function(req, res) { + console.log('got here'); + var userid = req.body.userid; + var username = req.body.username; + var plaintextPassword = req.body.password; + + bcrypt.hash(plaintextPassword, saltRounds) + .then(function(hash) { + // check if user exists + if (db.get('users').find({uid: userid}).value()) { + // user id is taken! + res.status(409).send('UID is already taken!'); + } else if (db.get('users').find({name: username}).value()) { + // user name is taken! + res.status(409).send('User name is already taken!'); + } else { + // add to db + db.get('users').push({ + name: username, + uid: userid, + passhash: hash + }).write(); + } + }) + .then(function(result) { + res.send('registered'); + }) + .catch(function(err) { + if( err.code == 'ER_DUP_ENTRY' ) { + res.status(409).send('UserId already taken'); + } else { + console.log('failed TO register User'); + + // res.writeHead(500, {'Content-Type':'text/plain'}); + res.end(err); + } + }); +} + +/*************************************** + * Login methods + **************************************/ + +/************************************************* + * This gets called when passport.authenticate() + * gets called. + * + * This checks that the credentials are valid. + * If so, passes the user info to the next middleware. + ************************************************/ +exports.passport.use(new BasicStrategy( + function(userid, plainTextPassword, done) { +// console.log('BasicStrategy: verifying credentials'); + const user = db.get('users').find({uid: userid}).value(); + if (user) { + var hashedPwd = user.passhash; + return bcrypt.compare(plainTextPassword, hashedPwd); + } else { + return false; + } + } +)); + +/************************************************************* + * This is a wrapper for auth.passport.authenticate(). + * We use this to change WWW-Authenticate header so + * the browser doesn't pop-up challenge dialog box by default. + * Browser's will pop-up up dialog when status is 401 and + * "WWW-Authenticate:Basic..." + *************************************************************/ +exports.authenticateViaPassport = function(req, res, next) { + exports.passport.authenticate('basic',{session:false}, + function(err, user, info) { + if(!user){ + res.set('WWW-Authenticate', 'x'+info); // change to xBasic + res.status(401).send('Invalid Authentication'); + } else { + req.user = user; + next(); + } + } + )(req, res, next); +}; + +/********************************** + * Generating/Signing a JWT token + * And attaches the user info into + * the payload to be sent on every + * request. + *********************************/ +exports.generateJWT = function(req, res, next) { + var payload = { + exp: Math.floor(Date.now() / 1000) + JWT_EXPIRATION + , user: req.user, +// , role: role + }; + req.token = jwt.sign(payload, SERVER_SECRET); + next(); +} + +exports.returnAuthResponse = function(req, res) { + res.status(200).json({ + user: req.user, + token: req.token + }); +} + +/*************************************** + * Authorization: middleware that checks the + * JWT token for validity before allowing + * the user to access anything. + * + * It also passes the user object to the next + * middleware through res.locals + **************************************/ +exports.ensureAuthenticatedElseError = function(req, res, next) { + var token = getToken(req.query); + if( token ) { + try { + var payload = jwt.verify(token, SERVER_SECRET); + // console.log('payload: ' + JSON.stringify(payload)); + // check if user still exists in database if you'd like + res.locals.user = payload.user; + next(); + } catch(err) { + res.status(401).send('Invalid Authentication'); + } + } else { + res.status(401).send('Missing Authorization header'); + } +} + +function getToken(queryParams) { + if (queryParams && queryParams.jwt) { + var parted = queryParams.jwt.split(' '); + if (parted.length === 2) { + return parted[1]; + } else { + return null; + } + } else { + return null; + } +}; \ No newline at end of file