UNPKG

pusudb-use-auth-jwt

Version:

A authentiation middleware for the pusudb-framework.

630 lines (561 loc) 22.1 kB
const path = require('path') const Cookies = require('cookies') const jwt = require('jsonwebtoken') const crypto = require('crypto'); if(!crypto) throw new Error('Crypto not avaiable.') /** * autor: yannick grund, 2018 * This class implements the authentication in the pusudb. It uses the jsonwebtoken-standard. The token is asigned to the cookie of the client. * It's possible to set routes which should be authenticatied * * Defined options: * - path to login, logout, signin pages * - redirect path when successful login or logout, this can be activated * - cookie name * - database name * - the body keys for the name and the password * - a secret to create the token * - a expire time formate by zeit/ms documentation https://github.com/zeit/ms * ms('2 days') // 172800000 ms('1d') // 86400000 ms('10h') // 36000000 ms('2.5 hrs') // 9000000 ms('2h') // 7200000 ms('1m') // 60000 ms('5s') // 5000 ms('1y') // 31557600000 ms('100') // 100 ms('-3 days') // -259200000 ms('-1h') // -3600000 ms('-200') // -200 */ class AuthHandler { /** * Default options { * login: '/login', * logout: '/logout', * signin: '/signin', * signinActive: true, * redirect: '/index', * redirectActive: false, * cookie : 'bearer', * db: 'users', * form: { password : 'password', name : 'email' }, * secret : 'my-super-secret', * expire: '1y', * assignUser: false * } * * @param {object} opt */ constructor(opt){ this.options = typeof opt === 'object' ? opt : {} this.loginPath = this.options.login ? this.options.login : '/login' this.logoutPath = this.options.logout ? this.options.logout : '/logout' this.signinPath = this.options.signin ? this.options.signin : '/signin' this.signinActive = this.options.signinActive ? this.options.signinActive : true this.redirectPath = this.options.redirect ? this.options.redirect : '/index' this.redirectActive = this.options.redirectActive ? this.options.redirectActive : false this.cookieName = this.options.cookie ? this.options.cookie : 'bearer' this.db = this.options.db ? this.options.db : './users' this.form = this.options.form ? this.options.form : { password : 'password', name : 'email'} this.secret = this.options.secret || crypto.randomBytes(32).toString('hex'); this.expire = this.options.expire ? this.options.expire : '1y' this.assignUser = this.options.assignUser ? this.options.assignUser : false this.routes = [] this.routesWs = [] this.cookies this.websocketToken = {} this.serve = this.serve.bind(this) this.serveWebSocket = this.serveWebSocket.bind(this) this.cookieOptions = opt.cookieOptions || { maxAge : 365 * 86400 } } /** * Set the auth-routes. These url's needs to be authenticated * Each argument is a route like '/admin' or '/secretepage' */ setRoutes(arr){ for(let i in arguments){ if(Array.isArray(arguments[i])) this.routes = this.routes.concat(arguments[i]) else this.routes.push(arguments[i]) } } /** * Set the auth-routes. These url's needs to be authenticated * Each argument is a route like '/admin' or '/secretepage' */ setRoutesWebSocket(arr){ for(let i in arguments){ if(Array.isArray(arguments[i])) this.routesWs = this.routesWs.concat(arguments[i]) else this.routesWs.push(arguments[i]) } } /** * Main-method. Serve the middleware * @param {object} req * @param {object} res * @param {function} next */ serve(req, res, next){ let self = this // Parse cookie this.cookies = new Cookies(req, res) // Get cookie req.token = '' req.token = this.cookies.get(this.cookieName) // Handle the request by url try{ switch(req.url){ case this.loginPath: this.login(req, res, function(code){ next(code) }) break case this.logoutPath: this.logout(req, res, function(code){ next(code) }) break case this.signinPath: if(this.signinActive){ this.signin(req, res, function(code){ next(code) }) } else{ next(401) } break default: // Check if the path needs to be authenticated if(this.pathNeedsAuthentication(this.routes, req.url)){ this.verify(req, res, function(code){ next(code) }) } // Whooohooooo, nothing to dooooo, only parse token if exist to assign the user to req.user else{ if(req.token && req.token !== '-'){ this.verify(req, res, function(code){ next() }) } else{ next() } } break } } catch(e){ next(e) } } /** * Handle the metas by websocket data. Login, logout or signin. Or parse the jsonwebtoken. If normal data, verify the route * @param {object} req * @param {object*} socket * @param {function} next */ serveWebSocket(req, socket, next){ let self = this // Get cookie req.token = '' req.token = req.headers['sec-websocket-key'] try{ if(Object.keys(req.body).length){ switch(req.body.meta){ case 'login': this.loginWs(req, socket, function(code){ next(code) }) break case 'logout': this.logoutWs(req, socket, function(code){ next(code) }) break case 'signin': if(this.signinActive){ this.signinWs(req, socket, function(code){ next(code) }) } else{ next(401) } break case 'token': jwt.verify( req.body.data, this.secret, function(err, decoded) { if(!err){ // Add deocded data to body req.body.data = decoded.data.value self.loginWs(req, socket, function(code){ next(code) }) } else{ next(500) } }) break default: // Check if the path needs to be authenticated if(this.pathNeedsAuthentication(this.routesWs, req.url)){ this.verifyWs(req, socket, function(code){ next(code) }) } // Whooohooooo, nothing to dooooo, only parse token if exist to assign the user to req.user else{ if(this.websocketToken[req.token]){ this.verifyWs(req, socket, function(code){ next() }) } else{ next() } } break } } else{ socket.send(JSON.stringify({ err : 500, data : null})) } } catch(e){ next(e) } } /** * Check if the url needs to be authenticated * It's possible to define a url like '/admin' then every deeper url needs also to be authenticated * * @param {array} routes * @param {string} url */ pathNeedsAuthentication(routes, url){ for(let p in routes){ if(url.startsWith(routes[p])){ return true } } return false } /** * Login-route * If a user POST the login data, check if user exist in database. If user exist validate the posted data * If user is authenticated, create a jsonwebtoken and set the token to the cookie * If user is unauthorized, fire next with statuscode 401 * * ToDo: Add host and client-stuff to add more security * * @param {object} req * @param {object} res * @param {function} next callback with statuscode when error */ login(req, res, next){ let self = this // Handle the login if(req.method === 'POST' && Object.keys(req.body).length){ // get the user by name. The key is the username or email or,... req.db.query(this.db, 'get', { key : req.body.value[this.form.name] }, function(doc){ if(!doc.err){ if( self.validateHash( req.body.value[self.form.password], doc.data.value[self.form.password]) ){ // Assign the client-ip to token doc.data = Object.assign(doc.data, { _clientIp : self.getClientIp(req)}) // Create the token req.token = jwt.sign({data: doc.data}, self.secret, { expiresIn: self.expire }); // Set the cookie self.cookies.set(self.cookieName, req.token, self.cookieOptions) // Response if(self.redirectActive){ res.writeHead(302, {'Location': self.redirectPath}); res.end(); next(302) } else{ next(200) } } // Unauthorized else{ next(401) } } else{ // User not existing next(401) } }) } // Do nothing, when client get the login-page else if(req.method === 'GET'){ next() } // Oooops somethin went wrong, method or body error else{ next(500) } } /** * Websocket login, get the user, hold the sec-websocket-token as key and the jsonwebtoken as value in the object * The websocket connection generates on each connection a new token, that's why we doesn't save the token in the db * * @param {object} req * @param {object} socket * @param {function} next */ loginWs(req, socket, next){ let self = this req.db.query(this.db, 'get', { key : req.body.data.value[this.form.name] }, function(doc){ if(!doc.err){ // Assign the client-ip to token doc.data = Object.assign(doc.data, { _clientIp : self.getClientIp(req)}) if(self.validateHash( req.body.data.value[self.form.password], doc.data.value[self.form.password])){ self.setLoggedWs(req, doc, socket) } // if token is passed else if(req.body.data.value[self.form.password] === doc.data.value[self.form.password]){ self.setLoggedWs(req, doc, socket) } // Unauthorized else{ next(401) } } else{ // User not existing socket.send(JSON.stringify({ err : doc.err, data : { body : 'User not exist.', status : 500}})) } }) } // clear the websocket-object before adding the new authenticated user // create a key = username, value = token, create key = token, value = jsonwebtoken // to verify the user check the jsonwebtoken and the websocket token setLoggedWs(req, doc, socket){ // clear the websocket token in object first this.deleteWsByName(req.body.data.value[this.form.name]) // property username bind the unique base64 websocket id this.websocketToken[ req.body.data.value[this.form.name] ] = req.token // property unique base64 websocket id bind the jsonwebtoken this.websocketToken[req.token] = jwt.sign({data: doc.data}, this.secret, { expiresIn: this.expire }) // Response socket.send(JSON.stringify({ err : doc.err, data : { body : 'logged in.', status : 200}})) } /** * Logout-route * When a client wants to logout, clear the cookie data while replacing the token, the token can't be a empty string * ToDo: Check if it's possible to clear the cookie * * @param {object} req * @param {object} res * @param {function} next */ logout(req, res, next){ // cleare cookie this.cookies.set(this.cookieName, '-') // redirect to index if(this.redirectActive){ res.writeHead(302, {'Location': this.redirectPath}); res.end(); next(302) } else{ next(200) } } /** * Logs out the websocket connection while deleting the token in object * @param {object} req * @param {object} socket * @param {function} next */ logoutWs(req, socket, next){ if( this.websocketToken[req.token] ){ delete this.websocketToken[req.token] socket.send(JSON.stringify({ err : null, data : 200})) } else{ socket.send(JSON.stringify({ err : 500, data : null})) } } /** * Signin-route * When a client creates a user, check if method is post and handle the body, if user exist, send statuscode 409 * When user-data is ok, create the hashed password and save the user in the db * The username is the key * * @param {object} req * @param {object} res * @param {function} next callback with statuscode when error */ signin( req, res, next ){ let self = this if(req.method === 'POST' && Object.keys(req.body).length){ // Check if user exist, the key is the name req.db.query(this.db, 'get', { key : req.body.value[this.form.name] }, function(doc){ if(doc.err){ // Encrypt password req.body.value[self.form.password] = self.generateHash(req.body.value[self.form.password].toString()); // Add user req.db.query(self.db, 'put', { key : req.body.value[self.form.name], value : req.body }, function(doc){ // Response if(self.redirectActive){ res.writeHead(302, {'Location': self.redirectPath}); res.end(); next(302) } else{ next(200) } }) } else{ res.statusCode = 409 next(409) } }) } // Do nothing when get the file else if(req.method === 'GET'){ // Not handle for the auth, it's the get requs next() } // Oooops somethin went wrong else{ next(500) } } /** * Signin a new user * @param {object} req * @param {object} socket * @param {function} next */ signinWs(req, socket, next){ let self = this // Check if user exist, the key is the name req.db.query(this.db, 'get', { key : req.body.data.value[this.form.name] }, function(doc){ if(doc.err){ // Encrypt password req.body.data.value[self.form.password] = self.generateHash(req.body.data.value[self.form.password].toString()); // Add user req.db.query(self.db, 'put', { key : req.body.data.value[self.form.name], value : req.body.data }, function(doc){ next(200) }) } else{ next(409) } }) } /** * Verify the user, parse the jsonwebtoken, get the user in the database and compare the password. Notice the password is hashed and not in plain text. * @param {object} req * @param {object} res * @param {function} next callback with statuscode when error */ verify(req, res, next){ let self = this if(!req.token){ next(401) } else{ // verify a token jwt.verify(req.token, this.secret, function(err, decoded) { if(!err){ req.db.query(self.db, 'get', { key : decoded.data.value[self.form.name] }, function(doc){ // Authorized if(!doc.err && doc.data.value[self.form.password] == decoded.data.value[self.form.password] && decoded.data._clientIp === self.getClientIp(req)){ // empty callback to put the request to the other middlewares self.assignUserToReq(req, decoded.data) next() } // Unauthorized else{ next(401) } }) } else{ // Token parsing error next(401) } }) } } verifyWs(req, socket, next){ // verify a token let self = this jwt.verify( this.websocketToken[req.token], this.secret, function(err, decoded) { if(!err){ // check if the base64 unique websocket id exist in object and the clientIp is the same like in the request if(self.websocketToken[ decoded.data.value[self.form.name] ] === req.token && decoded.data._clientIp === self.getClientIp(req)){ self.assignUserToReq(req, decoded.data) next() } else{ socket.send(JSON.stringify({ err : err, data : 401})) } } else{ next(500) } }) } deleteWsByName(name){ if(this.websocketToken[name]){ delete this.websocketToken[ this.websocketToken[name] ] delete this.websocketToken[name] } } /** * Generating a hash, parse the salt * @param {string} plain * * return string */ generateHash (password) { const salt = crypto.randomBytes(16).toString('hex'); const hash = crypto.pbkdf2Sync(password, salt, 2048, 32, 'sha512').toString('hex'); return salt + hash } /** * Check if password is valid * First split the hash into salt and original hash * Then create a new hash by the password and the given salt described above * At the end, compare the two hashes * @param {string} password plain password * @param {string} passwordHashed hashed password salt - secret - hash * * return boolean */ validateHash (password, passwordHashed) { try{ const saltAndOriginalHash = passwordHashed.split(this.secret) const hash = crypto.pbkdf2Sync(password, passwordHashed.substr(0, 32), 2048, 32, 'sha512').toString('hex'); return hash === passwordHashed.substr(32, passwordHashed.length) } catch(e){ return false } } assignUserToReq(req, user){ if(this.assignUser) req.docs = Object.assign(req.docs, { auth : { key : user.key, name : user.value[this.form.name] }}) req.user = user } getClientIp(req) { return (req.headers["X-Forwarded-For"] || req.headers["x-forwarded-for"] || '').split(/\s*,\s*/)[0] || req.client.remoteAddress; }; } module.exports = AuthHandler