Authentication and Authorization in Node
From Logic Wiki
Contents
Download Sample Project
http://logicwiki.co.uk/Files/Authentication-and-Authorization.zip
Download another sample project
https://github.com/cornflourblue/node-jwt-authentication-api
NPM Packages
lodash
npm i lodash
const _ = require('lodash');
_.pick(user, ['name', 'email']) // gets user object and return a new object with only name and email fields in it. (to hide password from returning object)
joi-password-complexity
npm i joi-password-complexity
hash password
npm i bcrypt
const bcrypt = require('bcrypt');
Salt
cosnt salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash('password', salt);
JWT
npm i jsonwebtoken
Model
/models/user.js
const config = require('config');
const jwt = require('jsonwebtoken');
const Joi = require('joi');
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 5,
maxlength: 50
},
email: {
type: String,
required: true,
minlength: 5,
maxlength: 255,
unique: true
},
password: {
type: String,
required: true,
minlength: 5,
maxlength: 1024
},
isAdmin: Boolean
});
userSchema.methods.generateAuthToken = function() {
const token = jwt.sign({ _id: this._id, isAdmin: this.isAdmin }, config.get('jwtPrivateKey'));
return token;
}
const User = mongoose.model('User', userSchema);
function validateUser(user) {
const schema = {
name: Joi.string().min(5).max(50).required(),
email: Joi.string().min(5).max(255).required().email(),
password: Joi.string().min(5).max(255).required()
};
return Joi.validate(user, schema);
}
exports.User = User;
exports.validate = validateUser;
Register
/router/user.js
const auth = require('../middleware/auth');
const jwt = require('jsonwebtoken');
const config = require('config');
const bcrypt = require('bcrypt');
const _ = require('lodash');
const {User, validate} = require('../models/user');
const mongoose = require('mongoose');
const express = require('express');
const router = express.Router();
router.get('/me', auth, async (req, res) => {
const user = await User.findById(req.user._id).select('-password');
res.send(user);
});
router.post('/', async (req, res) => {
const { error } = validate(req.body);
if (error) return res.status(400).send(error.details[0].message);
let user = await User.findOne({ email: req.body.email });
if (user) return res.status(400).send('User already registered.');
user = new User(_.pick(req.body, ['name', 'email', 'password']));
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password, salt);
await user.save();
const token = user.generateAuthToken();
res.header('x-auth-token', token).send(_.pick(user, ['_id', 'name', 'email']));
});
module.exports = router;
Login
/router/auth.js
const Joi = require('joi');
const bcrypt = require('bcrypt');
const _ = require('lodash');
const {User} = require('../models/user');
const mongoose = require('mongoose');
const express = require('express');
const router = express.Router();
router.post('/', async (req, res) => {
const { error } = validate(req.body);
if (error) return res.status(400).send(error.details[0].message);
let user = await User.findOne({ email: req.body.email });
if (!user) return res.status(400).send('Invalid email or password.');
const validPassword = await bcrypt.compare(req.body.password, user.password);
if (!validPassword) return res.status(400).send('Invalid email or password.');
const token = user.generateAuthToken();
res.send(token);
});
function validate(req) {
const schema = {
email: Joi.string().min(5).max(255).required().email(),
password: Joi.string().min(5).max(255).required()
};
return Joi.validate(req, schema);
}
module.exports = router;
Middleware
/middleware/auth.js
const jwt = require('jsonwebtoken');
const config = require('config');
module.exports = function (req, res, next) {
const token = req.header('x-auth-token');
if (!token) return res.status(401).send('Access denied. No token provided.');
try {
const decoded = jwt.verify(token, config.get('jwtPrivateKey'));
req.user = decoded;
next();
}
catch (ex) {
res.status(400).send('Invalid token.');
}
}
/middleware/admin.js
module.exports = function (req, res, next) {
// 401 Unauthorized
// 403 Forbidden
if (!req.user.isAdmin) return res.status(403).send('Access denied.');
next();
}
index.js
const config = require('config');
const Joi = require('joi');
Joi.objectId = require('joi-objectid')(Joi);
const mongoose = require('mongoose');
const genres = require('./routes/genres');
const customers = require('./routes/customers');
const movies = require('./routes/movies');
const rentals = require('./routes/rentals');
const users = require('./routes/users');
const auth = require('./routes/auth');
const express = require('express');
const app = express();
if (!config.get('jwtPrivateKey')) {
console.error('FATAL ERROR: jwtPrivateKey is not defined.');
process.exit(1);
}
mongoose.connect('mongodb://localhost/vidly')
.then(() => console.log('Connected to MongoDB...'))
.catch(err => console.error('Could not connect to MongoDB...'));
app.use(express.json());
app.use('/api/genres', genres);
app.use('/api/customers', customers);
app.use('/api/movies', movies);
app.use('/api/rentals', rentals);
app.use('/api/users', users);
app.use('/api/auth', auth);
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`Listening on port ${port}...`));
Config
default.json
{
"jwtPrivateKey": ""
}
custom-environment-variables.json
{
"jwtPrivateKey": "vidly_jwtPrivateKey"
}
Check Authentication & Authorization
With the parameter array of [auth, admin] we implement auth check and also check if the user is Admin
router.delete('/:id', [auth, admin], async (req, res) => {
const genre = await Genre.findByIdAndRemove(req.params.id);
if (!genre) return res.status(404).send('The genre with the given ID was not found.');
res.send(genre);
});