From 05c051ae3abd55dc46e5ef6842481f3cef3202a9 Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Tue, 15 Oct 2024 12:20:55 +0530 Subject: [PATCH 1/6] Generate refresh token & add it to response --- Server/controllers/authController.js | 24 ++++++++++++++++++++++-- Server/service/settingsService.js | 2 ++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 136a05a7a..bde728e32 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -33,6 +33,24 @@ const issueToken = (payload, appSettings) => { } }; +/** + * Creates and returns a JWT refresh token with an arbitrary payload + * @function + * @param {Object} payload - Here we have passed MongoDB assigned _id. + * @param {Object} appSettings + * @returns {String} + * @throws {Error} + */ +const issueRefreshToken = (payload, appSettings) => { + try { + const tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; + return jwt.sign({ id: payload }, appSettings.refreshTokenSecret, { expiresIn: tokenTTL }); + } catch (error) { + throw handleError(error, SERVICE_NAME, "issueRefreshToken"); + } +}; + + /** * Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required. * @async @@ -81,6 +99,7 @@ const registerUser = async (req, res, next) => { const appSettings = await req.settingsService.getSettings(); const token = issueToken(userForToken, appSettings); + const refreshToken = issueRefreshToken(userForToken._id, appSettings); req.emailService .buildAndSendEmail( @@ -99,7 +118,7 @@ const registerUser = async (req, res, next) => { return res.status(200).json({ success: true, msg: successMessages.AUTH_CREATE_USER, - data: { user: newUser, token: token }, + data: { user: newUser, token: token, refreshToken: refreshToken }, }); } catch (error) { console.log("ERROR", error); @@ -147,13 +166,14 @@ const loginUser = async (req, res, next) => { // Happy path, return token const appSettings = await req.settingsService.getSettings(); const token = issueToken(userWithoutPassword, appSettings); + const refreshToken = issueRefreshToken(userWithoutPassword._id, appSettings); // reset avatar image userWithoutPassword.avatarImage = user.avatarImage; return res.status(200).json({ success: true, msg: successMessages.AUTH_LOGIN_USER, - data: { user: userWithoutPassword, token: token }, + data: { user: userWithoutPassword, token: token, refreshToken: refreshToken }, }); } catch (error) { next(handleError(error, SERVICE_NAME, "loginController")); diff --git a/Server/service/settingsService.js b/Server/service/settingsService.js index 24e890d64..764c24db3 100644 --- a/Server/service/settingsService.js +++ b/Server/service/settingsService.js @@ -6,11 +6,13 @@ const envConfig = { apiBaseUrl: undefined, clientHost: process.env.CLIENT_HOST, jwtSecret: process.env.JWT_SECRET, + refreshTokenSecret: process.env.REFRESH_TOKEN_SECRET, dbType: process.env.DB_TYPE, dbConnectionString: process.env.DB_CONNECTION_STRING, redisHost: process.env.REDIS_HOST, redisPort: process.env.REDIS_PORT, jwtTTL: process.env.TOKEN_TTL, + refreshTokenTTL: process.env.REFRESH_TOKEN_TTL, pagespeedApiKey: process.env.PAGESPEED_API_KEY, systemEmailHost: process.env.SYSTEM_EMAIL_HOST, systemEmailPort: process.env.SYSTEM_EMAIL_PORT, From 581b6217a992fcf2135ae7f4395f3672c487fb7b Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Tue, 15 Oct 2024 14:11:47 +0530 Subject: [PATCH 2/6] Update appSetting for refresh token & Update documentation --- Server/db/models/AppSettings.js | 12 +++++++++++- guides/users-guide/quickstart.md | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Server/db/models/AppSettings.js b/Server/db/models/AppSettings.js index f95d58f4b..bbd863af8 100644 --- a/Server/db/models/AppSettings.js +++ b/Server/db/models/AppSettings.js @@ -22,6 +22,11 @@ const AppSettingsSchema = mongoose.Schema( required: true, default: "my_secret", }, + refreshTokenSecret: { + type: String, + required: true, + default: "my_refresh_secret", + }, dbType: { type: String, required: true, @@ -44,7 +49,12 @@ const AppSettingsSchema = mongoose.Schema( jwtTTL: { type: String, required: true, - default: "99d", + default: "2h", + }, + refreshTokenTTL: { + type: String, + required: true, + default: "7d", }, pagespeedApiKey: { type: String, diff --git a/guides/users-guide/quickstart.md b/guides/users-guide/quickstart.md index 225f4fdb5..2c469bdaa 100644 --- a/guides/users-guide/quickstart.md +++ b/guides/users-guide/quickstart.md @@ -79,7 +79,7 @@ Make sure you change the directory to the specified directories, as paths in com Configure the server with the following environmental variables: -
ENV Variable NameRequired/OptionalTypeDescriptionAccepted Values
CLIENT_HOSTRequiredstringFrontend Host
JWT_SECRETRequiredstringJWT secret
DB_TYPEOptionalstringSpecify DB to useMongoDB | FakeDB
DB_CONNECTION_STRINGRequiredstringSpecifies URL for MongoDB Database
PORTOptionalintegerSpecifies Port for Server
LOGIN_PAGE_URLRequiredstringLogin url to be used in emailing service
REDIS_HOSTRequiredstringHost address for Redis database
REDIS_PORTRequiredintegerPort for Redis database
TOKEN_TTLOptionalstringTime for token to liveIn vercel/ms format https://github.com/vercel/ms
PAGESPEED_API_KEYOptionalstringAPI Key for PageSpeed requests
SYSTEM_EMAIL_HOSTRequiredstringHost to send System Emails From
SYSTEM_EMAIL_PORTRequirednumberPort for System Email Host
SYSTEM_EMAIL_ADDRESSRequiredstringSystem Email Address
SYSTEM_EMAIL_PASSWORDRequiredstringSystem Email Password
+
ENV Variable NameRequired/OptionalTypeDescriptionAccepted Values
CLIENT_HOSTRequiredstringFrontend Host
JWT_SECRETRequiredstringJWT secret
REFRESH_TOKEN_SECRETRequiredstringRefresh JWT secret
DB_TYPEOptionalstringSpecify DB to useMongoDB | FakeDB
DB_CONNECTION_STRINGRequiredstringSpecifies URL for MongoDB Database
PORTOptionalintegerSpecifies Port for Server
LOGIN_PAGE_URLRequiredstringLogin url to be used in emailing service
REDIS_HOSTRequiredstringHost address for Redis database
REDIS_PORTRequiredintegerPort for Redis database
TOKEN_TTLOptionalstringTime for token to liveIn vercel/ms format https://github.com/vercel/ms
REFRESH_TOKEN_TTLOptionalstringTime for refresh token to live
PAGESPEED_API_KEYOptionalstringAPI Key for PageSpeed requests
SYSTEM_EMAIL_HOSTRequiredstringHost to send System Emails From
SYSTEM_EMAIL_PORTRequirednumberPort for System Email Host
SYSTEM_EMAIL_ADDRESSRequiredstringSystem Email Address
SYSTEM_EMAIL_PASSWORDRequiredstringSystem Email Password
--- From cd0160f421d5fb669a8fad97a501cac3298707da Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Tue, 15 Oct 2024 15:55:29 +0530 Subject: [PATCH 3/6] Pass whole user object instead of just _id --- Server/controllers/authController.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index bde728e32..7eefc6722 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -44,7 +44,7 @@ const issueToken = (payload, appSettings) => { const issueRefreshToken = (payload, appSettings) => { try { const tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; - return jwt.sign({ id: payload }, appSettings.refreshTokenSecret, { expiresIn: tokenTTL }); + return jwt.sign(payload, appSettings.refreshTokenSecret, { expiresIn: tokenTTL }); } catch (error) { throw handleError(error, SERVICE_NAME, "issueRefreshToken"); } @@ -99,7 +99,7 @@ const registerUser = async (req, res, next) => { const appSettings = await req.settingsService.getSettings(); const token = issueToken(userForToken, appSettings); - const refreshToken = issueRefreshToken(userForToken._id, appSettings); + const refreshToken = issueRefreshToken(userForToken, appSettings); req.emailService .buildAndSendEmail( @@ -166,7 +166,7 @@ const loginUser = async (req, res, next) => { // Happy path, return token const appSettings = await req.settingsService.getSettings(); const token = issueToken(userWithoutPassword, appSettings); - const refreshToken = issueRefreshToken(userWithoutPassword._id, appSettings); + const refreshToken = issueRefreshToken(userWithoutPassword, appSettings); // reset avatar image userWithoutPassword.avatarImage = user.avatarImage; From 29ce8bb116c1a19a07db128bad3bfb808a14af8f Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Wed, 16 Oct 2024 12:45:31 +0530 Subject: [PATCH 4/6] Refactor issueToken function for issueing the refresh token --- Server/controllers/authController.js | 48 ++++++++++++---------------- Server/utils/utils.js | 6 ++++ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index 7eefc6722..cb816fde6 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -12,7 +12,7 @@ require("dotenv").config(); const { errorMessages, successMessages } = require("../utils/messages"); const jwt = require("jsonwebtoken"); const SERVICE_NAME = "AuthController"; -const { getTokenFromHeaders } = require("../utils/utils"); +const { getTokenFromHeaders, tokenType } = require("../utils/utils"); const crypto = require("crypto"); const { handleValidationError, handleError } = require("./controllerUtils"); @@ -20,37 +20,31 @@ const { handleValidationError, handleError } = require("./controllerUtils"); * Creates and returns JWT token with an arbitrary payload * @function * @param {Object} payload + * @param {tokenType} typeOfToken - Whether to generate refresh token with long TTL or access token with short TTL. * @param {Object} appSettings * @returns {String} * @throws {Error} */ -const issueToken = (payload, appSettings) => { +const issueToken = (payload, typeOfToken, appSettings) => { try { - const tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h"; - return jwt.sign(payload, appSettings.jwtSecret, { expiresIn: tokenTTL }); + let tokenSecret, tokenTTL, payloadData = {}; + + if(typeOfToken === tokenType.REFRESH_TOKEN) { + tokenSecret = appSettings.refreshTokenSecret; + tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; + } + else { + tokenSecret = appSettings.jwtSecret; + tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h"; + payloadData = payload; + } + + return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); } catch (error) { throw handleError(error, SERVICE_NAME, "issueToken"); } }; -/** - * Creates and returns a JWT refresh token with an arbitrary payload - * @function - * @param {Object} payload - Here we have passed MongoDB assigned _id. - * @param {Object} appSettings - * @returns {String} - * @throws {Error} - */ -const issueRefreshToken = (payload, appSettings) => { - try { - const tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; - return jwt.sign(payload, appSettings.refreshTokenSecret, { expiresIn: tokenTTL }); - } catch (error) { - throw handleError(error, SERVICE_NAME, "issueRefreshToken"); - } -}; - - /** * Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required. * @async @@ -98,8 +92,8 @@ const registerUser = async (req, res, next) => { const appSettings = await req.settingsService.getSettings(); - const token = issueToken(userForToken, appSettings); - const refreshToken = issueRefreshToken(userForToken, appSettings); + const token = issueToken(userForToken, tokenType.ACCESS_TOKEN, appSettings); + const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, appSettings); req.emailService .buildAndSendEmail( @@ -165,8 +159,8 @@ const loginUser = async (req, res, next) => { // Happy path, return token const appSettings = await req.settingsService.getSettings(); - const token = issueToken(userWithoutPassword, appSettings); - const refreshToken = issueRefreshToken(userWithoutPassword, appSettings); + const token = issueToken(userWithoutPassword, tokenType.ACCESS_TOKEN, appSettings); + const refreshToken = issueToken({}, tokenType.REFRESH_TOKEN, appSettings); // reset avatar image userWithoutPassword.avatarImage = user.avatarImage; @@ -380,7 +374,7 @@ const resetPassword = async (req, res, next) => { const user = await req.db.resetPassword(req, res); const appSettings = await req.settingsService.getSettings(); - const token = issueToken(user._doc, appSettings); + const token = issueToken(user._doc, tokenType.ACCESS_TOKEN, appSettings); res.status(200).json({ success: true, msg: successMessages.AUTH_RESET_PASSWORD, diff --git a/Server/utils/utils.js b/Server/utils/utils.js index 4331a4610..f839eb464 100644 --- a/Server/utils/utils.js +++ b/Server/utils/utils.js @@ -27,7 +27,13 @@ const getTokenFromHeaders = (headers) => { return parts[1]; }; +const tokenType = Object.freeze({ + ACCESS_TOKEN: "Access token", + REFRESH_TOKEN: "Refresh token" +}); + module.exports = { ParseBoolean, getTokenFromHeaders, + tokenType, }; From 7a3d2619f60b60042d07f83c54df0b553188f04c Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Wed, 16 Oct 2024 12:46:44 +0530 Subject: [PATCH 5/6] add testcases for refresh token --- .../tests/controllers/authController.test.js | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/Server/tests/controllers/authController.test.js b/Server/tests/controllers/authController.test.js index 842959415..c33d13fcd 100644 --- a/Server/tests/controllers/authController.test.js +++ b/Server/tests/controllers/authController.test.js @@ -14,29 +14,61 @@ const jwt = require("jsonwebtoken"); const { errorMessages, successMessages } = require("../../utils/messages"); const sinon = require("sinon"); const logger = require("../../utils/logger"); +const { tokenType } = require("../../utils/utils"); describe("Auth Controller - issueToken", () => { + let stub; + + afterEach(() => { + sinon.restore(); // Restore stubs after each test + }); + it("should reject with an error if jwt.sign fails", () => { const error = new Error("jwt.sign error"); stub = sinon.stub(jwt, "sign").throws(error); const payload = { id: "123" }; const appSettings = { jwtSecret: "my_secret" }; - expect(() => issueToken(payload, appSettings)).to.throw(error); - stub.restore(); + expect(() => issueToken(payload, tokenType.ACCESS_TOKEN, appSettings)).to.throw(error); }); - it("should return a token if jwt.sign is successful and appSettings.jtwTTL is not defined", () => { + it("should return a token if jwt.sign is successful and appSettings.jwtTTL is not defined", () => { const payload = { id: "123" }; const appSettings = { jwtSecret: "my_secret" }; - const token = issueToken(payload, appSettings); - expect(token).to.be.a("string"); + const expectedToken = "mockToken"; + + stub = sinon.stub(jwt, "sign").returns(expectedToken); + const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings); + expect(token).to.equal(expectedToken); }); it("should return a token if jwt.sign is successful and appSettings.jwtTTL is defined", () => { const payload = { id: "123" }; const appSettings = { jwtSecret: "my_secret", jwtTTL: "1s" }; - const token = issueToken(payload, appSettings); - expect(token).to.be.a("string"); + const expectedToken = "mockToken"; + + stub = sinon.stub(jwt, "sign").returns(expectedToken); + const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings); + expect(token).to.equal(expectedToken); + }); + + it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is not defined", () => { + const payload = {}; + const appSettings = { refreshTokenSecret: "my_refresh_secret" }; + const expectedToken = "mockRefreshToken"; + + stub = sinon.stub(jwt, "sign").returns(expectedToken); + const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings); + expect(token).to.equal(expectedToken); + }); + + it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is defined", () => { + const payload = {}; + const appSettings = { refreshTokenSecret: "my_refresh_secret", refreshTokenTTL: "7d" }; + const expectedToken = "mockRefreshToken"; + + stub = sinon.stub(jwt, "sign").returns(expectedToken); + const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings); + expect(token).to.equal(expectedToken); }); }); @@ -61,6 +93,7 @@ describe("Auth Controller - registerUser", () => { settingsService: { getSettings: sinon.stub().resolves({ jwtSecret: "my_secret", + refreshTokenSecret: "my_refresh_secret", }), }, emailService: { @@ -140,7 +173,6 @@ describe("Auth Controller - registerUser", () => { req.db.checkSuperadmin.resolves(false); req.db.updateAppSettings.resolves(); req.db.insertUser.returns({ _id: "123" }); - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); req.emailService.buildAndSendEmail.rejects(new Error("emailService error")); await registerUser(req, res, next); expect(logger.error.calledOnce).to.be.true; @@ -151,7 +183,6 @@ describe("Auth Controller - registerUser", () => { req.db.checkSuperadmin.resolves(false); req.db.updateAppSettings.resolves(); req.db.insertUser.returns(user); - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); req.emailService.buildAndSendEmail.resolves("message-id"); await registerUser(req, res, next); expect(res.status.calledWith(200)).to.be.true; @@ -159,7 +190,7 @@ describe("Auth Controller - registerUser", () => { res.json.calledWith({ success: true, msg: successMessages.AUTH_CREATE_USER, - data: { user, token: sinon.match.string }, + data: { user, token: sinon.match.string, refreshToken: sinon.match.string }, }) ).to.be.true; expect(next.notCalled).to.be.true; @@ -176,6 +207,7 @@ describe("Auth Controller - loginUser", () => { settingsService: { getSettings: sinon.stub().resolves({ jwtSecret: "my_secret", + refreshTokenSecret: "my_refresh_token", }), }, }; @@ -220,6 +252,7 @@ describe("Auth Controller - loginUser", () => { avatarImage: undefined, }, token: sinon.match.string, + refreshToken: sinon.match.string, }, }) ).to.be.true; From 111f7d5a676621b08eeb32057f4f89f8e56f2901 Mon Sep 17 00:00:00 2001 From: Rushi Gandhi Date: Wed, 16 Oct 2024 14:13:48 +0530 Subject: [PATCH 6/6] remove if/else by using ternary operator --- Server/controllers/authController.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Server/controllers/authController.js b/Server/controllers/authController.js index cb816fde6..0f386bc4b 100644 --- a/Server/controllers/authController.js +++ b/Server/controllers/authController.js @@ -27,17 +27,9 @@ const { handleValidationError, handleError } = require("./controllerUtils"); */ const issueToken = (payload, typeOfToken, appSettings) => { try { - let tokenSecret, tokenTTL, payloadData = {}; - - if(typeOfToken === tokenType.REFRESH_TOKEN) { - tokenSecret = appSettings.refreshTokenSecret; - tokenTTL = appSettings.refreshTokenTTL ? appSettings.refreshTokenTTL : "7d"; - } - else { - tokenSecret = appSettings.jwtSecret; - tokenTTL = appSettings.jwtTTL ? appSettings.jwtTTL : "2h"; - payloadData = payload; - } + const tokenTTL = (typeOfToken === tokenType.REFRESH_TOKEN) ? (appSettings?.refreshTokenTTL?? "7d") : (appSettings?.jwtTTL?? "2h"); + const tokenSecret = (typeOfToken === tokenType.REFRESH_TOKEN) ? appSettings?.refreshTokenSecret : appSettings?.jwtSecret; + const payloadData = (typeOfToken === tokenType.REFRESH_TOKEN) ? {} : payload; return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); } catch (error) {