├── .gitignore ├── index.js ├── .DS_Store ├── src ├── .DS_Store ├── error │ ├── index.js │ └── HttpError.js ├── controllers │ ├── index.js │ ├── users.controller.js │ └── auth.controller.js ├── models │ ├── index.js │ ├── user.model.js │ └── refreshToken.model.js ├── logger │ └── index.js ├── routes │ ├── index.js │ ├── users.js │ └── auth.js ├── index.js ├── middlewares │ └── index.js ├── database │ └── index.js └── util │ └── index.js ├── docker-compose.yml └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | .idea 4 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | require('./src')(); -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxim04/video-1-nodejs-jwt/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxim04/video-1-nodejs-jwt/HEAD/src/.DS_Store -------------------------------------------------------------------------------- /src/error/index.js: -------------------------------------------------------------------------------- 1 | const HttpError = require("./HttpError"); 2 | 3 | module.exports = { 4 | HttpError 5 | }; -------------------------------------------------------------------------------- /src/controllers/index.js: -------------------------------------------------------------------------------- 1 | const auth = require('./auth.controller'); 2 | const users = require('./users.controller'); 3 | 4 | module.exports = { 5 | auth, 6 | users 7 | }; -------------------------------------------------------------------------------- /src/models/index.js: -------------------------------------------------------------------------------- 1 | const User = require('./user.model'); 2 | const RefreshToken = require('./refreshToken.model'); 3 | 4 | module.exports = { 5 | User, 6 | RefreshToken 7 | }; -------------------------------------------------------------------------------- /src/logger/index.js: -------------------------------------------------------------------------------- 1 | const pino = require('pino'); 2 | const logger = pino({ 3 | level: process.env.LOG_LEVEL, 4 | timestamp: pino.stdTimeFunctions.isoTime 5 | }); 6 | module.exports = logger; -------------------------------------------------------------------------------- /src/error/HttpError.js: -------------------------------------------------------------------------------- 1 | class HttpError extends Error { 2 | constructor(statusCode, message) { 3 | super(message); 4 | this.statusCode = statusCode; 5 | } 6 | } 7 | 8 | module.exports = HttpError; -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const authRouter = require('./auth'); 4 | const usersRouter = require('./users'); 5 | 6 | router.use('/auth', authRouter); 7 | router.use('/users', usersRouter); 8 | 9 | module.exports = router; -------------------------------------------------------------------------------- /src/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const controllers = require("../controllers"); 3 | const middlewares = require("../middlewares"); 4 | const router = express.Router(); 5 | 6 | router.get('/me', middlewares.verifyAccessToken, controllers.users.me); 7 | 8 | module.exports = router; -------------------------------------------------------------------------------- /src/models/user.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema, model} = mongoose; 3 | 4 | const userSchema = new Schema({ 5 | username: {type: String, unique: true}, 6 | password: {type: String, select: false} 7 | }); 8 | 9 | const User = model('User', userSchema); 10 | 11 | module.exports = User; -------------------------------------------------------------------------------- /src/models/refreshToken.model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const { Schema, model} = mongoose; 3 | 4 | const refreshTokenSchema = new Schema({ 5 | owner: {type: Schema.Types.ObjectId, ref: 'User'} 6 | }); 7 | 8 | const RefreshToken = model('RefreshToken', refreshTokenSchema); 9 | 10 | module.exports = RefreshToken; -------------------------------------------------------------------------------- /src/controllers/users.controller.js: -------------------------------------------------------------------------------- 1 | const {errorHandler} = require("../util"); 2 | const models = require("../models"); 3 | const {HttpError} = require("../error"); 4 | 5 | const me = errorHandler(async (req, res) => { 6 | const userDoc = await models.User.findById(req.userId).exec(); 7 | if (!userDoc) { 8 | throw new HttpError(400, 'User not found'); 9 | } 10 | return userDoc; 11 | }); 12 | 13 | module.exports = { 14 | me 15 | }; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | mongodb-primary: 5 | image: 'bitnami/mongodb:latest' 6 | ports: 7 | - "27017:27017" 8 | environment: 9 | - MONGODB_ADVERTISED_HOSTNAME=localhost 10 | - MONGODB_USERNAME=user1 11 | - MONGODB_PASSWORD=pass1 12 | - MONGODB_DATABASE=db1 13 | - MONGODB_REPLICA_SET_MODE=primary 14 | - MONGODB_ROOT_PASSWORD=rootpass1 15 | - MONGODB_REPLICA_SET_KEY=replicasetkey123 16 | - MONGODB_REPLICA_SET_NAME=rs1 -------------------------------------------------------------------------------- /src/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const controllers = require("../controllers"); 3 | const router = express.Router(); 4 | 5 | router.post('/signup', controllers.auth.signup); 6 | router.post('/login', controllers.auth.login); 7 | router.post('/logout', controllers.auth.logout); 8 | router.post('/logoutAll', controllers.auth.logoutAll); 9 | router.post('/accessToken', controllers.auth.newAccessToken); 10 | router.post('/refreshToken', controllers.auth.newRefreshToken); 11 | 12 | module.exports = router; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwt", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js | ./node_modules/.bin/pino-pretty --translateTime --colorize", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "argon2": "^0.28.5", 14 | "dotenv": "^16.0.0", 15 | "express": "^4.17.3", 16 | "jsonwebtoken": "^8.5.1", 17 | "mongoose": "^6.2.7", 18 | "pino": "^7.9.0", 19 | "pino-pretty": "^7.5.4" 20 | }, 21 | "devDependencies": { 22 | "nodemon": "^2.0.15" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const logger = require("./logger"); 3 | const routes = require('./routes'); 4 | const connectToDatabase = require("./database"); 5 | const app = express(); 6 | const port = process.env.PORT || 3000; 7 | 8 | app.use(express.json()); 9 | 10 | app.use('/api', routes); 11 | 12 | app.use((err, req, res, next) => { 13 | logger.error(err.stack); 14 | res.status(err.statusCode || 500) 15 | .send({ error: err.message }); 16 | }); 17 | 18 | async function startServer() { 19 | await connectToDatabase(); 20 | 21 | app.listen(port, () => { 22 | logger.info(`Server listening at http://localhost:${port}`); 23 | }); 24 | } 25 | 26 | module.exports = startServer; -------------------------------------------------------------------------------- /src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | const {HttpError} = require('../error'); 2 | const {errorHandler} = require("../util"); 3 | const jwt = require("jsonwebtoken"); 4 | 5 | 6 | const verifyAccessToken = errorHandler(async (req, res, next) => { 7 | const authHeader = req.headers['authorization']; 8 | const token = authHeader && authHeader.split(' ')[1]; 9 | 10 | if (!token) { 11 | throw new HttpError(401, 'Unauthorized'); 12 | } 13 | 14 | try { 15 | const decodedToken = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET); 16 | req.userId = decodedToken.userId; 17 | next(); 18 | } catch (e) { 19 | throw new HttpError(401, 'Unauthorized'); 20 | } 21 | }); 22 | 23 | module.exports = { 24 | verifyAccessToken 25 | }; -------------------------------------------------------------------------------- /src/database/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const logger = require("../logger"); 3 | mongoose.Promise = global.Promise; 4 | 5 | async function connectToDatabase() { 6 | try { 7 | const user = process.env.DB_USER; 8 | const password = process.env.DB_PASS; 9 | const host = process.env.DB_HOST; 10 | const port = process.env.DB_PORT; 11 | const dbName = process.env.DB_NAME; 12 | const rs = process.env.DB_REPLICA_SET; 13 | 14 | const connectionString = `mongodb://${user}:${password}@${host}:${port}/${dbName}?replicaSet=${rs}`; 15 | await mongoose.connect(connectionString, { 16 | serverSelectionTimeoutMS: 5000 17 | }); 18 | logger.info('Connected to database'); 19 | } catch (e) { 20 | logger.error(e); 21 | } 22 | } 23 | 24 | module.exports = connectToDatabase; -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | function errorHandler(fn) { 4 | return async function(req, res, next) { 5 | try { 6 | let nextCalled = false; 7 | const result = await fn(req, res, (params) => { 8 | nextCalled = true; 9 | next(params); 10 | }); 11 | if (!res.headersSent && !nextCalled) { 12 | res.json(result); 13 | } 14 | } catch (e) { 15 | next(e); 16 | } 17 | } 18 | } 19 | 20 | function withTransaction(fn) { 21 | return async function(req, res, next) { 22 | let result; 23 | await mongoose.connection.transaction(async (session) => { 24 | result = await fn(req, res, session); 25 | return result; 26 | }); 27 | 28 | return result; 29 | } 30 | } 31 | 32 | module.exports = { 33 | errorHandler, 34 | withTransaction 35 | }; -------------------------------------------------------------------------------- /src/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const models = require("../models"); 3 | const argon2 = require("argon2"); 4 | const {errorHandler, withTransaction} = require("../util"); 5 | const {HttpError} = require("../error"); 6 | 7 | const signup = errorHandler(withTransaction(async (req, res, session) => { 8 | const userDoc = models.User({ 9 | username: req.body.username, 10 | password: await argon2.hash(req.body.password) 11 | }); 12 | const refreshTokenDoc = models.RefreshToken({ 13 | owner: userDoc.id 14 | }); 15 | 16 | await userDoc.save({session}); 17 | await refreshTokenDoc.save({session}); 18 | 19 | const refreshToken = createRefreshToken(userDoc.id, refreshTokenDoc.id); 20 | const accessToken = createAccessToken(userDoc.id); 21 | 22 | return { 23 | id: userDoc.id, 24 | accessToken, 25 | refreshToken 26 | }; 27 | })); 28 | 29 | const login = errorHandler(withTransaction(async (req, res, session) => { 30 | const userDoc = await models.User 31 | .findOne({username: req.body.username}) 32 | .select('+password') 33 | .exec(); 34 | if (!userDoc) { 35 | throw new HttpError(401, 'Wrong username or password'); 36 | } 37 | await verifyPassword(userDoc.password, req.body.password); 38 | 39 | const refreshTokenDoc = models.RefreshToken({ 40 | owner: userDoc.id 41 | }); 42 | 43 | await refreshTokenDoc.save({session}); 44 | 45 | const refreshToken = createRefreshToken(userDoc.id, refreshTokenDoc.id); 46 | const accessToken = createAccessToken(userDoc.id); 47 | 48 | return { 49 | id: userDoc.id, 50 | accessToken, 51 | refreshToken 52 | }; 53 | })); 54 | 55 | const newRefreshToken = errorHandler(withTransaction(async (req, res, session) => { 56 | const currentRefreshToken = await validateRefreshToken(req.body.refreshToken); 57 | const refreshTokenDoc = models.RefreshToken({ 58 | owner: currentRefreshToken.userId 59 | }); 60 | 61 | await refreshTokenDoc.save({session}); 62 | await models.RefreshToken.deleteOne({_id: currentRefreshToken.tokenId}, {session}); 63 | 64 | const refreshToken = createRefreshToken(currentRefreshToken.userId, refreshTokenDoc.id); 65 | const accessToken = createAccessToken(currentRefreshToken.userId); 66 | 67 | return { 68 | id: currentRefreshToken.userId, 69 | accessToken, 70 | refreshToken 71 | }; 72 | })); 73 | 74 | const newAccessToken = errorHandler(async (req, res) => { 75 | const refreshToken = await validateRefreshToken(req.body.refreshToken); 76 | const accessToken = createAccessToken(refreshToken.userId); 77 | 78 | return { 79 | id: refreshToken.userId, 80 | accessToken, 81 | refreshToken: req.body.refreshToken 82 | }; 83 | }); 84 | 85 | const logout = errorHandler(withTransaction(async (req, res, session) => { 86 | const refreshToken = await validateRefreshToken(req.body.refreshToken); 87 | await models.RefreshToken.deleteOne({_id: refreshToken.tokenId}, {session}); 88 | return {success: true}; 89 | })); 90 | 91 | const logoutAll = errorHandler(withTransaction(async (req, res, session) => { 92 | const refreshToken = await validateRefreshToken(req.body.refreshToken); 93 | await models.RefreshToken.deleteMany({owner: refreshToken.userId}, {session}); 94 | return {success: true}; 95 | })); 96 | 97 | function createAccessToken(userId) { 98 | return jwt.sign({ 99 | userId: userId 100 | }, process.env.ACCESS_TOKEN_SECRET, { 101 | expiresIn: '10m' 102 | }); 103 | } 104 | 105 | function createRefreshToken(userId, refreshTokenId) { 106 | return jwt.sign({ 107 | userId: userId, 108 | tokenId: refreshTokenId 109 | }, process.env.REFRESH_TOKEN_SECRET, { 110 | expiresIn: '30d' 111 | }); 112 | } 113 | 114 | const verifyPassword = async (hashedPassword, rawPassword) => { 115 | if (await argon2.verify(hashedPassword, rawPassword)) { 116 | // password matches 117 | } else { 118 | throw new HttpError(401, 'Wrong username or password'); 119 | } 120 | }; 121 | 122 | const validateRefreshToken = async (token) => { 123 | const decodeToken = () => { 124 | try { 125 | return jwt.verify(token, process.env.REFRESH_TOKEN_SECRET); 126 | } catch(err) { 127 | // err 128 | throw new HttpError(401, 'Unauthorised'); 129 | } 130 | } 131 | 132 | const decodedToken = decodeToken(); 133 | const tokenExists = await models.RefreshToken.exists({_id: decodedToken.tokenId, owner: decodedToken.userId}); 134 | if (tokenExists) { 135 | return decodedToken; 136 | } else { 137 | throw new HttpError(401, 'Unauthorised'); 138 | } 139 | }; 140 | 141 | module.exports = { 142 | signup, 143 | login, 144 | newRefreshToken, 145 | newAccessToken, 146 | logout, 147 | logoutAll 148 | }; --------------------------------------------------------------------------------