Skip to content

Commit

Permalink
fix: added the backend feature of pwned passwords
Browse files Browse the repository at this point in the history
  • Loading branch information
KamilPawel committed Jun 25, 2023
1 parent 234b51f commit ffe6e1d
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 16 deletions.
2 changes: 1 addition & 1 deletion cfl_common/common/tests/utils/student.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def generate_independent_student_details():
name = "Independent Student %d" % generate_independent_student_details.next_id
email_address = "student%[email protected]" % generate_independent_student_details.next_id
username = email_address
password = "Password2"
password = "$RFVBGT%^YHNmju7"

generate_independent_student_details.next_id += 1

Expand Down
2 changes: 1 addition & 1 deletion cfl_common/common/tests/utils/teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def generate_details(**kwargs):
first_name = kwargs.get("first_name", "Test")
last_name = kwargs.get("last_name", f"Teacher {random_int}")
email_address = kwargs.get("email_address", f"testteacher{random_int}@codeforlife.com")
password = kwargs.get("password", "Password2!")
password = kwargs.get("password", "$RFVBGT%6yhn")

return first_name, last_name, email_address, password

Expand Down
39 changes: 39 additions & 0 deletions portal/helpers/password.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@
from django.core.exceptions import ValidationError


import hashlib
import requests


def is_password_pwned(password):
# Create SHA1 hash of the password
sha1_hash = hashlib.sha1(password.encode()).hexdigest()
prefix = sha1_hash[:5] # Take the first 5 characters of the hash as the prefix

# Make a request to the Pwned Passwords API
url = f"https://api.pwnedpasswords.com/range/{prefix}"
response = requests.get(url)

if response.status_code != 200:
return True # backend ignore this and frontend tells the user
# that we cannot verify this at the moment

# Check if the password's hash is found in the response body
hash_suffixes = response.text.split("\r\n")
for suffix in hash_suffixes:
if sha1_hash[5:].upper() == suffix[:35].upper():
return True
return False


class PasswordStrength(Enum):
STUDENT = auto()
INDEPENDENT = auto()
Expand All @@ -28,6 +53,11 @@ def password_test(self, password):
raise forms.ValidationError(
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."
)

elif self is PasswordStrength.INDEPENDENT:
minimum_password_length = 8
if password and not password_strength_test(
Expand All @@ -42,6 +72,10 @@ def password_test(self, password):
f"Password not strong enough, consider using at least {minimum_password_length} characters, "
"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."
)
else:
minimum_password_length = 10
if password and not password_strength_test(
Expand All @@ -56,6 +90,11 @@ def password_test(self, password):
f"Password not strong enough, consider using at least {minimum_password_length} characters, "
"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."
)

return password


Expand Down
27 changes: 22 additions & 5 deletions portal/tests/test_independent_student.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ def test_signup_common_password_fails(self):
# Assert response isn't a redirect (submit failure)
assert response.status_code == 200

response = c.post(
reverse("register"),
{
"independent_student_signup-date_of_birth_day": 7,
"independent_student_signup-date_of_birth_month": 10,
"independent_student_signup-date_of_birth_year": 1997,
"independent_student_signup-name": "Test Name",
"independent_student_signup-email": "[email protected]",
"independent_student_signup-consent_ticked": "on",
"independent_student_signup-password": "Password123$",
"independent_student_signup-confirm_password": "Password123$",
"g-recaptcha-response": "something",
},
)
assert response.status_code == 200

def test_signup_passwords_do_not_match_fails(self):
c = Client()

Expand Down Expand Up @@ -110,8 +126,8 @@ def test_signup_invalid_name_fails(self):
"independent_student_signup-name": "///",
"independent_student_signup-email": "[email protected]",
"independent_student_signup-consent_ticked": "on",
"independent_student_signup-password": "Password1!",
"independent_student_signup-confirm_password": "Password1!",
"independent_student_signup-password": "$RRFVBGT%6yhnmju7",
"independent_student_signup-confirm_password": "$RRFVBGT%6yhnmju7",
"g-recaptcha-response": "something",
},
)
Expand All @@ -131,8 +147,8 @@ def test_signup_under_13_sends_parent_email(self):
"independent_student_signup-name": "Young person",
"independent_student_signup-email": "[email protected]",
"independent_student_signup-consent_ticked": "on",
"independent_student_signup-password": "Password1!",
"independent_student_signup-confirm_password": "Password1!",
"independent_student_signup-password": "$RRFVBGT%6yhnmju7",
"independent_student_signup-confirm_password": "$RRFVBGT%6yhnmju7",
"g-recaptcha-response": "something",
},
)
Expand Down Expand Up @@ -215,8 +231,9 @@ def test_signup_duplicate_email_with_teacher(self):
page = self.go_to_homepage()
page = page.go_to_signup_page()

strong_password = "£EDCVFR$5tgbnhy6"
page = page.independent_student_signup(
"indy", teacher_email, password="Password1!", confirm_password="Password1!"
"indy", teacher_email, password=strong_password, confirm_password=strong_password
)

page.return_to_home_page()
Expand Down
2 changes: 1 addition & 1 deletion portal/tests/test_invite_teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_invite_teacher_successful(self):
invited_teacher_first_name = "Valid"
invited_teacher_last_name = "Name"
invited_teacher_email = "[email protected]"
invited_teacher_password = "Password1!"
invited_teacher_password = "$RRFVBGT%^yhnmju7"

# Invite another teacher to school and check they got an email
dashboard_url = reverse("dashboard")
Expand Down
6 changes: 4 additions & 2 deletions portal/tests/test_ratelimit.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ def test_student_login_ratelimit(self):
self._block_user(Student, student_name, klass_access_code)
assert self._is_user_blocked(Student, student_name, klass_access_code)
url = reverse_lazy("teacher_edit_student", kwargs={"pk": current_student.id})
data = {"password": "password1", "confirm_password": "password1", "set_password": ""}
strong_password = "£EDCVFR$5tgbnhy6"
data = {"password": strong_password, "confirm_password": strong_password, "set_password": ""}

c.post(url, data)
assert not self._is_user_blocked(Student, student_name, klass_access_code)
Expand Down Expand Up @@ -406,7 +407,8 @@ def test_lockout_reset_tracking(self):
c.login(username=teacher_email, password=teacher_password)

url = reverse_lazy("teacher_edit_student", kwargs={"pk": student.id})
data = {"password": "password1", "confirm_password": "password1", "set_password": ""}
strong_password = "£EDCVFR$5tgb"
data = {"password": strong_password, "confirm_password": strong_password, "set_password": ""}

response = c.post(url, data)
old_daily_activity = DailyActivity.objects.get(date=old_date)
Expand Down
8 changes: 5 additions & 3 deletions portal/tests/test_school_student.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ def test_update_password_current_password_wrong(self):
)
assert self.is_dashboard(page)

page = page.go_to_account_page().update_password_failure("NewPassword", "NewPassword", "WrongPassword")
page = page.go_to_account_page().update_password_failure(
"£EDCVFR$5tgb", "£EDCVFR$5tgb", "Wrong_123$£$3_Password"
)
assert self.is_account_page(page)
assert page.was_form_invalid("student_account_form", "Your current password was incorrect")

Expand All @@ -122,7 +124,7 @@ def test_update_password_passwords_not_match(self):
)
assert self.is_dashboard(page)

page = page.go_to_account_page().update_password_failure("NewPassword1", "OtherPassword1", student_password)
page = page.go_to_account_page().update_password_failure("£EDECVFR$5tgb", "%TGBNHY^&ujm,ki8", student_password)
assert self.is_account_page(page)
assert page.was_form_invalid("student_account_form", "Your new passwords do not match")

Expand Down Expand Up @@ -163,7 +165,7 @@ def test_update_password_success(self):
)
assert self.is_dashboard(page)

new_password = "NewPassword"
new_password = "£EDCVFR$%TGBhny6"

page = page.go_to_account_page().update_password_success(new_password, student_password)
assert is_student_details_updated_message_showing(self.selenium)
Expand Down
21 changes: 19 additions & 2 deletions portal/tests/test_teacher.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,22 @@ def test_signup_common_password_fails(self):
# Assert response isn't a redirect (submit failure)
assert response.status_code == 200

response = c.post(
reverse("register"),
{
"independent_student_signup-date_of_birth_day": 7,
"independent_student_signup-date_of_birth_month": 10,
"independent_student_signup-date_of_birth_year": 1997,
"independent_student_signup-name": "Test Name",
"independent_student_signup-email": "[email protected]",
"independent_student_signup-consent_ticked": "on",
"independent_student_signup-password": "Password123$",
"independent_student_signup-confirm_password": "Password123$",
"g-recaptcha-response": "something",
},
)
assert response.status_code == 200

def test_signup_passwords_do_not_match_fails(self):
c = Client()

Expand Down Expand Up @@ -483,7 +499,7 @@ def test_edit_details(self):
page = HomePage(self.selenium).go_to_teacher_login_page().login(email, password).open_account_tab()

page = page.change_teacher_details(
{"first_name": "Paulina", "last_name": "Koch", "current_password": "Password2!"}
{"first_name": "Paulina", "last_name": "Koch", "current_password": "$RFVBGT%6yhn"}
)
assert self.is_dashboard_page(page)
assert is_teacher_details_updated_message_showing(self.selenium)
Expand All @@ -503,8 +519,9 @@ def test_edit_details_non_admin(self):
self.selenium.get(self.live_server_url)
page = HomePage(self.selenium).go_to_teacher_login_page().login(email_2, password_2).open_account_tab()

strong_password = "$RFV`bgt%6yhn"
page = page.change_teacher_details(
{"first_name": "Florian", "last_name": "Aucomte", "current_password": "Password2!"}
{"first_name": "Florian", "last_name": "Aucomte", "current_password": password_2}
)
assert self.is_dashboard_page(page)
assert is_teacher_details_updated_message_showing(self.selenium)
Expand Down
2 changes: 1 addition & 1 deletion portal/tests/test_teacher_student.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def test_update_student_password(self):

assert page.is_student_name(name)

new_student_password = "New_password1"
new_student_password = "£EDCVFR$5tgb"

page = page.type_student_password(new_student_password)
page = page.click_set_password_button()
Expand Down

0 comments on commit ffe6e1d

Please sign in to comment.