├── .eslintrc.js ├── .gitignore ├── README.md ├── app.js ├── config.example.js ├── middlewares └── auth.js ├── models └── user.js ├── package.json └── routes └── api ├── auth ├── auth.controller.js └── index.js ├── index.js └── user ├── index.js └── user.controller.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | 4 14 | ], 15 | "linebreak-style": [ 16 | "error", 17 | "windows" 18 | ], 19 | "quotes": [ 20 | "error", 21 | "single" 22 | ], 23 | "semi": [ 24 | "error", 25 | "never" 26 | ], 27 | "no-console": 0 28 | } 29 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NODEJS-JWT-EXAMPLE 2 | This project is a sample implementation of an authentication system that uses JSON Web Token to manage users' login data in Node.js web server. 3 | 4 | Express.js, Mongoose, ES6 Syntax is used in this project. 5 | 6 | Tutorial on this project is available at https://velopert.com/2448 (KOREAN) 7 | 8 | ## Getting Started 9 | ### Prerequisites 10 | - node.js 6.9.x 11 | - npm 3.x 12 | - MongoDB 3.0 13 | 14 | ### Installing & Configuration 15 | 1) Install dependencies 16 | ``` 17 | npm install 18 | ``` 19 | 2) Rename `config.example.js` to `config.js` 20 | 3) Get a mongodb server and input `mongodbUri` of `config.js` 21 | 22 | ### Run the server 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ## APIs 28 | ### Auth Route 29 | #### Register 30 | `POST /api/auth/register` 31 | ``` 32 | { 33 | username, 34 | password 35 | } 36 | ``` 37 | **Description**: creates a new user; first user will be assigned as an admin user. Password is stored in `HMAC-SHA1` format 38 | #### Login 39 | `POST /api/auth/login` 40 | ``` 41 | { 42 | username, 43 | password 44 | } 45 | ``` 46 | **Description**: logs in to the server. Server will return a JWT token as: 47 | ```javascript 48 | { 49 | "message": "logged in successfully", 50 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1ODQ4MjU1NjJhOWRlMDE5NmM5MTI4ZmIiLCJ1c2VybmFtZSI6InRlc3RlciIsImFkbWluIjp0cnVlLCJpYXQiOjE0ODExMjMxNjMsImV4cCI6MTQ4MTcyNzk2MywiaXNzIjoidmVsb3BlcnQuY29tIiwic3ViIjoidXNlckluZm8ifQ.vh8LPqxYWJtO6Bxe7reL7sEon13dYFFnhpnyyEmaLBk" 51 | } 52 | ``` 53 | 54 | #### Check 55 | `GET /api/auth/check` or `GET /api/auth/check?token={token}` 56 | 57 | **Description**: checks the JWT. Token should be passed as Url-encoded query or `x-access-token` header 58 | 59 | ### User Route 60 | APIs in user routes need admin's permission to process 61 | 62 | #### Check 63 | `GET /api/user/list` 64 | 65 | **Description**: retrieves all user list 66 | 67 | #### Assign Admin 68 | `POST /api/user/assign-admin/:username` 69 | 70 | **Description**: assigns admin permission to the given user 71 | 72 | ## License 73 | [MIT License](http://opensource.org/licenses/MIT). 74 | Copyright (c) 2016 [velopert](https://www.velopert.com/). -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* ======================= 2 | LOAD THE DEPENDENCIES 3 | ==========================*/ 4 | const express = require('express') 5 | const bodyParser = require('body-parser') 6 | const morgan = require('morgan') 7 | const mongoose = require('mongoose') 8 | 9 | /* ======================= 10 | LOAD THE CONFIG 11 | ==========================*/ 12 | const config = require('./config') 13 | const port = process.env.PORT || 3000 14 | 15 | /* ======================= 16 | EXPRESS CONFIGURATION 17 | ==========================*/ 18 | const app = express() 19 | 20 | // parse JSON and url-encoded query 21 | app.use(bodyParser.urlencoded({extended: false})) 22 | app.use(bodyParser.json()) 23 | 24 | // print the request log on console 25 | app.use(morgan('dev')) 26 | 27 | // set the secret key variable for jwt 28 | app.set('jwt-secret', config.secret) 29 | 30 | // index page, just for testing 31 | app.get('/', (req, res) => { 32 | res.send('Hello JWT') 33 | }) 34 | 35 | // configure api router 36 | app.use('/api', require('./routes/api')) 37 | 38 | // open the server 39 | app.listen(port, () => { 40 | console.log(`Express is running on port ${port}`) 41 | }) 42 | 43 | 44 | 45 | /* ======================= 46 | CONNECT TO MONGODB SERVER 47 | ==========================*/ 48 | mongoose.connect(config.mongodbUri) 49 | mongoose.Promise = global.Promise 50 | const db = mongoose.connection 51 | db.on('error', console.error) 52 | db.once('open', ()=>{ 53 | console.log('connected to mongodb server') 54 | }) -------------------------------------------------------------------------------- /config.example.js: -------------------------------------------------------------------------------- 1 | // please rename this file to config.js 2 | module.exports = { 3 | 'secret': 'SeCrEtKeYfOrHaShInG', 4 | 'mongodbUri': 'mongodb://username:pass@host:port/collection_name' 5 | } -------------------------------------------------------------------------------- /middlewares/auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | 3 | const authMiddleware = (req, res, next) => { 4 | // read the token from header or url 5 | const token = req.headers['x-access-token'] || req.query.token 6 | 7 | // token does not exist 8 | if(!token) { 9 | return res.status(403).json({ 10 | success: false, 11 | message: 'not logged in' 12 | }) 13 | } 14 | 15 | // create a promise that decodes the token 16 | const p = new Promise( 17 | (resolve, reject) => { 18 | jwt.verify(token, req.app.get('jwt-secret'), (err, decoded) => { 19 | if(err) reject(err) 20 | resolve(decoded) 21 | }) 22 | } 23 | ) 24 | 25 | // if it has failed to verify, it will return an error message 26 | const onError = (error) => { 27 | res.status(403).json({ 28 | success: false, 29 | message: error.message 30 | }) 31 | } 32 | 33 | // process the promise 34 | p.then((decoded)=>{ 35 | req.decoded = decoded 36 | next() 37 | }).catch(onError) 38 | } 39 | 40 | module.exports = authMiddleware -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | const Schema = mongoose.Schema 3 | const crypto = require('crypto') 4 | const config = require('../config') 5 | 6 | const User = new Schema({ 7 | username: String, 8 | password: String, 9 | admin: { type: Boolean, default: false } 10 | }) 11 | 12 | 13 | // crypto.createHmac('sha1', 'secret') 14 | // .update('mypasswssord') 15 | // .digest('base64') 16 | 17 | 18 | // create new User document 19 | User.statics.create = function(username, password) { 20 | const encrypted = crypto.createHmac('sha1', config.secret) 21 | .update(password) 22 | .digest('base64') 23 | 24 | const user = new this({ 25 | username, 26 | password: encrypted 27 | }) 28 | 29 | // return the Promise 30 | return user.save() 31 | } 32 | 33 | // find one user by using username 34 | User.statics.findOneByUsername = function(username) { 35 | return this.findOne({ 36 | username 37 | }).exec() 38 | } 39 | 40 | // verify the password of the User documment 41 | User.methods.verify = function(password) { 42 | const encrypted = crypto.createHmac('sha1', config.secret) 43 | .update(password) 44 | .digest('base64') 45 | console.log(this.password === encrypted) 46 | 47 | return this.password === encrypted 48 | } 49 | 50 | User.methods.assignAdmin = function() { 51 | this.admin = true 52 | return this.save() 53 | } 54 | 55 | module.exports = mongoose.model('User', User) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-jwt-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "body-parser": "^1.15.2", 15 | "express": "^4.14.0", 16 | "jsonwebtoken": "^7.1.9", 17 | "mongoose": "^4.7.1", 18 | "morgan": "^1.7.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^3.11.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /routes/api/auth/auth.controller.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const User = require('../../../models/user') 3 | 4 | /* 5 | POST /api/auth/register 6 | { 7 | username, 8 | password 9 | } 10 | */ 11 | 12 | exports.register = (req, res) => { 13 | const { username, password } = req.body 14 | let newUser = null 15 | 16 | // create a new user if does not exist 17 | const create = (user) => { 18 | if(user) { 19 | throw new Error('username exists') 20 | } else { 21 | return User.create(username, password) 22 | } 23 | } 24 | 25 | // count the number of the user 26 | const count = (user) => { 27 | newUser = user 28 | return User.count({}).exec() 29 | } 30 | 31 | // assign admin if count is 1 32 | const assign = (count) => { 33 | if(count === 1) { 34 | return newUser.assignAdmin() 35 | } else { 36 | // if not, return a promise that returns false 37 | return Promise.resolve(false) 38 | } 39 | } 40 | 41 | // respond to the client 42 | const respond = (isAdmin) => { 43 | res.json({ 44 | message: 'registered successfully', 45 | admin: isAdmin ? true : false 46 | }) 47 | } 48 | 49 | // run when there is an error (username exists) 50 | const onError = (error) => { 51 | res.status(409).json({ 52 | message: error.message 53 | }) 54 | } 55 | 56 | // check username duplication 57 | User.findOneByUsername(username) 58 | .then(create) 59 | .then(count) 60 | .then(assign) 61 | .then(respond) 62 | .catch(onError) 63 | } 64 | 65 | /* 66 | POST /api/auth/login 67 | { 68 | username, 69 | password 70 | } 71 | */ 72 | 73 | exports.login = (req, res) => { 74 | const {username, password} = req.body 75 | const secret = req.app.get('jwt-secret') 76 | 77 | // check the user info & generate the jwt 78 | const check = (user) => { 79 | if(!user) { 80 | // user does not exist 81 | throw new Error('login failed') 82 | } else { 83 | // user exists, check the password 84 | if(user.verify(password)) { 85 | // create a promise that generates jwt asynchronously 86 | const p = new Promise((resolve, reject) => { 87 | jwt.sign( 88 | { 89 | _id: user._id, 90 | username: user.username, 91 | admin: user.admin 92 | }, 93 | secret, 94 | { 95 | expiresIn: '7d', 96 | issuer: 'velopert.com', 97 | subject: 'userInfo' 98 | }, (err, token) => { 99 | if (err) reject(err) 100 | resolve(token) 101 | }) 102 | }) 103 | return p 104 | } else { 105 | throw new Error('login failed') 106 | } 107 | } 108 | } 109 | 110 | // respond the token 111 | const respond = (token) => { 112 | res.json({ 113 | message: 'logged in successfully', 114 | token 115 | }) 116 | } 117 | 118 | // error occured 119 | const onError = (error) => { 120 | res.status(403).json({ 121 | message: error.message 122 | }) 123 | } 124 | 125 | // find the user 126 | User.findOneByUsername(username) 127 | .then(check) 128 | .then(respond) 129 | .catch(onError) 130 | 131 | } 132 | 133 | /* 134 | GET /api/auth/check 135 | */ 136 | 137 | exports.check = (req, res) => { 138 | res.json({ 139 | success: true, 140 | info: req.decoded 141 | }) 142 | } -------------------------------------------------------------------------------- /routes/api/auth/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const controller = require('./auth.controller') 3 | const authMiddleware = require('../../../middlewares/auth') 4 | 5 | router.post('/register', controller.register) 6 | router.post('/login', controller.login) 7 | 8 | router.use('/check', authMiddleware) 9 | router.get('/check', controller.check) 10 | 11 | module.exports = router -------------------------------------------------------------------------------- /routes/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const authMiddleware = require('../../middlewares/auth') 3 | const auth = require('./auth') 4 | const user = require('./user') 5 | 6 | router.use('/auth', auth) 7 | router.use('/user', authMiddleware) 8 | router.use('/user', user) 9 | 10 | module.exports = router -------------------------------------------------------------------------------- /routes/api/user/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router() 2 | const controller = require('./user.controller') 3 | 4 | router.get('/list', controller.list) 5 | router.post('/assign-admin/:username', controller.assignAdmin) 6 | 7 | module.exports = router -------------------------------------------------------------------------------- /routes/api/user/user.controller.js: -------------------------------------------------------------------------------- 1 | const User = require('../../../models/user') 2 | 3 | /* 4 | GET /api/user/list 5 | */ 6 | 7 | exports.list = (req, res) => { 8 | // refuse if not an admin 9 | if(!req.decoded.admin) { 10 | return res.status(403).json({ 11 | message: 'you are not an admin' 12 | }) 13 | } 14 | 15 | User.find({}, '-password').exec() 16 | .then( 17 | users=> { 18 | res.json({users}) 19 | } 20 | ) 21 | 22 | } 23 | 24 | 25 | /* 26 | POST /api/user/assign-admin/:username 27 | */ 28 | exports.assignAdmin = (req, res) => { 29 | // refuse if not an admin 30 | if(!req.decoded.admin) { 31 | return res.status(403).json({ 32 | message: 'you are not an admin' 33 | }) 34 | } 35 | 36 | User.findOneByUsername(req.params.username) 37 | .then( 38 | user => { 39 | if(!user) throw new Error('user not found') 40 | user.assignAdmin() 41 | } 42 | ).then( 43 | res.json({ 44 | success: true 45 | }) 46 | ).catch( 47 | (err) => { res.status(404).json({message: err.message})} 48 | ) 49 | } --------------------------------------------------------------------------------