From 42391cdf7f4040a6c12a1c481284c74b0461d91a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20K=C3=BChl?= Date: Thu, 13 Mar 2025 08:57:27 +0100 Subject: [PATCH] added refresh token functionality --- backend/config/auth.config.js | 11 ++++- backend/controllers/auth.controller.js | 56 +++++++++++++++++++++----- backend/middleware/authJwt.js | 42 ++++++++++--------- backend/models/index.js | 9 ++++- backend/models/refreshToken.model.js | 36 +++++++++++++++++ backend/package-lock.json | 24 ++++++++--- backend/package.json | 3 +- backend/routes/auth.routes.js | 3 +- 8 files changed, 146 insertions(+), 38 deletions(-) create mode 100644 backend/models/refreshToken.model.js diff --git a/backend/config/auth.config.js b/backend/config/auth.config.js index af453d5..2e87ed6 100644 --- a/backend/config/auth.config.js +++ b/backend/config/auth.config.js @@ -1,3 +1,10 @@ module.exports = { - secret: "bullpen-secret-key" -}; \ No newline at end of file + secret: "bullpen-secret-key", + + // jwtExpiration: 3600, // 1 hour + // jwtRefreshExpiration: 86400, // 24 hours + + /* for test */ + jwtExpiration: 60, // 1 minute + jwtRefreshExpiration: 120 // 2 minutes +}; diff --git a/backend/controllers/auth.controller.js b/backend/controllers/auth.controller.js index 625f72e..04fcea1 100644 --- a/backend/controllers/auth.controller.js +++ b/backend/controllers/auth.controller.js @@ -1,7 +1,6 @@ const db = require("../models"); const config = require("../config/auth.config"); -const User = db.user; -const Role = db.role; +const { user: User, role: Role, refreshToken: RefreshToken } = db; const Op = db.Sequelize.Op; @@ -47,16 +46,12 @@ exports.signin = (req, res) => { email: req.body.email } }) - .then(user => { + .then(async (user) => { if (!user) { return res.status(404).send({ message: "User Not found." }); } const passwordIsValid = user.validPassword(req.body.password); - // const passwordIsValid = bcrypt.compareSync( - // req.body.password, - // user.password - // ); if (!passwordIsValid) { return res.status(401).send({ @@ -70,9 +65,11 @@ exports.signin = (req, res) => { { algorithm: 'HS256', allowInsecureKeySizes: true, - expiresIn: 86400, // 24 hours + expiresIn: config.jwtExpiration }); + let refreshToken = await RefreshToken.createToken(user); + const authorities = []; user.getRoles().then(roles => { for (let i = 0; i < roles.length; i++) { @@ -83,7 +80,8 @@ exports.signin = (req, res) => { username: user.username, email: user.email, roles: authorities, - accessToken: token + accessToken: token, + refreshToken: refreshToken }); }); }) @@ -91,3 +89,43 @@ exports.signin = (req, res) => { res.status(500).send({ message: err.message }); }); }; + +exports.refreshToken = async (req, res) => { + const { refreshToken: requestToken } = req.body; + + if (requestToken == null) { + return res.status(403).json({ message: "Refresh Token is required!" }); + } + + try { + let refreshToken = await RefreshToken.findOne({ where: { token: requestToken } }); + + console.log(refreshToken) + + if (!refreshToken) { + res.status(403).json({ message: "Refresh token is not in database!" }); + return; + } + + if (RefreshToken.verifyExpiration(refreshToken)) { + RefreshToken.destroy({ where: { id: refreshToken.id } }); + + res.status(403).json({ + message: "Refresh token was expired. Please make a new signin request", + }); + return; + } + + const user = await refreshToken.getUser(); + let newAccessToken = jwt.sign({ id: user.id }, config.secret, { + expiresIn: config.jwtExpiration, + }); + + return res.status(200).json({ + accessToken: newAccessToken, + refreshToken: refreshToken.token, + }); + } catch (err) { + return res.status(500).send({ message: err }); + } +}; diff --git a/backend/middleware/authJwt.js b/backend/middleware/authJwt.js index 659c406..bcd476c 100644 --- a/backend/middleware/authJwt.js +++ b/backend/middleware/authJwt.js @@ -3,29 +3,33 @@ const config = require("../config/auth.config.js"); const db = require("../models"); const User = db.user; -verifyToken = (req, res, next) => { +const { TokenExpiredError } = jwt; + +const catchError = (err, res) => { + if (err instanceof TokenExpiredError) { + return res.status(401).send({ message: "Unauthorized! Access Token was expired!" }); + } + + return res.sendStatus(401).send({ message: "Unauthorized!" }); +} + +const verifyToken = (req, res, next) => { let token = req.headers["x-access-token"]; if (!token) { - return res.status(403).send({ - message: "No token provided!" - }); + return res.status(403).send({ message: "No token provided!" }); } - jwt.verify(token, - config.secret, - (err, decoded) => { - if (err) { - return res.status(401).send({ - message: "Unauthorized!", - }); - } - req.userId = decoded.id; - next(); - }); + jwt.verify(token, config.secret, (err, decoded) => { + if (err) { + return catchError(err, res); + } + req.userId = decoded.id; + next(); + }); }; -isAdmin = (req, res, next) => { +const isAdmin = (req, res, next) => { User.findByPk(req.userId).then(user => { user.getRoles().then(roles => { for (let i = 0; i < roles.length; i++) { @@ -42,7 +46,7 @@ isAdmin = (req, res, next) => { }); }; -isModerator = (req, res, next) => { +const isModerator = (req, res, next) => { User.findByPk(req.userId).then(user => { user.getRoles().then(roles => { for (let i = 0; i < roles.length; i++) { @@ -59,7 +63,7 @@ isModerator = (req, res, next) => { }); }; -isModeratorOrAdmin = (req, res, next) => { +const isModeratorOrAdmin = (req, res, next) => { User.findByPk(req.userId).then(user => { user.getRoles().then(roles => { for (let i = 0; i < roles.length; i++) { @@ -87,4 +91,4 @@ const authJwt = { isModerator: isModerator, isModeratorOrAdmin: isModeratorOrAdmin }; -module.exports = authJwt; \ No newline at end of file +module.exports = authJwt; diff --git a/backend/models/index.js b/backend/models/index.js index cadb198..deecabf 100644 --- a/backend/models/index.js +++ b/backend/models/index.js @@ -34,6 +34,7 @@ db.role = require("../models/role.model.js")(sequelize); db.pitchType = require("../models/pitchType.model.js")(sequelize); db.pitch = require("../models/pitch.model.js")(sequelize); db.bullpenSession = require("../models/bullpenSession.model.js")(sequelize); +db.refreshToken = require("../models/refreshToken.model.js")(sequelize); db.role.belongsToMany(db.user, { through: "UserRoles" @@ -41,6 +42,12 @@ db.role.belongsToMany(db.user, { db.user.belongsToMany(db.role, { through: "UserRoles" }); +db.refreshToken.belongsTo(db.user, { + foreignKey: 'userId', targetKey: 'id' +}); +db.user.hasOne(db.refreshToken, { + foreignKey: 'userId', targetKey: 'id' +}); db.ROLES = ["user", "admin", "moderator"]; @@ -51,4 +58,4 @@ db.bullpenSession.belongsTo(db.user, { db.pitch.belongsTo(db.bullpenSession); db.pitch.belongsTo(db.pitchType); -module.exports = db; \ No newline at end of file +module.exports = db; diff --git a/backend/models/refreshToken.model.js b/backend/models/refreshToken.model.js new file mode 100644 index 0000000..f020c4b --- /dev/null +++ b/backend/models/refreshToken.model.js @@ -0,0 +1,36 @@ +const { DataTypes } = require('sequelize'); +const config = require("../config/auth.config"); +const { v4: uuidv4 } = require("uuid"); + +module.exports = (sequelize, Sequelize) => { + const RefreshToken = sequelize.define("refreshToken", { + token: { + type: DataTypes.STRING, + }, + expiryDate: { + type: DataTypes.DATE, + }, + }); + + RefreshToken.createToken = async function (user) { + let expiredAt = new Date(); + + expiredAt.setSeconds(expiredAt.getSeconds() + config.jwtRefreshExpiration); + + let _token = uuidv4(); + + let refreshToken = await this.create({ + token: _token, + userId: user.id, + expiryDate: expiredAt.getTime(), + }); + + return refreshToken.token; + }; + + RefreshToken.verifyExpiration = (token) => { + return token.expiryDate.getTime() < new Date().getTime(); + }; + + return RefreshToken; +}; diff --git a/backend/package-lock.json b/backend/package-lock.json index 865de27..ae5bd59 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -18,7 +18,8 @@ "pg": "^8.13.3", "pg-hstore": "^2.3.4", "sequelize": "^6.37.5", - "sqlite3": "^5.1.7" + "sqlite3": "^5.1.7", + "uuid": "^11.1.0" }, "devDependencies": { "sequelize-cli": "^6.6.2" @@ -3258,6 +3259,15 @@ "node": ">= 10.0.0" } }, + "node_modules/sequelize/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -3891,12 +3901,16 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/validator": { diff --git a/backend/package.json b/backend/package.json index 78f601f..dcc7f11 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,7 +23,8 @@ "pg": "^8.13.3", "pg-hstore": "^2.3.4", "sequelize": "^6.37.5", - "sqlite3": "^5.1.7" + "sqlite3": "^5.1.7", + "uuid": "^11.1.0" }, "devDependencies": { "sequelize-cli": "^6.6.2" diff --git a/backend/routes/auth.routes.js b/backend/routes/auth.routes.js index c611e08..770cc0e 100644 --- a/backend/routes/auth.routes.js +++ b/backend/routes/auth.routes.js @@ -20,4 +20,5 @@ module.exports = function(app) { ); app.post("/api/auth/signin", controller.signin); -}; \ No newline at end of file + app.post("/api/auth/refreshtoken", controller.refreshToken); +};