feat: Refresh Token for improved Session Security (#927)

* feat(api): refresh token logic

* feat(client): refresh token logic

* feat(data-provider): refresh token logic

* fix: SSE uses esm

* chore: add default refresh token expiry to AuthService, add message about env var not set when generating a token

* chore: update scripts to more compatible bun methods, ran bun install again

* chore: update env.example and playwright workflow with JWT_REFRESH_SECRET

* chore: update breaking changes docs

* chore: add timeout to url visit

* chore: add default SESSION_EXPIRY in generateToken logic, add act script for testing github actions

* fix(e2e): refresh automatically in development environment to pass e2e tests
This commit is contained in:
Danny Avila 2023-09-11 13:10:46 -04:00 committed by GitHub
parent 75be9a3279
commit 33f087d38f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 420 additions and 232 deletions

View file

@ -1,32 +1,36 @@
const crypto = require('crypto');
const bcrypt = require('bcryptjs');
const User = require('../../models/User');
const Session = require('../../models/Session');
const Token = require('../../models/schema/tokenSchema');
const { registerSchema } = require('../../strategies/validators');
const config = require('../../../config/loader');
const { sendEmail } = require('../utils');
const domains = config.domains;
const isProduction = config.isProduction;
/**
* Logout user
*
* @param {Object} user
* @param {String} userId
* @param {*} refreshToken
* @returns
*/
const logoutUser = async (user, refreshToken) => {
const logoutUser = async (userId, refreshToken) => {
try {
const userFound = await User.findById(user._id);
const tokenIndex = userFound.refreshToken.findIndex(
(item) => item.refreshToken === refreshToken,
);
const hash = crypto.createHash('sha256').update(refreshToken).digest('hex');
if (tokenIndex !== -1) {
userFound.refreshToken.id(userFound.refreshToken[tokenIndex]._id).remove();
// Find the session with the matching user and refreshTokenHash
const session = await Session.findOne({ user: userId, refreshTokenHash: hash });
if (session) {
try {
await Session.deleteOne({ _id: session._id });
} catch (deleteErr) {
console.error(deleteErr);
return { status: 500, message: 'Failed to delete session.' };
}
}
await userFound.save();
return { status: 200, message: 'Logout successful' };
} catch (err) {
return { status: 500, message: err.message };
@ -83,9 +87,6 @@ const registerUser = async (user) => {
role: isFirstRegisteredUser ? 'ADMIN' : 'USER',
});
// todo: implement refresh token
// const refreshToken = newUser.generateRefreshToken();
// newUser.refreshToken.push({ refreshToken });
const salt = bcrypt.genSaltSync(10);
const hash = bcrypt.hashSync(newUser.password, salt);
newUser.password = hash;
@ -188,9 +189,51 @@ const resetPassword = async (userId, token, password) => {
return { message: 'Password reset was successful' };
};
/**
* Set Auth Tokens
*
* @param {String} userId
* @param {Object} res
* @param {String} sessionId
* @returns
*/
const setAuthTokens = async (userId, res, sessionId = null) => {
try {
const user = await User.findOne({ _id: userId });
const token = await user.generateToken();
let session;
let refreshTokenExpires;
if (sessionId) {
session = await Session.findById(sessionId);
refreshTokenExpires = session.expiration.getTime();
} else {
session = new Session({ user: userId });
const { REFRESH_TOKEN_EXPIRY } = process.env ?? {};
const expires = eval(REFRESH_TOKEN_EXPIRY) ?? 1000 * 60 * 60 * 24 * 7;
refreshTokenExpires = Date.now() + expires;
}
const refreshToken = await session.generateRefreshToken();
res.cookie('refreshToken', refreshToken, {
expires: new Date(refreshTokenExpires),
httpOnly: true,
secure: isProduction,
sameSite: 'strict',
});
return token;
} catch (error) {
console.log('Error in setting authentication tokens:', error);
throw error;
}
};
module.exports = {
registerUser,
logoutUser,
requestPasswordReset,
resetPassword,
setAuthTokens,
};