├── .gitignore ├── README.md ├── app.js ├── controllers ├── AuthController.js ├── RecipeController.js └── UserController.js ├── models ├── Recipe.js └── User.js ├── package.json └── private └── config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Important! This repository is over 5 years old, is not maintained anymore, and is likely not up to date with best security practices! Please don't use it any production system, I've kept it around in archive-only mode for educational purposes. 2 | 3 | # NodeJS secure RESTFUL api 4 | 5 | A minimal, secure RESTFUL api for NodeJS. This project includes user login, access control of objects, and encrypted hashing of passwords right out of the box! Just delete the example model, add your own, and run! 6 | 7 | # Installation 8 | 9 | * Clone the repo by using ```git clone```. 10 | * Run ```npm install``` on the cloned directory. 11 | * Edit the private/config.js file to suit your needs. 12 | * Add APIs using the instructions below to suit your needs. 13 | 14 | # Steps to add new API 15 | 16 | * Copy the template model (models/Recipe.js) to a new file in the **models** folder and make the modifications outlined in the header. 17 | 18 | ```copy models/Recipe.js --> models/Custom.js``` 19 | 20 | * Copy the template controller (controllers/RecipeController.js) to a new file in the **controllers** folder and make the modifications outlined in the header. 21 | 22 | ```copy controllers/RecipeController.js --> controllers/CustomController.js``` 23 | 24 | * Import your controller in app.js underneath the existing controllers, like so: 25 | 26 | ``` 27 | var recipeController = require('./controllers/RecipeController'); 28 | var customController = require('./controllers/CustomController'); 29 | ``` 30 | 31 | * Add the routing line to app.js underneath the existing routes, like so: 32 | 33 | ``` 34 | app.use('/api', recipeController); 35 | app.use('/api', customController); 36 | ``` 37 | 38 | # Running the software 39 | 40 | * ```node app.js``` for simple setups. 41 | * I would recommend looking at [the pm2 module](https://www.npmjs.com/package/pm2) for running on a production server. 42 | 43 | # Creating users 44 | 45 | To create users, simply send a GET to /user/create with the required fields in the query string, like so: 46 | 47 | ``` 48 | http://localhost:3000/user/create?username=hello&password=world 49 | ``` 50 | 51 | # API Endpoints 52 | 53 | ``` 54 | GET http://localhost:3000/api/recipe/list 55 | GET http://localhost:3000/api/recipe/create?foo=hello&bar=world // creates object with fields foo=hello, bar=world 56 | GET http://localhost:3000/api/recipe/get/:id // gets object with Mongo id ":id" 57 | GET http://localhost:3000/api/update/get/:id?foo=hello&bar=world // updates object with Mongo id ":id" and fields foo=hello, bar=world 58 | GET http://localhost:3000/api/recipe/delete/:id // deletes object with Mongo id ":id" 59 | ``` 60 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * Main execution file for this project. 5 | */ 6 | 7 | /** External modules **/ 8 | var express = require('express'); 9 | var mongoose = require('mongoose'); 10 | var bodyParser = require('body-parser'); 11 | var session = require('cookie-session'); 12 | var cookieParser = require('cookie-parser'); 13 | var passport = require('passport'); 14 | 15 | /** Internal modules **/ 16 | var config = require('./private/config'); 17 | var authController = require('./controllers/AuthController'); 18 | var userController = require('./controllers/UserController'); 19 | var recipeController = require('./controllers/RecipeController'); 20 | 21 | /** Database setup **/ 22 | mongoose.connect(config.DB_PATH); 23 | 24 | /** Express setup **/ 25 | var app = express(); 26 | 27 | app.set('trust proxy',1) // trust first proxy 28 | app.set('json spaces',4); 29 | app.use(cookieParser()); 30 | app.use(bodyParser.urlencoded({ extended: false})); 31 | app.use(session({ 32 | keys: config.SESSION_SECRET_KEYS, 33 | cookie: { maxAge: 60000 } 34 | })) 35 | app.use(passport.initialize()); 36 | app.use(passport.session()); 37 | 38 | /** Express routing **/ 39 | 40 | app.use('*', function (req, res, next) { 41 | console.log("METHOD:",req.method,req.originalUrl,"| USER:",req.user !== undefined ? req.user.username : "undefined"); 42 | next(); 43 | }); 44 | 45 | app.use('/', authController.router); 46 | app.use('/', userController); 47 | app.use('/api', authController.isAuthenticated); 48 | app.use('/api', recipeController); 49 | app.all('*', function (req, res){ 50 | res.status(403).send('403 - Forbidden'); 51 | }) 52 | 53 | /** Server deployment **/ 54 | var port = config.PORT || 3000; 55 | app.listen(port) 56 | 57 | console.log('\n--- Information ---'); 58 | console.log(' Port:',port); 59 | console.log(' Database:',config.DB_PATH); 60 | console.log(' Cookie Session Keys:'); 61 | 62 | for (i in config.SESSION_SECRET_KEYS) { 63 | console.log(' - '+config.SESSION_SECRET_KEYS[i]); 64 | } 65 | -------------------------------------------------------------------------------- /controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthController.js 3 | * 4 | * Unless you are trying to implement some custom functionality, you shouldn't 5 | * need to edit this file. 6 | */ 7 | 8 | var modelLocation = '../models/User' 9 | 10 | /**************************************************************** 11 | * DO NOT TOUCH BELOW THIS LINE * 12 | ****************************************************************/ 13 | 14 | var util = require('util'); 15 | var express = require('express'); 16 | var router = express.Router(); 17 | var passport = require('passport'); 18 | var config = require('../private/config'); 19 | 20 | /** Model and route setup **/ 21 | 22 | var User = require(modelLocation).model; 23 | 24 | /**************************************************************** 25 | * Local Strategy * 26 | ****************************************************************/ 27 | 28 | var LocalStrategy = require('passport-local').Strategy; 29 | 30 | passport.use('local', new LocalStrategy({ 31 | passReqToCallback: true 32 | }, 33 | function(req, username, password, done) { 34 | var caseInsensitiveRegex = new RegExp('^' + username + '$', "i"); 35 | User.findOne({ 36 | 'username': caseInsensitiveRegex 37 | }, 38 | function(err, user) { 39 | if (err) return done(err); 40 | if (!user) return done(null, false, req.flash('error', 'User Not found.')); 41 | 42 | user.authenticate(password, function(res) { 43 | if (res === false) 44 | return done(null, false, req.flash('error', 'Invalid Password')); 45 | 46 | req.logIn(user, function(err) { 47 | if (err) return next(err); 48 | return done(null, user); 49 | }); 50 | }); 51 | }); 52 | 53 | })); 54 | 55 | passport.serializeUser(function(user, done) { 56 | done(null, user); 57 | }); 58 | 59 | passport.deserializeUser(function(user, done) { 60 | User.findById(user._id, function (err, user) { 61 | done(err, user); 62 | }); 63 | }); 64 | 65 | module.exports.isAuthenticated = function (req, res, next) { 66 | if (!req.user) return res.status(403).send('Unauthorized'); 67 | if (User.findOne({'_id': req.user._id}, function (err, res) { 68 | if (err) return res.status(403).send('Unauthorized'); 69 | next(); 70 | })); 71 | } 72 | 73 | /**************************************************************** 74 | * Login methods * 75 | ****************************************************************/ 76 | 77 | router.post('/login', passport.authenticate('local'), function(req, res) { 78 | console.log('User: ',req.user.username); 79 | return res.json({status: 'Success', message: 'Logged in!'}) 80 | }); 81 | 82 | module.exports.router = router; 83 | -------------------------------------------------------------------------------- /controllers/RecipeController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default REST contoller for your db. 3 | * 4 | * Usage: 5 | * (1) Change the modelLocation variable to the location where your corresponding model 6 | * is stored. 7 | * 8 | * (2 - optional) Add custom routing for your API. NOTE: If you don't know what this means, 9 | * you don't need it. 10 | */ 11 | 12 | var modelLocation = '../models/Recipe' 13 | 14 | /**************************************************************** 15 | * DO NOT TOUCH BELOW THIS LINE * 16 | ****************************************************************/ 17 | 18 | var util = require('util'); 19 | var express = require('express'); 20 | var bodyParser = require('body-parser'); 21 | var authController = require('./AuthController'); 22 | 23 | /** Model and route setup **/ 24 | 25 | var model = require(modelLocation).model; 26 | var userModel = require('../models/User').model; 27 | 28 | const route = require(modelLocation).route; 29 | const routeIdentifier = util.format('/%s', route); 30 | 31 | /** Express setup **/ 32 | 33 | var router = express.Router(); 34 | 35 | /** Express routing **/ 36 | 37 | /* 38 | * Check to make sure user option is valid. This prevents user spoofing 39 | * and cements and invalid login attempts. 40 | * 41 | */ 42 | 43 | router.use('*', function (req, res, next) { 44 | if (!req.user) { 45 | return res.status(403).send('403 - Forbidden'); 46 | } 47 | 48 | if (userModel.findOne({'_id': req.user._id}, function (err, res) { 49 | if (err) { 50 | return res.send(err); 51 | } 52 | 53 | next(); 54 | })); 55 | }); 56 | 57 | /* 58 | * GET /list 59 | * 60 | */ 61 | 62 | router.get(routeIdentifier+'/list', function(req, res, next) { 63 | model.find({'owner':req.user._id}, function (err, objects) { 64 | if (err) return res.send(err); 65 | return res.json(objects); 66 | }); 67 | }); 68 | 69 | /* 70 | * GET /create 71 | * 72 | */ 73 | 74 | router.get(routeIdentifier+'/create', function(req, res, next) { 75 | req.body.owner = req.user._id; 76 | model.create(req.query, function (err, entry) { 77 | if (err) return next(err); 78 | return res.json({ 79 | status: 'Success', 80 | message: 'Item created!' 81 | }); 82 | }); 83 | }); 84 | 85 | /* 86 | * GET /get/:id 87 | * 88 | */ 89 | 90 | router.get(routeIdentifier+'/get/:id', function (req, res, next) { 91 | model.findOne({ 92 | '_id':req.params.id, 93 | 'owner':req.user._id 94 | }, function (err, entry){ 95 | if(err) return res.send(err); 96 | return res.json(entry); 97 | }); 98 | }); 99 | 100 | /* 101 | * GET /update/:id 102 | * 103 | */ 104 | 105 | router.get(routeIdentifier+'/update/:id', function(req, res, next) { 106 | model.findOneAndUpdate({ 107 | '_id':req.params.id, 108 | 'owner':req.user._id 109 | }, 110 | req.query, 111 | function (err, entry) { 112 | if (err) return res.send(err); 113 | return res.json({status: 'Success', message: 'Updated item'}); 114 | }); 115 | }); 116 | 117 | /* 118 | * GET /delete/:id 119 | * 120 | */ 121 | 122 | router.get(routeIdentifier+'/delete/:id', function (req, res, next) { 123 | model.findOneAndRemove({ 124 | '_id':req.params.id, 125 | 'owner':req.user._id 126 | }, 127 | req.body, 128 | function (err, entry) { 129 | if (err) return res.send(err); 130 | return res.json({status: 'Success', message: 'Deleted item'}); 131 | }); 132 | }); 133 | 134 | module.exports = router; 135 | -------------------------------------------------------------------------------- /controllers/UserController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UserController.js 3 | * 4 | * Unless you are trying to implement some custom functionality, you shouldn't 5 | * need to edit this file. 6 | */ 7 | 8 | var modelLocation = '../models/User' 9 | 10 | /**************************************************************** 11 | * DO NOT TOUCH BELOW THIS LINE * 12 | ****************************************************************/ 13 | 14 | var util = require('util'); 15 | var express = require('express'); 16 | 17 | /** Model and route setup **/ 18 | 19 | var model = require(modelLocation).model; 20 | const route = require(modelLocation).route; 21 | const routeIdentifier = util.format('/%s', route); 22 | 23 | /** Router setup **/ 24 | 25 | var router = express.Router(); 26 | 27 | /** Express routing **/ 28 | 29 | /* 30 | * GET /create 31 | * 32 | */ 33 | 34 | router.get(routeIdentifier+'/create', function(req, res, next) { 35 | if (req.query === undefined || req.query.username === undefined || req.query.password === undefined) { 36 | return res.json({ 37 | status: 'Failure', 38 | message: 'Both username and password must be defined in the query string!' 39 | }); 40 | } 41 | 42 | if (req.query.username === "") { 43 | return res.json({ 44 | status: 'Failure', 45 | message: 'Username cannot be empty!' 46 | }); 47 | } 48 | 49 | if (req.query.password === "") { 50 | return res.json({ 51 | status: 'Failure', 52 | message: 'Password cannot be empty!' 53 | }); 54 | } 55 | 56 | model.create(req.query, function (err, entry) { 57 | if (err) return res.send(err); 58 | 59 | return res.json({ 60 | status: 'Success', 61 | message: 'User was created!' 62 | }); 63 | }); 64 | }); 65 | 66 | 67 | module.exports = router; 68 | -------------------------------------------------------------------------------- /models/Recipe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default skeleton for a model in your db. 3 | * 4 | * Usage: 5 | * (1) Change the route to what route you would like to access this model at. 6 | * For instance, if the route is 'recipe', then your API should be accessible 7 | * at http://localhost:3000/api/recipe. 8 | * 9 | * (2) Change the modelId to the name of your file without the extension (.js). 10 | * This name is generally capitalized. 11 | * 12 | * (3) Edit the mongoose schema (instructions on doing so: http://mongoosejs.com/docs/guide.html) 13 | */ 14 | 15 | var mongoose = require('mongoose'); 16 | 17 | const route = 'recipe'; // Route: 'recipe' routes to /recipe 18 | const modelId = 'Recipe'; // Same name as file, no extension: Recipe' 19 | 20 | var Schema = new mongoose.Schema({ 21 | 22 | /** Make your edits here **/ 23 | 24 | name: {type: String, required: true}, 25 | author: String, 26 | type: String, 27 | feeds: Number, 28 | updated_at: { type: Date, default: Date.now }, 29 | 30 | /** Must keep the owner property **/ 31 | 32 | owner: { 33 | type: String, 34 | required: true 35 | } 36 | }); 37 | 38 | /**************************************************************** 39 | * DO NOT TOUCH BELOW THIS LINE * 40 | ****************************************************************/ 41 | 42 | module.exports = { 43 | model: mongoose.model(modelId, Schema), 44 | route: route 45 | } -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default skeleton for a model in your db. 3 | * 4 | * Usage: 5 | * (1) Change the route to what route you would like to access this model at. 6 | * For instance, if the route is 'recipe', then your API should be accessible 7 | * at http://localhost:3000/api/recipe. 8 | * 9 | * (2) Change the modelId to the name of your file without the extension (.js). 10 | * This name is generally capitalized. 11 | * 12 | * (3) Edit the mongoose schema (instructions on doing so: http://mongoosejs.com/docs/guide.html) 13 | */ 14 | 15 | var mongoose = require('mongoose'); 16 | var bcrypt = require('bcrypt-nodejs'); 17 | 18 | const route = 'user'; // Route: 'recipe' routes to /recipe 19 | const modelId = 'User'; // Same name as file, no extension: Recipe' 20 | 21 | var Schema = new mongoose.Schema({ 22 | username: { 23 | type: String, 24 | unique: true, 25 | required: true 26 | }, 27 | password: { 28 | type: String, 29 | required: true 30 | }, 31 | email: { 32 | type: String, 33 | unique: true 34 | } 35 | }); 36 | 37 | Schema.pre('save', function (cb) { 38 | var currentUser = this; 39 | if (!currentUser.isModified('password')) return cb(); 40 | 41 | bcrypt.genSalt(5, function (err, salt){ 42 | if (err) return cb(err); 43 | 44 | bcrypt.hash(currentUser.password, salt, null, function (err, hash) { 45 | if (err) return cb(err); 46 | currentUser.password = hash; 47 | return cb(); 48 | }); 49 | }); 50 | }); 51 | 52 | Schema.methods.authenticate = function (pass, cb) { 53 | bcrypt.compare(pass, this.password, function (err, res){ 54 | console.log("Login: "+res); 55 | if (err) return cb(err); 56 | cb(res); 57 | }); 58 | }; 59 | 60 | Schema.plugin(require('mongoose-findorcreate')); 61 | 62 | /**************************************************************** 63 | * DO NOT TOUCH BELOW THIS LINE * 64 | ****************************************************************/ 65 | 66 | module.exports = { 67 | model: mongoose.model(modelId, Schema), 68 | route: route 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-secure-rest-api", 3 | "version": "0.1.0", 4 | "description": "A skeleton for a secure REST api for NodeJS.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "REST", 11 | "api" 12 | ], 13 | "author": "Clay McLeod", 14 | "license": "MIT", 15 | "dependencies": { 16 | "bcrypt-nodejs": "0.0.3", 17 | "body-parser": "1.12.2", 18 | "cookie-parser": "1.3.4", 19 | "cookie-session": "^1.2.0", 20 | "express": "4.12.3", 21 | "mongoose": "3.8.25", 22 | "mongoose-findorcreate": "0.1.2", 23 | "morgan": "1.5.2", 24 | "passport": "0.2.1", 25 | "passport-local": "^1.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /private/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * config.js 3 | * 4 | * Edit each of these configuration settings as you like. 5 | */ 6 | 7 | var crypto = require('crypto'); 8 | 9 | module.exports = { 10 | /** Recommended customization **/ 11 | 12 | DB_PATH: 'mongodb://localhost:27017/mydatabase', 13 | PORT: 3000, 14 | 15 | /** Recommend that you leave these configuration settings **/ 16 | 17 | SESSION_SECRET_KEYS: [ 18 | crypto.randomBytes(32).toString('hex'), crypto.randomBytes(32).toString('hex'), 19 | crypto.randomBytes(32).toString('hex'), crypto.randomBytes(32).toString('hex') 20 | ], 21 | } 22 | --------------------------------------------------------------------------------