Skip to content

Commit

Permalink
fix: added frontend tests as well as updated the password strength meter
Browse files Browse the repository at this point in the history
  • Loading branch information
KamilPawel committed Jun 26, 2023
1 parent 45e4df4 commit a5803a6
Show file tree
Hide file tree
Showing 7 changed files with 542 additions and 437 deletions.
4 changes: 3 additions & 1 deletion cfl_common/common/csp_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
CSP_CONNECT_SRC = (
"'self'",
"https://*.onetrust.com/",
"https://api.pwnedpasswords.com",
"https://euc-widget.freshworks.com/",
"https://codeforlife.freshdesk.com/",
"https://api.iconify.design/",
Expand All @@ -14,14 +15,15 @@
"https://www.google-analytics.com/",
"https://pyodide-cdn2.iodide.io/v0.15.0/full/",
"https://crowdin.com/",
f"wss://{MODULE_NAME}-aimmo.codeforlife.education/",
"" f"wss://{MODULE_NAME}-aimmo.codeforlife.education/",
f"https://{MODULE_NAME}-aimmo.codeforlife.education/",
)
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com/", "https://fonts.googleapis.com/", "https://use.typekit.net/")
CSP_SCRIPT_SRC = (
"'self'",
"'unsafe-inline'",
"'unsafe-eval'",
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js",
"https://cdn.crowdin.com/",
"https://*.onetrust.com/",
"https://code.jquery.com/",
Expand Down
12 changes: 3 additions & 9 deletions portal/helpers/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,7 @@ def password_test(self, password):
f"Password not strong enough, consider using at least {minimum_password_length} characters and making it hard to guess."
)
if is_password_pwned(password):
raise forms.ValidationError(
"Password has been found in a data breach, please choose a different password."
)
raise forms.ValidationError("Password is too common, consider using a different password.")

elif self is PasswordStrength.INDEPENDENT:
minimum_password_length = 8
Expand All @@ -73,9 +71,7 @@ def password_test(self, password):
"upper and lower case letters, and numbers and making it hard to guess."
)
if is_password_pwned(password):
raise forms.ValidationError(
"Password has been found in a data breach, please choose a different password."
)
raise forms.ValidationError("Password is too common, consider using a different password.")
else:
minimum_password_length = 10
if password and not password_strength_test(
Expand All @@ -91,9 +87,7 @@ def password_test(self, password):
"upper and lower case letters, numbers, special characters and making it hard to guess."
)
if is_password_pwned(password):
raise forms.ValidationError(
"Password has been found in a data breach, please choose a different password."
)
raise forms.ValidationError("Password is too common, consider using a different password.")

return password

Expand Down
174 changes: 111 additions & 63 deletions portal/static/portal/js/passwordStrength.js
Original file line number Diff line number Diff line change
@@ -1,82 +1,130 @@
var TEACHER_PASSWORD_FIELD_ID = '';
var INDEP_STUDENT_PASSWORD_FIELD_ID = '';
let teacher_password_field = '';
let indep_student_password_field = '';
let most_used_passwords = ['Abcd1234', 'Password1', 'Qwerty123'];

let password_strengths = [
{ name: 'No password!', colour: '#FF0000' },
{ name: 'Password too weak!', colour: '#DBA901' },
{ name: 'Strong password!', colour: '#088A08' },
{ name: 'Password too common!', colour: '#DBA901' }
{ name: 'No password!', colour: '#FF0000' },
{ name: 'Password too weak!', colour: '#DBA901' },
{ name: 'Password too common!', colour: '#DBA901' },
{ name: 'Strong password!', colour: '#088A08' }
];

$(function() {
async function handlePasswordStrength() {
const teacherPassword = $('#id_teacher_signup-teacher_password').val();
const independentStudentPassword = $(
'#id_independent_student_signup-password'
).val();

const isTeacherPasswordNonEmpty = teacherPassword.length > 0;
const isIndependentStudentPasswordNonEmpty = independentStudentPassword.length > 0;

const isTeacherPasswordComplex = isPasswordStrong(teacherPassword, true) && isTeacherPasswordNonEmpty;
const isIndependentStudentPasswordComplex = isPasswordStrong(independentStudentPassword, false) && isIndependentStudentPasswordNonEmpty;

const isTeacherPasswordNotPwned = await getPwnedStatus(teacherPassword) && isTeacherPasswordComplex;
const isIndependentStudentPasswordNotPwned = await getPwnedStatus(independentStudentPassword) && isIndependentStudentPasswordComplex;

const teacherPasswordStrength = [isTeacherPasswordNonEmpty, isTeacherPasswordComplex, isTeacherPasswordNotPwned].filter(value => value).length;
const independentStudentPasswordStrength = [isIndependentStudentPasswordNonEmpty, isIndependentStudentPasswordComplex, isIndependentStudentPasswordNotPwned].filter(value => value).length;

console.log(teacherPasswordStrength, independentStudentPasswordStrength)
console.log(password_strengths)

$('#teacher-password-sign').css(
'background-color',
password_strengths[teacherPasswordStrength].colour
);
$('#teacher-password-text').html(password_strengths[teacherPasswordStrength].name);
$('#student-password-sign').css(
'background-color',
password_strengths[independentStudentPasswordStrength].colour
);
$('#student-password-text').html(password_strengths[independentStudentPasswordStrength].name);
}

teacher_password_field = $('#' + TEACHER_PASSWORD_FIELD_ID);
indep_student_password_field = $('#' + INDEP_STUDENT_PASSWORD_FIELD_ID);

setUpDynamicUpdates(teacher_password_field, true);
setUpDynamicUpdates(indep_student_password_field, false);
const getPwnedStatus = async (password) => {
const computeSHA1Hash = (password) =>
CryptoJS.SHA1(password).toString().toUpperCase();

updatePasswordStrength(true);
updatePasswordStrength(false);
});
const doesSuffixExist = (data, suffix) => data.includes(suffix);

function setUpDynamicUpdates(password_field, isTeacher) {
password_field.on('keyup', function(){
updatePasswordStrength(isTeacher)
});
password_field.on('paste', function(){
updatePasswordStrength(isTeacher)
});
password_field.on('cut', function(){
updatePasswordStrength(isTeacher)
});
}
const calculateLevenshteinDistance = async (a, b) => {
if (a.length === 0) return b.length;
if (b.length === 0) return a.length;

function updatePasswordStrength(isTeacher) {
// The reason for the timeout is that if we just got $('#...').val() we'd get the
// old value before the keypress / change. Apparently even jQuery itself implements
// things this way, so maybe there is no better workaround.
const matrix = Array.from({ length: a.length + 1 }, (_, i) => [i]);
for (let j = 1; j <= b.length; j++) matrix[0][j] = j;

setTimeout(function() {
let password;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + cost // substitution
);
}
}

if (isTeacher) {
password = $('#' + TEACHER_PASSWORD_FIELD_ID).val();
}
else {
password = $('#' + INDEP_STUDENT_PASSWORD_FIELD_ID).val();
}
return matrix[a.length][b.length];
};

let strength = 0;
if (password.length > 0) { strength++; }
if (isPasswordStrong(password, isTeacher)) { strength++; }
try {
const hashedPassword = computeSHA1Hash(password);
const prefix = hashedPassword.substring(0, 5);
const suffix = hashedPassword.substring(5);
const apiUrl = `https://api.pwnedpasswords.com/range/${prefix}`;

if ($.inArray(password, most_used_passwords) >= 0 && strength == 2) { strength = 3; }
const response = await fetch(apiUrl);

if (isTeacher) {
updatePasswordCSS('#teacher-password-sign', '#teacher-password-text', strength);
}
else {
updatePasswordCSS('#student-password-sign', '#student-password-text', strength);
}
if (!response.ok) {
return true; // ignore the check if the server is down as the popup warns
// the user that we cannot check the password
}

});
}
const data = await response.text();

function isPasswordStrong(password, isTeacher) {
if (isTeacher) {
return password.length >= 10 && !(password.search(/[A-Z]/) === -1 || password.search(/[a-z]/) === -1 || password.search(/[0-9]/) === -1 || password.search(/[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?]/) === -1)
if (doesSuffixExist(data, suffix)) {
return false;
}
else {
return password.length >= 8 && !(password.search(/[A-Z]/) === -1 || password.search(/[a-z]/) === -1 || password.search(/[0-9]/) === -1)

const similarPasswords = data
.split('\n')
.map((line) => line.split(':'))
.filter(
([hashSuffix]) => calculateLevenshteinDistance(suffix, hashSuffix) <= 2
);

if (similarPasswords.length > 0) {
similarPasswords.forEach(([hashSuffix, count]) =>
console.log(`Hash: ${hashSuffix}, Occurrences: ${count}`)
);
} else {
return true;
}
}
return true;
} catch (error) {
console.error(`Request failed with error: ${error.message}`);
return false;
}
};

function updatePasswordCSS(passwordStrengthSign, passwordStrengthText, strength) {
$(passwordStrengthSign).css('background-color', password_strengths[strength].colour);
$(passwordStrengthText).html(password_strengths[strength].name);
function isPasswordStrong(password, isTeacher) {
if (isTeacher) {
return (
password.length >= 10 &&
!(
password.search(/[A-Z]/) === -1 ||
password.search(/[a-z]/) === -1 ||
password.search(/[0-9]/) === -1 ||
password.search(/[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?]/) === -1
)
);
} else {
return (
password.length >= 8 &&
!(
password.search(/[A-Z]/) === -1 ||
password.search(/[a-z]/) === -1 ||
password.search(/[0-9]/) === -1
)
);
}
}

Loading

0 comments on commit a5803a6

Please sign in to comment.