├── .gitignore ├── README.md ├── app ├── config │ ├── auth.config.js │ └── db.config.js ├── controllers │ ├── auth.controller.js │ └── user.controller.js ├── middleware │ ├── authJwt.js │ ├── index.js │ └── verifySignUp.js ├── models │ ├── index.js │ ├── refreshToken.model.js │ ├── role.model.js │ └── user.model.js └── routes │ ├── auth.routes.js │ └── user.routes.js ├── jwt-refresh-token-node-js-example-flow.png ├── jwt-token-authentication-node-js-example-flow.png ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js JWT Refresh Token with MySQL example 2 | JWT Refresh Token Implementation with Node.js Express and MySQL. You can know how to expire the JWT, then renew the Access Token with Refresh Token. 3 | 4 | For instruction, please visit: 5 | > [Node.js JWT Refresh Token example](https://bezkoder.com/jwt-refresh-token-node-js/) 6 | 7 | The code in this post bases on previous article that you need to read first: 8 | > [Node.js JWT Authentication & Authorization example](https://bezkoder.com/node-js-jwt-authentication-mysql/) 9 | 10 | ## User Registration, User Login and Authorization process. 11 | 12 | The diagram shows flow of how we implement User Registration, User Login and Authorization process. 13 | 14 | ![jwt-token-authentication-node-js-example-flow](jwt-token-authentication-node-js-example-flow.png) 15 | 16 | And this is for Refresh Token: 17 | 18 | ![jwt-refresh-token-node-js-example-flow](jwt-refresh-token-node-js-example-flow.png) 19 | 20 | ## More Practice: 21 | > [Build Node.js Rest APIs with Express, Sequelize & MySQL](https://bezkoder.com/node-js-express-sequelize-mysql/) 22 | 23 | > [Server side Pagination in Node.js with Sequelize and MySQL](https://bezkoder.com/node-js-sequelize-pagination-mysql/) 24 | 25 | > [Node.js Express File Upload Rest API example](https://bezkoder.com/node-js-express-file-upload/) 26 | 27 | > [Node.js Express File Upload with Google Cloud Storage example](https://bezkoder.com/google-cloud-storage-nodejs-upload-file/) 28 | 29 | Associations: 30 | > [Sequelize Associations: One-to-Many Relationship example](https://bezkoder.com/sequelize-associate-one-to-many/) 31 | 32 | > [Sequelize Associations: Many-to-Many Relationship example](https://bezkoder.com/sequelize-associate-many-to-many/) 33 | 34 | Deployment: 35 | > [Deploying/Hosting Node.js app on Heroku with MySQL database](https://bezkoder.com/deploy-node-js-app-heroku-cleardb-mysql/) 36 | 37 | Integration on same Server/Port: 38 | > [Integrate Angular 8 with Node.js Express](https://bezkoder.com/integrate-angular-8-node-js/) 39 | 40 | > [Integrate Angular 10 with Node.js Express](https://bezkoder.com/integrate-angular-10-node-js/) 41 | 42 | > [Integrate React with Node.js Express](https://bezkoder.com/integrate-react-express-same-server-port/) 43 | 44 | ## Project setup 45 | ``` 46 | npm install 47 | ``` 48 | 49 | ### Run 50 | ``` 51 | node server.js 52 | ``` 53 | -------------------------------------------------------------------------------- /app/config/auth.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | secret: "bezkoder-secret-key", 3 | // jwtExpiration: 3600, // 1 hour 4 | // jwtRefreshExpiration: 86400, // 24 hours 5 | 6 | /* for test */ 7 | jwtExpiration: 60, // 1 minute 8 | jwtRefreshExpiration: 120, // 2 minutes 9 | }; 10 | -------------------------------------------------------------------------------- /app/config/db.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | HOST: "localhost", 3 | USER: "root", 4 | PASSWORD: "123456", 5 | DB: "testdb", 6 | dialect: "mysql", 7 | pool: { 8 | max: 5, 9 | min: 0, 10 | acquire: 30000, 11 | idle: 10000 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /app/controllers/auth.controller.js: -------------------------------------------------------------------------------- 1 | const db = require("../models"); 2 | const config = require("../config/auth.config"); 3 | const { user: User, role: Role, refreshToken: RefreshToken } = db; 4 | 5 | const Op = db.Sequelize.Op; 6 | 7 | const jwt = require("jsonwebtoken"); 8 | const bcrypt = require("bcryptjs"); 9 | 10 | exports.signup = (req, res) => { 11 | // Save User to Database 12 | User.create({ 13 | username: req.body.username, 14 | email: req.body.email, 15 | password: bcrypt.hashSync(req.body.password, 8) 16 | }) 17 | .then(user => { 18 | if (req.body.roles) { 19 | Role.findAll({ 20 | where: { 21 | name: { 22 | [Op.or]: req.body.roles 23 | } 24 | } 25 | }).then(roles => { 26 | user.setRoles(roles).then(() => { 27 | res.send({ message: "User registered successfully!" }); 28 | }); 29 | }); 30 | } else { 31 | // user role = 1 32 | user.setRoles([1]).then(() => { 33 | res.send({ message: "User registered successfully!" }); 34 | }); 35 | } 36 | }) 37 | .catch(err => { 38 | res.status(500).send({ message: err.message }); 39 | }); 40 | }; 41 | 42 | exports.signin = (req, res) => { 43 | User.findOne({ 44 | where: { 45 | username: req.body.username 46 | } 47 | }) 48 | .then(async (user) => { 49 | if (!user) { 50 | return res.status(404).send({ message: "User Not found." }); 51 | } 52 | 53 | const passwordIsValid = bcrypt.compareSync( 54 | req.body.password, 55 | user.password 56 | ); 57 | 58 | if (!passwordIsValid) { 59 | return res.status(401).send({ 60 | accessToken: null, 61 | message: "Invalid Password!" 62 | }); 63 | } 64 | 65 | const token = jwt.sign({ id: user.id }, config.secret, { 66 | expiresIn: config.jwtExpiration 67 | }); 68 | 69 | let refreshToken = await RefreshToken.createToken(user); 70 | 71 | let authorities = []; 72 | user.getRoles().then(roles => { 73 | for (let i = 0; i < roles.length; i++) { 74 | authorities.push("ROLE_" + roles[i].name.toUpperCase()); 75 | } 76 | 77 | res.status(200).send({ 78 | id: user.id, 79 | username: user.username, 80 | email: user.email, 81 | roles: authorities, 82 | accessToken: token, 83 | refreshToken: refreshToken, 84 | }); 85 | }); 86 | }) 87 | .catch(err => { 88 | res.status(500).send({ message: err.message }); 89 | }); 90 | }; 91 | 92 | exports.refreshToken = async (req, res) => { 93 | const { refreshToken: requestToken } = req.body; 94 | 95 | if (requestToken == null) { 96 | return res.status(403).json({ message: "Refresh Token is required!" }); 97 | } 98 | 99 | try { 100 | let refreshToken = await RefreshToken.findOne({ where: { token: requestToken } }); 101 | 102 | console.log(refreshToken) 103 | 104 | if (!refreshToken) { 105 | res.status(403).json({ message: "Refresh token is not in database!" }); 106 | return; 107 | } 108 | 109 | if (RefreshToken.verifyExpiration(refreshToken)) { 110 | RefreshToken.destroy({ where: { id: refreshToken.id } }); 111 | 112 | res.status(403).json({ 113 | message: "Refresh token was expired. Please make a new signin request", 114 | }); 115 | return; 116 | } 117 | 118 | const user = await refreshToken.getUser(); 119 | let newAccessToken = jwt.sign({ id: user.id }, config.secret, { 120 | expiresIn: config.jwtExpiration, 121 | }); 122 | 123 | return res.status(200).json({ 124 | accessToken: newAccessToken, 125 | refreshToken: refreshToken.token, 126 | }); 127 | } catch (err) { 128 | return res.status(500).send({ message: err }); 129 | } 130 | }; -------------------------------------------------------------------------------- /app/controllers/user.controller.js: -------------------------------------------------------------------------------- 1 | exports.allAccess = (req, res) => { 2 | res.status(200).send("Public Content."); 3 | }; 4 | 5 | exports.userBoard = (req, res) => { 6 | res.status(200).send("User Content."); 7 | }; 8 | 9 | exports.adminBoard = (req, res) => { 10 | res.status(200).send("Admin Content."); 11 | }; 12 | 13 | exports.moderatorBoard = (req, res) => { 14 | res.status(200).send("Moderator Content."); 15 | }; 16 | -------------------------------------------------------------------------------- /app/middleware/authJwt.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const config = require("../config/auth.config.js"); 3 | const db = require("../models"); 4 | const User = db.user; 5 | 6 | const { TokenExpiredError } = jwt; 7 | 8 | const catchError = (err, res) => { 9 | if (err instanceof TokenExpiredError) { 10 | return res.status(401).send({ message: "Unauthorized! Access Token was expired!" }); 11 | } 12 | 13 | return res.sendStatus(401).send({ message: "Unauthorized!" }); 14 | } 15 | 16 | const verifyToken = (req, res, next) => { 17 | let token = req.headers["x-access-token"]; 18 | 19 | if (!token) { 20 | return res.status(403).send({ message: "No token provided!" }); 21 | } 22 | 23 | jwt.verify(token, config.secret, (err, decoded) => { 24 | if (err) { 25 | return catchError(err, res); 26 | } 27 | req.userId = decoded.id; 28 | next(); 29 | }); 30 | }; 31 | 32 | const isAdmin = (req, res, next) => { 33 | User.findByPk(req.userId).then(user => { 34 | user.getRoles().then(roles => { 35 | for (let i = 0; i < roles.length; i++) { 36 | if (roles[i].name === "admin") { 37 | next(); 38 | return; 39 | } 40 | } 41 | 42 | res.status(403).send({ 43 | message: "Require Admin Role!" 44 | }); 45 | return; 46 | }); 47 | }); 48 | }; 49 | 50 | const isModerator = (req, res, next) => { 51 | User.findByPk(req.userId).then(user => { 52 | user.getRoles().then(roles => { 53 | for (let i = 0; i < roles.length; i++) { 54 | if (roles[i].name === "moderator") { 55 | next(); 56 | return; 57 | } 58 | } 59 | 60 | res.status(403).send({ 61 | message: "Require Moderator Role!" 62 | }); 63 | }); 64 | }); 65 | }; 66 | 67 | const isModeratorOrAdmin = (req, res, next) => { 68 | User.findByPk(req.userId).then(user => { 69 | user.getRoles().then(roles => { 70 | for (let i = 0; i < roles.length; i++) { 71 | if (roles[i].name === "moderator") { 72 | next(); 73 | return; 74 | } 75 | 76 | if (roles[i].name === "admin") { 77 | next(); 78 | return; 79 | } 80 | } 81 | 82 | res.status(403).send({ 83 | message: "Require Moderator or Admin Role!" 84 | }); 85 | }); 86 | }); 87 | }; 88 | 89 | const authJwt = { 90 | verifyToken: verifyToken, 91 | isAdmin: isAdmin, 92 | isModerator: isModerator, 93 | isModeratorOrAdmin: isModeratorOrAdmin 94 | }; 95 | module.exports = authJwt; 96 | -------------------------------------------------------------------------------- /app/middleware/index.js: -------------------------------------------------------------------------------- 1 | const authJwt = require("./authJwt"); 2 | const verifySignUp = require("./verifySignUp"); 3 | 4 | module.exports = { 5 | authJwt, 6 | verifySignUp 7 | }; 8 | -------------------------------------------------------------------------------- /app/middleware/verifySignUp.js: -------------------------------------------------------------------------------- 1 | const db = require("../models"); 2 | const ROLES = db.ROLES; 3 | const User = db.user; 4 | 5 | checkDuplicateUsernameOrEmail = (req, res, next) => { 6 | // Username 7 | User.findOne({ 8 | where: { 9 | username: req.body.username 10 | } 11 | }).then(user => { 12 | if (user) { 13 | res.status(400).send({ 14 | message: "Failed! Username is already in use!" 15 | }); 16 | return; 17 | } 18 | 19 | // Email 20 | User.findOne({ 21 | where: { 22 | email: req.body.email 23 | } 24 | }).then(user => { 25 | if (user) { 26 | res.status(400).send({ 27 | message: "Failed! Email is already in use!" 28 | }); 29 | return; 30 | } 31 | 32 | next(); 33 | }); 34 | }); 35 | }; 36 | 37 | checkRolesExisted = (req, res, next) => { 38 | if (req.body.roles) { 39 | for (let i = 0; i < req.body.roles.length; i++) { 40 | if (!ROLES.includes(req.body.roles[i])) { 41 | res.status(400).send({ 42 | message: "Failed! Role does not exist = " + req.body.roles[i] 43 | }); 44 | return; 45 | } 46 | } 47 | } 48 | 49 | next(); 50 | }; 51 | 52 | const verifySignUp = { 53 | checkDuplicateUsernameOrEmail: checkDuplicateUsernameOrEmail, 54 | checkRolesExisted: checkRolesExisted 55 | }; 56 | 57 | module.exports = verifySignUp; 58 | -------------------------------------------------------------------------------- /app/models/index.js: -------------------------------------------------------------------------------- 1 | const config = require("../config/db.config.js"); 2 | 3 | const Sequelize = require("sequelize"); 4 | const sequelize = new Sequelize( 5 | config.DB, 6 | config.USER, 7 | config.PASSWORD, 8 | { 9 | host: config.HOST, 10 | dialect: config.dialect, 11 | operatorsAliases: false, 12 | 13 | pool: { 14 | max: config.pool.max, 15 | min: config.pool.min, 16 | acquire: config.pool.acquire, 17 | idle: config.pool.idle 18 | } 19 | } 20 | ); 21 | 22 | const db = {}; 23 | 24 | db.Sequelize = Sequelize; 25 | db.sequelize = sequelize; 26 | 27 | db.user = require("../models/user.model.js")(sequelize, Sequelize); 28 | db.role = require("../models/role.model.js")(sequelize, Sequelize); 29 | db.refreshToken = require("../models/refreshToken.model.js")(sequelize, Sequelize); 30 | 31 | db.role.belongsToMany(db.user, { 32 | through: "user_roles", 33 | foreignKey: "roleId", 34 | otherKey: "userId" 35 | }); 36 | 37 | db.user.belongsToMany(db.role, { 38 | through: "user_roles", 39 | foreignKey: "userId", 40 | otherKey: "roleId" 41 | }); 42 | 43 | db.refreshToken.belongsTo(db.user, { 44 | foreignKey: 'userId', targetKey: 'id' 45 | }); 46 | db.user.hasOne(db.refreshToken, { 47 | foreignKey: 'userId', targetKey: 'id' 48 | }); 49 | 50 | db.ROLES = ["user", "admin", "moderator"]; 51 | 52 | module.exports = db; 53 | -------------------------------------------------------------------------------- /app/models/refreshToken.model.js: -------------------------------------------------------------------------------- 1 | const config = require("../config/auth.config"); 2 | const { v4: uuidv4 } = require("uuid"); 3 | 4 | module.exports = (sequelize, Sequelize) => { 5 | const RefreshToken = sequelize.define("refreshToken", { 6 | token: { 7 | type: Sequelize.STRING, 8 | }, 9 | expiryDate: { 10 | type: Sequelize.DATE, 11 | }, 12 | }); 13 | 14 | RefreshToken.createToken = async function (user) { 15 | let expiredAt = new Date(); 16 | 17 | expiredAt.setSeconds(expiredAt.getSeconds() + config.jwtRefreshExpiration); 18 | 19 | let _token = uuidv4(); 20 | 21 | let refreshToken = await this.create({ 22 | token: _token, 23 | userId: user.id, 24 | expiryDate: expiredAt.getTime(), 25 | }); 26 | 27 | return refreshToken.token; 28 | }; 29 | 30 | RefreshToken.verifyExpiration = (token) => { 31 | return token.expiryDate.getTime() < new Date().getTime(); 32 | }; 33 | 34 | return RefreshToken; 35 | }; 36 | -------------------------------------------------------------------------------- /app/models/role.model.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, Sequelize) => { 2 | const Role = sequelize.define("roles", { 3 | id: { 4 | type: Sequelize.INTEGER, 5 | primaryKey: true 6 | }, 7 | name: { 8 | type: Sequelize.STRING 9 | } 10 | }); 11 | 12 | return Role; 13 | }; 14 | -------------------------------------------------------------------------------- /app/models/user.model.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, Sequelize) => { 2 | const User = sequelize.define("users", { 3 | username: { 4 | type: Sequelize.STRING 5 | }, 6 | email: { 7 | type: Sequelize.STRING 8 | }, 9 | password: { 10 | type: Sequelize.STRING 11 | } 12 | }); 13 | 14 | return User; 15 | }; 16 | -------------------------------------------------------------------------------- /app/routes/auth.routes.js: -------------------------------------------------------------------------------- 1 | const { verifySignUp } = require("../middleware"); 2 | const controller = require("../controllers/auth.controller"); 3 | 4 | module.exports = function(app) { 5 | app.use(function(req, res, next) { 6 | res.header( 7 | "Access-Control-Allow-Headers", 8 | "x-access-token, Origin, Content-Type, Accept" 9 | ); 10 | next(); 11 | }); 12 | 13 | app.post( 14 | "/api/auth/signup", 15 | [ 16 | verifySignUp.checkDuplicateUsernameOrEmail, 17 | verifySignUp.checkRolesExisted 18 | ], 19 | controller.signup 20 | ); 21 | 22 | app.post("/api/auth/signin", controller.signin); 23 | 24 | app.post("/api/auth/refreshtoken", controller.refreshToken); 25 | }; 26 | -------------------------------------------------------------------------------- /app/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | const { authJwt } = require("../middleware"); 2 | const controller = require("../controllers/user.controller"); 3 | 4 | module.exports = function(app) { 5 | app.use(function(req, res, next) { 6 | res.header( 7 | "Access-Control-Allow-Headers", 8 | "x-access-token, Origin, Content-Type, Accept" 9 | ); 10 | next(); 11 | }); 12 | 13 | app.get("/api/test/all", controller.allAccess); 14 | 15 | app.get( 16 | "/api/test/user", 17 | [authJwt.verifyToken], 18 | controller.userBoard 19 | ); 20 | 21 | app.get( 22 | "/api/test/mod", 23 | [authJwt.verifyToken, authJwt.isModerator], 24 | controller.moderatorBoard 25 | ); 26 | 27 | app.get( 28 | "/api/test/admin", 29 | [authJwt.verifyToken, authJwt.isAdmin], 30 | controller.adminBoard 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /jwt-refresh-token-node-js-example-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/jwt-refresh-token-node-js/dd44008cf717c2f397323027a03453fb458bbf19/jwt-refresh-token-node-js-example-flow.png -------------------------------------------------------------------------------- /jwt-token-authentication-node-js-example-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bezkoder/jwt-refresh-token-node-js/dd44008cf717c2f397323027a03453fb458bbf19/jwt-token-authentication-node-js-example-flow.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-js-jwt-auth", 3 | "version": "1.0.0", 4 | "description": "Node.js Demo for JWT Authentication", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "node.js", 11 | "express", 12 | "jwt", 13 | "refresh token", 14 | "authentication", 15 | "authorization", 16 | "mysql" 17 | ], 18 | "author": "bezkoder", 19 | "license": "ISC", 20 | "dependencies": { 21 | "bcryptjs": "^2.4.3", 22 | "cors": "^2.8.5", 23 | "express": "^4.17.1", 24 | "jsonwebtoken": "^8.5.1", 25 | "mysql2": "^2.1.0", 26 | "sequelize": "^5.21.3", 27 | "uuid": "^8.3.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const cors = require("cors"); 3 | 4 | const app = express(); 5 | 6 | const corsOptions = { 7 | origin: "http://localhost:8081" 8 | }; 9 | 10 | app.use(cors(corsOptions)); 11 | 12 | // parse requests of content-type - application/json 13 | app.use(express.json()); 14 | 15 | // parse requests of content-type - application/x-www-form-urlencoded 16 | app.use(express.urlencoded({ extended: true })); 17 | 18 | // database 19 | const db = require("./app/models"); 20 | const Role = db.role; 21 | 22 | db.sequelize.sync(); 23 | // force: true will drop the table if it already exists 24 | // db.sequelize.sync({force: true}).then(() => { 25 | // console.log('Drop and Resync Database with { force: true }'); 26 | // initial(); 27 | // }); 28 | 29 | // simple route 30 | app.get("/", (req, res) => { 31 | res.json({ message: "Welcome to bezkoder application." }); 32 | }); 33 | 34 | // routes 35 | require('./app/routes/auth.routes')(app); 36 | require('./app/routes/user.routes')(app); 37 | 38 | // set port, listen for requests 39 | const PORT = process.env.PORT || 8080; 40 | app.listen(PORT, () => { 41 | console.log(`Server is running on port ${PORT}.`); 42 | }); 43 | 44 | function initial() { 45 | Role.create({ 46 | id: 1, 47 | name: "user" 48 | }); 49 | 50 | Role.create({ 51 | id: 2, 52 | name: "moderator" 53 | }); 54 | 55 | Role.create({ 56 | id: 3, 57 | name: "admin" 58 | }); 59 | } --------------------------------------------------------------------------------