generated from graasp/graasp-repo
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: make own validation for password (#688)
* feat: make own validation for password * fix: add mention of the origin of the code * fix: add extensive password checks
- Loading branch information
Showing
7 changed files
with
232 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { describe, expect, it, test } from 'vitest'; | ||
|
||
import { isStrongPassword } from './isPasswordStrong.js'; | ||
|
||
const defaultOptions = { | ||
minLength: 8, | ||
minLowercase: 1, | ||
minUppercase: 1, | ||
minNumbers: 1, | ||
minSymbols: 1, | ||
}; | ||
|
||
describe('isStrongPassword', () => { | ||
it('not a strong password', () => { | ||
expect(isStrongPassword('', {})).toBeFalsy(); | ||
}); | ||
it('uses default values when not given', () => { | ||
expect(isStrongPassword('a', { minLength: 1 })).toBeFalsy(); | ||
// need to specify all values for the password to be strong in this setting | ||
expect( | ||
isStrongPassword('a', { | ||
minLength: 1, | ||
minLowercase: 0, | ||
minUppercase: 0, | ||
minNumbers: 0, | ||
minSymbols: 0, | ||
}), | ||
).toBeTruthy(); | ||
}); | ||
it('password is strong', () => { | ||
expect(isStrongPassword('aTest0!zu', {})).toBeTruthy(); | ||
}); | ||
|
||
test.each([ | ||
'%2%k{7BsL"M%Kd6e', | ||
'EXAMPLE of very long_password123!', | ||
'mxH_+2vs&54_+H3P', | ||
'+&DxJ=X7-4L8jRCD', | ||
'etV*p%Nr6w&H%FeF', | ||
'£3.ndSau_7', | ||
'VaLIDWith\\Symb0l', | ||
])('valid password "%s"', (value) => { | ||
expect(isStrongPassword(value, defaultOptions)).toBeTruthy(); | ||
}); | ||
|
||
test.each([ | ||
'', | ||
'password', | ||
'hunter2', | ||
'hello world', | ||
'passw0rd', | ||
'password!', | ||
'PASSWORD!', | ||
])('invalid password "%s"', (value) => { | ||
expect(isStrongPassword(value, defaultOptions)).toBeFalsy(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
/** | ||
* This code was adapted from the `validator.js` package | ||
* https://github.com/validatorjs/validator.js/tree/master | ||
*/ | ||
import { countChars, merge } from './utils.js'; | ||
|
||
const upperCaseRegex = /^[A-Z]$/; | ||
const lowerCaseRegex = /^[a-z]$/; | ||
const numberRegex = /^\d$/; | ||
const symbolRegex = /^[-#!$@£%^&*()_+|~=`{}[\]:";'<>?,./\\ ]$/; | ||
|
||
const defaultOptions = { | ||
minLength: 8, | ||
minLowercase: 1, | ||
minUppercase: 1, | ||
minNumbers: 1, | ||
minSymbols: 1, | ||
returnScore: false, | ||
pointsPerUnique: 1, | ||
pointsPerRepeat: 0.5, | ||
pointsForContainingLower: 10, | ||
pointsForContainingUpper: 10, | ||
pointsForContainingNumber: 10, | ||
pointsForContainingSymbol: 10, | ||
}; | ||
type PasswordOptions = typeof defaultOptions; | ||
|
||
type PasswordAnalysis = { | ||
length: number; | ||
uniqueChars: number; | ||
uppercaseCount: number; | ||
lowercaseCount: number; | ||
numberCount: number; | ||
symbolCount: number; | ||
}; | ||
|
||
/* Return information about a password */ | ||
function analyzePassword(password: string): PasswordAnalysis { | ||
const charMap = countChars(password); | ||
const analysis = { | ||
length: password.length, | ||
uniqueChars: Object.keys(charMap).length, | ||
uppercaseCount: 0, | ||
lowercaseCount: 0, | ||
numberCount: 0, | ||
symbolCount: 0, | ||
}; | ||
Object.keys(charMap).forEach((char) => { | ||
/* istanbul ignore else */ | ||
if (upperCaseRegex.test(char)) { | ||
analysis.uppercaseCount += charMap[char]; | ||
} else if (lowerCaseRegex.test(char)) { | ||
analysis.lowercaseCount += charMap[char]; | ||
} else if (numberRegex.test(char)) { | ||
analysis.numberCount += charMap[char]; | ||
} else if (symbolRegex.test(char)) { | ||
analysis.symbolCount += charMap[char]; | ||
} | ||
}); | ||
return analysis; | ||
} | ||
|
||
function scorePassword( | ||
analysis: PasswordAnalysis, | ||
scoringOptions: PasswordOptions, | ||
): number { | ||
let points = 0; | ||
points += analysis.uniqueChars * scoringOptions.pointsPerUnique; | ||
points += | ||
(analysis.length - analysis.uniqueChars) * scoringOptions.pointsPerRepeat; | ||
if (analysis.lowercaseCount > 0) { | ||
points += scoringOptions.pointsForContainingLower; | ||
} | ||
if (analysis.uppercaseCount > 0) { | ||
points += scoringOptions.pointsForContainingUpper; | ||
} | ||
if (analysis.numberCount > 0) { | ||
points += scoringOptions.pointsForContainingNumber; | ||
} | ||
if (analysis.symbolCount > 0) { | ||
points += scoringOptions.pointsForContainingSymbol; | ||
} | ||
return points; | ||
} | ||
|
||
export function isStrongPassword( | ||
str: string, | ||
options: Partial<PasswordOptions>, | ||
) { | ||
const analysis = analyzePassword(str); | ||
const newOptions = merge(options || {}, defaultOptions); | ||
if (newOptions.returnScore) { | ||
return scorePassword(analysis, newOptions); | ||
} | ||
return ( | ||
analysis.length >= newOptions.minLength && | ||
analysis.lowercaseCount >= newOptions.minLowercase && | ||
analysis.uppercaseCount >= newOptions.minUppercase && | ||
analysis.numberCount >= newOptions.minNumbers && | ||
analysis.symbolCount >= newOptions.minSymbols | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
|
||
import { countChars, merge } from './utils.js'; | ||
|
||
describe('countChars', () => { | ||
it('count empty string', () => { | ||
expect(countChars('')).toEqual({}); | ||
}); | ||
it('count small string', () => { | ||
expect(countChars('abcd')).toEqual({ a: 1, b: 1, c: 1, d: 1 }); | ||
}); | ||
it('count repeating string', () => { | ||
expect(countChars('aaaaaaaaaa')).toEqual({ a: 10 }); | ||
}); | ||
it('count string with spaces', () => { | ||
expect(countChars(' aaaaaaaaaa ')).toEqual({ a: 10, ' ': 2 }); | ||
}); | ||
}); | ||
|
||
describe('merge', () => { | ||
it('only default options', () => { | ||
const defaultOptions = { a: 1, b: false, c: 'test' }; | ||
expect(merge({}, { a: 1, b: false, c: 'test' })).toEqual(defaultOptions); | ||
}); | ||
it('provided value takes precedence over default options', () => { | ||
const defaultOptions = { a: 1, b: false, c: 'test' }; | ||
expect(merge({ a: 2 }, { a: 1, b: false, c: 'test' })).toEqual({ | ||
...defaultOptions, | ||
a: 2, | ||
}); | ||
}); | ||
it('value not in options is kept, but should be a ts error', () => { | ||
const defaultOptions = { a: 1, b: false, c: 'test' }; | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-expect-error | ||
expect(merge({ a: 2, k: 'hello' }, { a: 1, b: false, c: 'test' })).toEqual({ | ||
...defaultOptions, | ||
a: 2, | ||
k: 'hello', | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
export function merge<T extends Record<string, unknown>>( | ||
obj: Partial<T>, | ||
defaults: T, | ||
): T { | ||
for (const key in defaults) { | ||
if (typeof obj[key] === 'undefined') { | ||
obj[key] = defaults[key]; | ||
} | ||
} | ||
return obj as T; | ||
} | ||
|
||
/** | ||
* Count occurrence of characters in a string | ||
* @param str string to process | ||
* @returns an object with character keys and values occurrence of char | ||
*/ | ||
export function countChars(str: string) { | ||
const result: Record<string, number> = {}; | ||
for (const char of Array.from(str)) { | ||
if (result[char]) { | ||
result[char] += 1; | ||
} else { | ||
result[char] = 1; | ||
} | ||
} | ||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters