From 4f40d8db413b5cfd827193b7ee95d947641db30d Mon Sep 17 00:00:00 2001 From: Matt Fancher <142915944+FancMa01@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:19:57 -0700 Subject: [PATCH 1/3] Wire Password Reset --- .../src/components/login/ForgotPassword.js | 13 +++- .../src/components/login/ResetPassword.js | 61 +++++++++++++------ Tombolo/server/controllers/authController.js | 40 +++++++++--- 3 files changed, 83 insertions(+), 31 deletions(-) diff --git a/Tombolo/client-reactjs/src/components/login/ForgotPassword.js b/Tombolo/client-reactjs/src/components/login/ForgotPassword.js index 5a29d95f..a3be9c46 100644 --- a/Tombolo/client-reactjs/src/components/login/ForgotPassword.js +++ b/Tombolo/client-reactjs/src/components/login/ForgotPassword.js @@ -1,10 +1,17 @@ import React from 'react'; import { Form, Input, Button, Divider, message } from 'antd'; +import { authHeader } from '../common/AuthHeader'; const ForgotPassword = () => { - const onFinish = (values) => { - console.log('Received values:', values); - success(); + const onFinish = async (values) => { + try { + const url = '/api/auth/handlePasswordResetRequest'; + await fetch(url, { headers: authHeader(), method: 'POST', body: JSON.stringify(values) }); + + success(); + } catch (err) { + message.error(err.message); + } }; const [messageApi, contextHolder] = message.useMessage(); diff --git a/Tombolo/client-reactjs/src/components/login/ResetPassword.js b/Tombolo/client-reactjs/src/components/login/ResetPassword.js index 28a89867..181cbeb9 100644 --- a/Tombolo/client-reactjs/src/components/login/ResetPassword.js +++ b/Tombolo/client-reactjs/src/components/login/ResetPassword.js @@ -2,8 +2,12 @@ import React, { useEffect, useState } from 'react'; import { Form, Input, Button, Divider, message, Popover } from 'antd'; import { useParams } from 'react-router-dom'; import passwordComplexityValidator from '../common/passwordComplexityValidator'; +import { authHeader } from '../common/AuthHeader'; + +import { getDeviceInfo } from './utils'; +import { setUser } from '../common/userStorage'; + const ResetPassword = () => { - const [user, setUser] = useState(null); const [popOverContent, setPopOverContent] = useState(null); //we will get the reset token from the url and test if it is valid to get the user information @@ -29,28 +33,49 @@ const ResetPassword = () => { }); }; + //if there is no token, we will show an error message to the user useEffect(() => { - //check if reset token is valid, if it is, we will get the user ID and store it, if not, we will redirect to the login page - if (user === null && resetToken !== undefined) { - //get user information by reset token + if (resetToken === undefined) { + invalidToken(); + } + }, []); - // get user by reset token route + const onFinish = async (values) => { + try { + const url = '/api/auth/resetTempPassword'; + const password = values.newPassword; + const deviceInfo = getDeviceInfo(); - //if user is found, set user and return - console.log(setUser); + const response = await fetch(url, { + headers: authHeader(), + method: 'POST', + body: JSON.stringify({ password, token: resetToken, deviceInfo }), + }); - //if user is not found, message - invalidToken(); - } + if (!response.ok) { + let json = await response.json(); - if (resetToken === undefined) { - //redirect to login page - } - }, []); + if (json.message) { + message.error(json.message); + } else { + message.error('An undefined error occurred. Please try again later'); + } + return; + } - const onFinish = (values) => { - console.log('Received values:', values); - alert('reset password code fires here'); + if (response.ok) { + message.success('Password reset successfully.'); + let json = await response.json(); + if (json.success === true) { + json.data.isAuthenticated = true; + setUser(json.data); + //reload window + window.location.href = '/'; + } + } + } catch (err) { + message.error(err.message); + } }; useEffect(() => {}, [popOverContent]); @@ -119,7 +144,7 @@ const ResetPassword = () => { - diff --git a/Tombolo/server/controllers/authController.js b/Tombolo/server/controllers/authController.js index 5e3e006d..9ae17bf5 100644 --- a/Tombolo/server/controllers/authController.js +++ b/Tombolo/server/controllers/authController.js @@ -283,7 +283,7 @@ const verifyEmail = async (req, res) => { //Reset Temp password const resetTempPassword = async (req, res) => { try { - const { tempPassword, password, token, deviceInfo } = req.body; + const { password, token, deviceInfo } = req.body; // From AccountVerificationCodes table findUser ID by code, where code is resetToken const accountVerificationCode = await AccountVerificationCodes.findOne({ @@ -308,11 +308,6 @@ const resetTempPassword = async (req, res) => { throw { status: 404, message: "User not found" }; } - // Compare temp password with hash. - if (!bcrypt.compareSync(tempPassword, user.hash)) { - throw { status: 500, message: "Invalid temporary password" }; - } - // Hash the new password const salt = bcrypt.genSaltSync(10); user.hash = bcrypt.hashSync(password, salt); @@ -328,6 +323,11 @@ const resetTempPassword = async (req, res) => { where: { code: token }, }); + //delete password reset link + await PasswordResetLinks.destroy({ + where: { id: token }, + }); + // Create token id const tokenId = uuidv4(); @@ -549,7 +549,14 @@ const handlePasswordResetRequest = async (req, res) => { // Generate a password reset token const randomId = uuidv4(); - const passwordRestLink = `${process.env.WEB_URL}/reset-password/${randomId}`; + let webUrl = process.env.WEB_URL; + + //cut off last character if it is a slash + if (webUrl[webUrl.length - 1] === "/") { + webUrl = webUrl.slice(0, -1); + } + + const passwordRestLink = `${webUrl}/reset-password/${randomId}`; // Notification subject let subject = "Password Reset Link"; @@ -557,6 +564,9 @@ const handlePasswordResetRequest = async (req, res) => { subject = `${process.env.INSTANCE_NAME} - ${subject}`; } + //include searchableNotificationId in the notification meta data + const searchableNotificationId = uuidv4(); + // Queue notification await NotificationQueue.create({ type: "email", @@ -566,9 +576,12 @@ const handlePasswordResetRequest = async (req, res) => { createdBy: "System", updatedBy: "System", metaData: { - mainRecipients: [email], - subject, - body: "Password reset link", + notificationId: searchableNotificationId, + recipientName: `${user.firstName}`, + notificationOrigin: "Reset Password", + subject: "PasswordResetLink", + mainRecipients: [user.email], + notificationDescription: "Password Reset Link", validForHours: 24, passwordRestLink, }, @@ -583,6 +596,13 @@ const handlePasswordResetRequest = async (req, res) => { expiresAt: new Date(new Date().getTime() + 24 * 60 * 60 * 1000), }); + // Create account verification code + await AccountVerificationCodes.create({ + code: randomId, + userId: user.id, + expiresAt: new Date(new Date().getTime() + 24 * 60 * 60 * 1000), + }); + // response res.status(200).json({ success: true }); } catch (err) { From 29dbbb61353dba2f8f4695678b10399c43393fdb Mon Sep 17 00:00:00 2001 From: Matt Fancher <142915944+FancMa01@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:27:28 -0700 Subject: [PATCH 2/3] Remove unecessary reset temp password code that isn't used with temp password. We use the link and token method for resetting temp passwords, no need to have temporary password code any longer --- .../src/components/login/AuthRoutes.js | 2 - .../src/components/login/ResetPassword.js | 2 +- .../src/components/login/ResetTempPw.jsx | 126 ------------------ .../src/components/login/utils.js | 21 --- Tombolo/server/controllers/authController.js | 67 +--------- Tombolo/server/routes/authRoutes.js | 8 +- 6 files changed, 11 insertions(+), 215 deletions(-) delete mode 100644 Tombolo/client-reactjs/src/components/login/ResetTempPw.jsx diff --git a/Tombolo/client-reactjs/src/components/login/AuthRoutes.js b/Tombolo/client-reactjs/src/components/login/AuthRoutes.js index b95123be..00acd3c6 100644 --- a/Tombolo/client-reactjs/src/components/login/AuthRoutes.js +++ b/Tombolo/client-reactjs/src/components/login/AuthRoutes.js @@ -5,7 +5,6 @@ const Login = React.lazy(() => import('./login.js')); const Register = React.lazy(() => import('./register.js')); const ResetPassword = React.lazy(() => import('./ResetPassword.js')); const ForgotPassword = React.lazy(() => import('./ForgotPassword.js')); -const resetTempPassword = React.lazy(() => import('./ResetTempPw')); const AuthRoutes = () => { //if traditional login isn't enabled, redirect user to login page @@ -32,7 +31,6 @@ const AuthRoutes = () => { - {/* redirect all other routes hit to login */} diff --git a/Tombolo/client-reactjs/src/components/login/ResetPassword.js b/Tombolo/client-reactjs/src/components/login/ResetPassword.js index 181cbeb9..0f6ec064 100644 --- a/Tombolo/client-reactjs/src/components/login/ResetPassword.js +++ b/Tombolo/client-reactjs/src/components/login/ResetPassword.js @@ -42,7 +42,7 @@ const ResetPassword = () => { const onFinish = async (values) => { try { - const url = '/api/auth/resetTempPassword'; + const url = '/api/auth/resetPasswordWithToken'; const password = values.newPassword; const deviceInfo = getDeviceInfo(); diff --git a/Tombolo/client-reactjs/src/components/login/ResetTempPw.jsx b/Tombolo/client-reactjs/src/components/login/ResetTempPw.jsx deleted file mode 100644 index ed5eb879..00000000 --- a/Tombolo/client-reactjs/src/components/login/ResetTempPw.jsx +++ /dev/null @@ -1,126 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Form, Input, Button, Spin, message, Popover } from 'antd'; -import { resetTempPassword } from './utils'; -import passwordComplexityValidator from '../common/passwordComplexityValidator'; -import { setUser } from '../common/userStorage'; - -function ResetTempPassword() { - const [loading, setLoading] = useState(false); - const [popOverContent, setPopOverContent] = useState(null); - const [resetToken, setResetToken] = useState(null); - const [form] = Form.useForm(); - - // For password validator pop over - const validatePassword = (value) => { - setPopOverContent(passwordComplexityValidator({ password: value, generateContent: true })); - }; - - // On component load, get the token from the URL - useEffect(() => { - const url = window.location.href; - const urlParts = url.split('/'); - const token = urlParts[urlParts.length - 1]; - setResetToken(token); - }, []); - - // Handle form submission - const handleSubmit = async () => { - try { - setLoading(true); - let values; - try { - values = await form.validateFields(); - } catch (err) { - return; - } - values.token = resetToken; - const result = await resetTempPassword(values); - - // Save user token to local storage - setUser(JSON.stringify(result.data)); - window.location.href = '/'; - } catch (err) { - message.error(err.message); - } finally { - setLoading(false); - } - }; - - return ( -
- value.trim()} - rules={[ - { - required: true, - message: 'Please input your temporary password!', - }, - ]}> - - - - value.trim()} - rules={[ - { - required: true, - message: 'Please input your new password!', - }, - () => ({ - validator(_, value) { - //passwordComplexityValidator always returns an array with at least one attributes element - const errors = passwordComplexityValidator({ password: value }); - if (!value || errors.length === 1) { - return Promise.resolve(); - } - return Promise.reject(new Error('Password does not meet complexity requirements!')); - }, - }), - ]}> - { - validatePassword(e.target.value); - }} - onFocus={(e) => { - validatePassword(e.target.value); - }} - /> - - - value.trim()} - rules={[ - { - required: true, - message: 'Please confirm your new password!', - }, - { - validator: async (_, value) => { - if (!value || value === form.getFieldValue('password')) { - return Promise.resolve(); - } - return Promise.reject(new Error('The two passwords that you entered do not match!')); - }, - }, - ]}> - - - -
- ); -} - -export default ResetTempPassword; diff --git a/Tombolo/client-reactjs/src/components/login/utils.js b/Tombolo/client-reactjs/src/components/login/utils.js index 916d2908..10d0aa32 100644 --- a/Tombolo/client-reactjs/src/components/login/utils.js +++ b/Tombolo/client-reactjs/src/components/login/utils.js @@ -22,27 +22,6 @@ export const getDeviceInfo = () => { return { os, browser: browserName }; }; -// Make a request to the server to reset the temporary password -export const resetTempPassword = async (resetData) => { - const payload = { - method: 'POST', - headers: authHeader(), - body: JSON.stringify({ ...resetData, deviceInfo: getDeviceInfo() }), - }; - - const response = await fetch('/api/auth/resetTempPassword', payload); - - // Get the data from the response - const responseJson = await response.json(); - - // Check if the response is ok - if (!response.ok) { - throw new Error(responseJson.message); - } - - return responseJson; -}; - // Make POST request to api/auth/verifyEmail with token in body export const verifyEmail = async (token) => { // eslint-disable-next-line no-unreachable diff --git a/Tombolo/server/controllers/authController.js b/Tombolo/server/controllers/authController.js index 9ae17bf5..0e1d4301 100644 --- a/Tombolo/server/controllers/authController.js +++ b/Tombolo/server/controllers/authController.js @@ -281,7 +281,7 @@ const verifyEmail = async (req, res) => { }; //Reset Temp password -const resetTempPassword = async (req, res) => { +const resetPasswordWithToken = async (req, res) => { try { const { password, token, deviceInfo } = req.body; @@ -328,6 +328,11 @@ const resetTempPassword = async (req, res) => { where: { id: token }, }); + //remove all sessions for user before initiating new session + await RefreshTokens.destroy({ + where: { userId: user.id }, + }); + // Create token id const tokenId = uuidv4(); @@ -613,63 +618,6 @@ const handlePasswordResetRequest = async (req, res) => { } }; -// Reset password -const resetPassword = async (req, res) => { - try { - // Get the password reset token - const { token, password } = req.body; - - // Find the password reset token - const passwordResetLink = await PasswordResetLinks.findOne({ - where: { id: token }, - }); - - // Token does not exist - if (!passwordResetLink) { - logger.error(`Reset password: Token ${token} does not exist`); - return res.status(404).json({ - success: false, - message: "Password reset link Invalid or expired", - }); - } - - // Token has expired - if (new Date() > passwordResetLink.expiresAt) { - logger.error(`Reset password: Token ${token} has expired`); - return res - .status(400) - .json({ success: false, message: "Token has expired" }); - } - - // Find the user - const user = await User.findOne({ - where: { id: passwordResetLink.userId }, - }); - - // Hash the new password - const salt = bcrypt.genSaltSync(10); - user.hash = bcrypt.hashSync(password, salt); - - // Save the user - await user.save(); - - // Delete the password link from - PasswordResetLinks.destroy({ - where: { id: token }, - }); - - // response - res - .status(200) - .json({ success: true, message: "Password updated successfully" }); - } catch (err) { - logger.error(`Reset password: ${err.message}`); - res - .status(err.status || 500) - .json({ success: false, message: err.message }); - } -}; - // Login or register with azure user - loginOrRegisterAzureUser [ `https://login.microsoftonline.com/${tenant_id}/oauth2/v2.0/token`] const loginOrRegisterAzureUser = async (req, res, next) => { try { @@ -829,11 +777,10 @@ const loginOrRegisterAzureUser = async (req, res, next) => { module.exports = { createBasicUser, verifyEmail, - resetTempPassword, + resetPasswordWithToken, loginBasicUser, logOutBasicUser, handlePasswordResetRequest, - resetPassword, createApplicationOwner, loginOrRegisterAzureUser, }; diff --git a/Tombolo/server/routes/authRoutes.js b/Tombolo/server/routes/authRoutes.js index 2a3bcc53..760120a4 100644 --- a/Tombolo/server/routes/authRoutes.js +++ b/Tombolo/server/routes/authRoutes.js @@ -18,9 +18,8 @@ const { loginBasicUser, logOutBasicUser, handlePasswordResetRequest, - resetPassword, createApplicationOwner, - resetTempPassword, + resetPasswordWithToken, verifyEmail, loginOrRegisterAzureUser, } = require("../controllers/authController"); @@ -45,11 +44,10 @@ router.post( validatePasswordResetRequestPayload, handlePasswordResetRequest ); // Reset password -router.post("/resetPassword", validateResetPasswordPayload, resetPassword); // Reset password router.post( - "/resetTempPassword", + "/resetPasswordWithToken", validateResetPasswordPayload, - resetTempPassword + resetPasswordWithToken ); // Complete registration by resetting temporary password router.post("/verifyEmail", verifyEmail); // Verify email // TODO - Forgot password route From 2403a88d5d0df43d21cc984ddd2b6a5e41ef348d Mon Sep 17 00:00:00 2001 From: Matt Fancher <142915944+FancMa01@users.noreply.github.com> Date: Tue, 24 Dec 2024 08:33:43 -0700 Subject: [PATCH 3/3] Flag session from the requested token with current --- .../application/myAccount/myAccountTable.jsx | 6 +++++- Tombolo/server/controllers/sessionController.js | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/Tombolo/client-reactjs/src/components/application/myAccount/myAccountTable.jsx b/Tombolo/client-reactjs/src/components/application/myAccount/myAccountTable.jsx index 29256172..5ecdb086 100644 --- a/Tombolo/client-reactjs/src/components/application/myAccount/myAccountTable.jsx +++ b/Tombolo/client-reactjs/src/components/application/myAccount/myAccountTable.jsx @@ -12,6 +12,7 @@ const MyAccountTable = ({ user }) => { if (!sessions?.success) { return; } + setSessions(sessions.data); return; }; @@ -36,7 +37,10 @@ const MyAccountTable = ({ user }) => { title: 'Revoke', dataIndex: 'id', key: 'id', - render: (id) => { + render: (id, current) => { + if (current.current) { + return Active Session; + } return ( { diff --git a/Tombolo/server/controllers/sessionController.js b/Tombolo/server/controllers/sessionController.js index 5d8de27a..0a6b9ed6 100644 --- a/Tombolo/server/controllers/sessionController.js +++ b/Tombolo/server/controllers/sessionController.js @@ -2,6 +2,7 @@ const jwt = require("jsonwebtoken"); const models = require("../models"); const { blacklistToken } = require("../utils/tokenBlackListing"); const logger = require("../config/logger"); +const { verifyToken } = require("../utils/authUtil"); const RefreshTokens = models.RefreshTokens; @@ -26,6 +27,16 @@ const activeSessionsByUserId = async (req, res) => { } }); + //grab current session token id from the request + const token = req.cookies.token; + let decoded = await verifyToken(token, process.env.JWT_SECRET); + const currentTokenId = decoded.tokenId; + + // Mark the current token + activeSessions.forEach((session) => { + session.dataValues.current = session.id === currentTokenId; + }); + // response res.status(200).json({ success: true, data: activeSessions }); } catch (err) {