Strengthen password requirement

This commit is contained in:
Tuan Dang
2023-06-06 23:06:44 +01:00
parent 846f5c6680
commit c5be497052
6 changed files with 198 additions and 171 deletions
@@ -10,7 +10,10 @@ import {
clearTokens clearTokens
} from '../../helpers'; } from '../../helpers';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; import {
TOKEN_EMAIL_PASSWORD_RESET,
AUTH_MODE_JWT
} from '../../variables';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { import {
getSiteURL, getSiteURL,
@@ -231,7 +234,9 @@ export const changePassword = async (req: Request, res: Response) => {
} }
); );
await clearTokens(user._id); if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
}
// clear httpOnly cookie // clear httpOnly cookie
+39 -48
View File
@@ -2,7 +2,7 @@ import crypto from 'crypto';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; import { faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import jsrp from 'jsrp'; import jsrp from 'jsrp';
import nacl from 'tweetnacl'; import nacl from 'tweetnacl';
@@ -13,7 +13,7 @@ import getOrganizations from '@app/pages/api/organization/getOrgs';
import ProjectService from '@app/services/ProjectService'; import ProjectService from '@app/services/ProjectService';
import InputField from '../basic/InputField'; import InputField from '../basic/InputField';
import passwordCheck from '../utilities/checks/PasswordCheck'; import checkPassword from '../utilities/checks/checkPassword';
import Aes256Gcm from '../utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '../utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '../utilities/cryptography/crypto'; import { deriveArgonKey } from '../utilities/cryptography/crypto';
import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage'; import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage';
@@ -37,6 +37,15 @@ interface UserInfoStepProps {
providerAuthToken?: string; providerAuthToken?: string;
} }
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
/** /**
* This is the step of the sign up flow where people provife their name/surname and password * This is the step of the sign up flow where people provife their name/surname and password
* @param {object} obj * @param {object} obj
@@ -69,6 +78,8 @@ export default function UserInfoStep({
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@@ -89,12 +100,10 @@ export default function UserInfoStep({
} else { } else {
setOrganizationNameError(false); setOrganizationNameError(false);
} }
errorCheck = passwordCheck({
errorCheck = checkPassword({
password, password,
setPasswordErrorLength, setErrors
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck
}); });
if (!errorCheck) { if (!errorCheck) {
@@ -248,12 +257,9 @@ export default function UserInfoStep({
label={t('section.password.password')} label={t('section.password.password')}
onChangeHandler={(pass: string) => { onChangeHandler={(pass: string) => {
setPassword(pass); setPassword(pass);
passwordCheck({ checkPassword({
password: pass, password: pass,
setPasswordErrorLength, setErrors
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
}); });
}} }}
type="password" type="password"
@@ -263,44 +269,29 @@ export default function UserInfoStep({
autoComplete="new-password" autoComplete="new-password"
id="new-password" id="new-password"
/> />
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( {Object.keys(errors).length > 0 && (
<div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">{t('section.password.validate-base')}</div> <div className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
<div className="ml-1 flex flex-row items-center justify-start"> {Object.keys(errors).map((key) => {
{passwordErrorLength ? ( if (errors[key as keyof Errors]) {
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" /> return (
) : ( <div className="ml-1 flex flex-row items-top justify-start">
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> <div>
)} <FontAwesomeIcon
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}> icon={faXmark}
{t('section.password.validate-length')} className="text-md text-red ml-0.5 mr-2.5"
</div> />
</div> </div>
<div className="ml-1 flex flex-row items-center justify-start"> <p className="text-gray-400 text-sm">
{passwordErrorLowerCase ? ( {errors[key as keyof Errors]}
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" /> </p>
) : ( </div>
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> );
)} }
<div
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`} return null;
> })}
{t('section.password.validate-case')}
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faXmark} className="text-md text-red ml-0.5 mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
{t('section.password.validate-number')}
</div>
</div>
</div> </div>
) : (
<div className="py-2" />
)} )}
</div> </div>
<div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left"> <div className="flex flex-col items-center justify-center lg:w-[19%] w-1/4 min-w-[20rem] mt-2 max-w-xs md:max-w-md mx-auto text-sm text-center md:text-left">
@@ -17,6 +17,7 @@ const passwordCheck = ({
setPasswordErrorLowerCase, setPasswordErrorLowerCase,
errorCheck errorCheck
}: PasswordCheckProps) => { }: PasswordCheckProps) => {
if (!password || password.length < 14) { if (!password || password.length < 14) {
setPasswordErrorLength(true); setPasswordErrorLength(true);
errorCheck = true; errorCheck = true;
@@ -0,0 +1,65 @@
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
interface CheckPasswordParams {
password: string;
setErrors: (value: Errors) => void;
}
/**
* Validate that the password [password] is at least:
* - 8 characters long
* - Contains 1 uppercase character (A-Z)
* - Contains 1 lowercase character (a-z)
* - Contains 1 number (0-9)
* - Does not contain 3 repeat, consecutive characters
*
* The function returns whether or not the password [password]
* passes the minimum requirements above. It sets errors on
* an erorr object via [setErrors].
*
* @param {Object} obj
* @param {String} obj.password - the password to check
* @param {Function} obj.setErrors - set state function to set error object
*/
const checkPassword = ({
password,
setErrors
}: CheckPasswordParams): boolean => {
let errors: Errors = {};
if (password.length < 8) {
errors.length = "8 characters";
}
if (!/[A-Z]/.test(password)) {
errors.upperCase = "1 uppercase character (A-Z)";
}
if (!/[a-z]/.test(password)) {
errors.lowerCase = "1 lowercase character (a-z)";
}
if (!/[0-9]/.test(password)) {
errors.number = "1 number (0-9)";
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.specialChar = "1 special character (!@#$%^&*(),.?)";
}
if (/([A-Za-z0-9])\1\1\1/.test(password)) {
errors.repeatedChar = "No 3 repeat, consecutive characters";
}
setErrors(errors);
return Object.keys(errors).length > 0;
}
export default checkPassword;
+43 -66
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faCheck, faPlus, faX, faBan } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faPlus, faXmark, faBan } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Button from '@app/components/basic/buttons/Button'; import Button from '@app/components/basic/buttons/Button';
@@ -10,7 +10,7 @@ import InputField from '@app/components/basic/InputField';
import ListBox from '@app/components/basic/Listbox'; import ListBox from '@app/components/basic/Listbox';
import ApiKeyTable from '@app/components/basic/table/ApiKeyTable'; import ApiKeyTable from '@app/components/basic/table/ApiKeyTable';
import NavHeader from '@app/components/navigation/NavHeader'; import NavHeader from '@app/components/navigation/NavHeader';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck'; import checkPassword from '@app/components/utilities/checks/checkPassword';
import changePassword from '@app/components/utilities/cryptography/changePassword'; import changePassword from '@app/components/utilities/cryptography/changePassword';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey'; import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection'; import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection';
@@ -22,12 +22,18 @@ import {
useRevokeAllSessions useRevokeAllSessions
} from '@app/hooks/api'; } from '@app/hooks/api';
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function PersonalSettings() { export default function PersonalSettings() {
const [personalEmail, setPersonalEmail] = useState(''); const [personalEmail, setPersonalEmail] = useState('');
const [personalName, setPersonalName] = useState(''); const [personalName, setPersonalName] = useState('');
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [currentPasswordError, setCurrentPasswordError] = useState(false); const [currentPasswordError, setCurrentPasswordError] = useState(false);
const [currentPassword, setCurrentPassword] = useState(''); const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState(''); const [newPassword, setNewPassword] = useState('');
@@ -37,6 +43,7 @@ export default function PersonalSettings() {
const [backupKeyError, setBackupKeyError] = useState(false); const [backupKeyError, setBackupKeyError] = useState(false);
const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false); const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false);
const [apiKeys, setApiKeys] = useState<any[]>([]); const [apiKeys, setApiKeys] = useState<any[]>([]);
const [errors, setErrors] = useState<Errors>({});
const revokeAllSessions = useRevokeAllSessions(); const revokeAllSessions = useRevokeAllSessions();
@@ -159,78 +166,52 @@ export default function PersonalSettings() {
label={t('section.password.new') as string} label={t('section.password.new') as string}
onChangeHandler={(password) => { onChangeHandler={(password) => {
setNewPassword(password); setNewPassword(password);
passwordCheck({ checkPassword({
password, password,
setPasswordErrorLength, setErrors
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
}); });
}} }}
type="password" type="password"
value={newPassword} value={newPassword}
isRequired isRequired
error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber} error={Object.keys(errors).length > 0}
autoComplete="new-password" autoComplete="new-password"
id="new-password" id="new-password"
/> />
</div> </div>
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( {Object.keys(errors).length > 0 && (
<div className="mt-3 mb-2 flex w-full max-w-xl flex-col items-start rounded-md bg-white/5 px-2 py-2"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400"> <div className="mb-2 text-sm text-gray-400">{t('section.password.validate-base')}</div>
{t('section.password.validate-base')} {Object.keys(errors).map((key) => {
</div> if (errors[key as keyof Errors]) {
<div className="ml-1 flex flex-row items-center justify-start"> return (
{passwordErrorLength ? ( <div className="ml-1 flex flex-row items-top justify-start">
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> <div>
) : ( <FontAwesomeIcon
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" /> icon={faXmark}
)} className="text-md text-red ml-0.5 mr-2.5"
<div />
className={`${ </div>
passwordErrorLength ? 'text-gray-400' : 'text-gray-600' <p className="text-gray-400 text-sm">
} text-sm`} {errors[key as keyof Errors]}
> </p>
{t('section.password.validate-length')} </div>
</div> );
</div> }
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowerCase ? ( return null;
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" /> })}
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section.password.validate-case')}
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'
} text-sm`}
>
{t('section.password.validate-number')}
</div>
</div>
</div> </div>
) : (
<div className="py-2" />
)} )}
<div className="mt-3 flex w-52 flex-row items-center pr-3"> <div className="mt-3 flex w-52 flex-row items-center pr-3">
<Button <Button
text={t('section.password.change') as string} text={t('section.password.change') as string}
onButtonPressed={() => { onButtonPressed={() => {
if (!passwordErrorLength && !passwordErrorLowerCase && !passwordErrorNumber) { const errorCheck = checkPassword({
password: newPassword,
setErrors
});
if (!errorCheck) {
changePassword( changePassword(
personalEmail, personalEmail,
currentPassword, currentPassword,
@@ -244,11 +225,7 @@ export default function PersonalSettings() {
}} }}
color="mineshaft" color="mineshaft"
size="md" size="md"
active={ active={true}
newPassword !== '' &&
currentPassword !== '' &&
!(passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber)
}
textDisabled={t('section.password.change') as string} textDisabled={t('section.password.change') as string}
/> />
<FontAwesomeIcon <FontAwesomeIcon
+43 -55
View File
@@ -7,7 +7,7 @@ import Head from 'next/head';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { faCheck, faWarning, faX } from '@fortawesome/free-solid-svg-icons'; import { faCheck, faWarning, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import jsrp from 'jsrp'; import jsrp from 'jsrp';
import queryString from 'query-string'; import queryString from 'query-string';
@@ -18,6 +18,9 @@ import Button from '@app/components/basic/buttons/Button';
import InputField from '@app/components/basic/InputField'; import InputField from '@app/components/basic/InputField';
import attemptLogin from '@app/components/utilities/attemptLogin'; import attemptLogin from '@app/components/utilities/attemptLogin';
import passwordCheck from '@app/components/utilities/checks/PasswordCheck'; import passwordCheck from '@app/components/utilities/checks/PasswordCheck';
import checkPassword from '@app/components/utilities/checks/checkPassword';
import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm'; import Aes256Gcm from '@app/components/utilities/cryptography/aes-256-gcm';
import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto'; import { deriveArgonKey } from '@app/components/utilities/cryptography/crypto';
import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey'; import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey';
@@ -32,21 +35,26 @@ import verifySignupInvite from './api/auth/VerifySignupInvite';
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
type Errors = {
length?: string,
upperCase?: string,
lowerCase?: string,
number?: string,
specialChar?: string,
repeatedChar?: string,
};
export default function SignupInvite() { export default function SignupInvite() {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState(''); const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState(''); const [lastName, setLastName] = useState('');
const [firstNameError, setFirstNameError] = useState(false); const [firstNameError, setFirstNameError] = useState(false);
const [lastNameError, setLastNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false);
const [passwordErrorLength, setPasswordErrorLength] = useState(false);
const [passwordErrorNumber, setPasswordErrorNumber] = useState(false);
const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false);
const [errorLogin, setErrorLogin] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [backupKeyError, setBackupKeyError] = useState(false); const [backupKeyError, setBackupKeyError] = useState(false);
const [verificationToken, setVerificationToken] = useState('');
const [backupKeyIssued, setBackupKeyIssued] = useState(false); const [backupKeyIssued, setBackupKeyIssued] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const router = useRouter(); const router = useRouter();
const parsedUrl = queryString.parse(router.asPath.split('?')[1]); const parsedUrl = queryString.parse(router.asPath.split('?')[1]);
@@ -70,12 +78,10 @@ export default function SignupInvite() {
} else { } else {
setLastNameError(false); setLastNameError(false);
} }
errorCheck = passwordCheck({
errorCheck = checkPassword({
password, password,
setPasswordErrorLength, setErrors
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck
}); });
if (!errorCheck) { if (!errorCheck) {
@@ -252,60 +258,42 @@ export default function SignupInvite() {
label="Password" label="Password"
onChangeHandler={(pass) => { onChangeHandler={(pass) => {
setPassword(pass); setPassword(pass);
passwordCheck({ checkPassword({
password: pass, password: pass,
setPasswordErrorLength, setErrors
setPasswordErrorNumber,
setPasswordErrorLowerCase,
errorCheck: false
}); });
}} }}
type="password" type="password"
value={password} value={password}
isRequired isRequired
error={passwordErrorLength && passwordErrorNumber && passwordErrorLowerCase} error={Object.keys(errors).length > 0}
autoComplete="new-password" autoComplete="new-password"
id="new-password" id="new-password"
/> />
{passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( {Object.keys(errors).length > 0 && (
<div className="w-full mt-4 bg-white/5 px-2 flex flex-col items-start py-2 rounded-md"> <div className="mt-4 flex w-full flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="text-gray-400 text-sm mb-1">Password should contain at least:</div> <div className="mb-2 text-sm text-gray-400">Password should contain at least:</div>
<div className="flex flex-row justify-start items-center ml-1"> {Object.keys(errors).map((key) => {
{passwordErrorLength ? ( if (errors[key as keyof Errors]) {
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" /> return (
) : ( <div className="ml-1 flex flex-row items-top justify-start">
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" /> <div>
)} <FontAwesomeIcon
<div className={`${passwordErrorLength ? 'text-gray-400' : 'text-gray-600'} text-sm`}> icon={faXmark}
14 characters className="text-md text-red ml-0.5 mr-2.5"
</div> />
</div>
<p className="text-gray-400 text-sm">
{errors[key as keyof Errors]}
</p>
</div>
);
}
return null;
})}
</div> </div>
<div className="flex flex-row justify-start items-center ml-1"> )}
{passwordErrorLowerCase ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div
className={`${passwordErrorLowerCase ? 'text-gray-400' : 'text-gray-600'} text-sm`}
>
1 lowercase character
</div>
</div>
<div className="flex flex-row justify-start items-center ml-1">
{passwordErrorNumber ? (
<FontAwesomeIcon icon={faX} className="text-md text-red mr-2.5" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md text-primary mr-2" />
)}
<div className={`${passwordErrorNumber ? 'text-gray-400' : 'text-gray-600'} text-sm`}>
1 number
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
</div> </div>
<div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg"> <div className="flex flex-col items-center justify-center md:px-4 md:py-5 mt-2 px-2 py-3 max-h-24 max-w-max mx-auto text-lg">
<Button <Button