Merge pull request #19051 from edx/cstenson/unicode_normalization

Add unicode normalization to passwords.
This commit is contained in:
Dillon-Dumesnil
2018-10-15 09:41:20 -04:00
committed by GitHub
9 changed files with 149 additions and 23 deletions

View File

@@ -35,6 +35,7 @@ from lms.djangoapps.verify_student.utils import is_verification_expiring_soon, v
from openedx.core.djangoapps.certificates.api import certificates_viewable_for_course
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.theming import helpers as theming_helpers
from openedx.core.djangoapps.user_api.config.waffle import PASSWORD_UNICODE_NORMALIZE_FLAG
from openedx.core.djangoapps.theming.helpers import get_themes
from openedx.core.djangoapps.user_authn.utils import is_safe_login_or_logout_redirect
from student.models import (
@@ -47,6 +48,7 @@ from student.models import (
email_exists_or_retired,
username_exists_or_retired
)
from util.password_policy_validators import normalize_password
# Enumeration of per-course verification statuses
@@ -412,6 +414,8 @@ def authenticate_new_user(request, username, password):
logged in until they close the browser. They can't log in again until they click
the activation link from the email.
"""
if PASSWORD_UNICODE_NORMALIZE_FLAG.is_enabled():
password = normalize_password(password)
backend = load_backend(NEW_USER_AUTH_BACKEND)
user = backend.authenticate(request=request, username=username, password=password)
user.backend = NEW_USER_AUTH_BACKEND
@@ -610,7 +614,10 @@ def do_create_account(form, custom_form=None):
email=form.cleaned_data["email"],
is_active=False
)
user.set_password(form.cleaned_data["password"])
password = form.cleaned_data["password"]
if PASSWORD_UNICODE_NORMALIZE_FLAG.is_enabled():
password = normalize_password(password)
user.set_password(password)
registration = Registration()
# TODO: Rearrange so that if part of the process fails, the whole process fails.

View File

@@ -7,7 +7,6 @@ from __future__ import unicode_literals
import logging
import unicodedata
from django.conf import settings
from django.contrib.auth.password_validation import (
get_default_password_validators,
validate_password as django_validate_password,
@@ -15,6 +14,7 @@ from django.contrib.auth.password_validation import (
)
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _, ungettext
from openedx.core.djangoapps.user_api.config.waffle import PASSWORD_UNICODE_NORMALIZE_FLAG
from six import text_type
log = logging.getLogger(__name__)
@@ -89,6 +89,14 @@ def password_validators_restrictions():
return complexity_restrictions
def normalize_password(password):
"""
Normalize all passwords to 'NFKC' across the platform to prevent mismatched hash strings when comparing entered
passwords on login. See LEARNER-4283 for more context.
"""
return unicodedata.normalize('NFKC', password)
def validate_password(password, user=None):
"""
EdX's custom password validator for passwords. This function performs the
@@ -117,6 +125,8 @@ def validate_password(password, user=None):
# no reason to get into weeds
raise ValidationError([_('Invalid password.')])
if PASSWORD_UNICODE_NORMALIZE_FLAG.is_enabled():
password = normalize_password(password)
django_validate_password(password, user)

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
"""Tests for util.password_policy_validators module."""
import mock
import unittest
@@ -10,13 +9,15 @@ from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.test.utils import override_settings
from openedx.core.djangoapps.user_api.config.waffle import PASSWORD_UNICODE_NORMALIZE_FLAG
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase
from util.password_policy_validators import (
create_validator_config, validate_password, password_validators_instruction_texts,
)
@ddt
class PasswordPolicyValidatorsTestCase(unittest.TestCase):
class PasswordPolicyValidatorsTestCase(CacheIsolationTestCase):
"""
Tests for password validator utility functions
@@ -25,7 +26,6 @@ class PasswordPolicyValidatorsTestCase(unittest.TestCase):
2) requiring multiple instances of the check (also checks proper plural message)
3) successful check
"""
def validation_errors_checker(self, password, msg, user=None):
"""
This helper function is used to check the proper error messages are
@@ -61,6 +61,20 @@ class PasswordPolicyValidatorsTestCase(unittest.TestCase):
# Test badly encoded password
self.validation_errors_checker(b'\xff\xff', 'Invalid password.')
def test_password_unicode_normalization(self):
""" Tests that validate_password normalizes passwords """
# s ̣ ̇ (s with combining dot below and combining dot above)
not_normalized_password = u'\u0073\u0323\u0307'
self.assertEqual(len(not_normalized_password), 3)
# When the flag is not set, the validation should succeed since len > 2
self.validation_errors_checker(not_normalized_password, None)
# When we normalize we expect the not_normalized password to fail
# because it should be normalized to u'\u1E69' -> ṩ
with PASSWORD_UNICODE_NORMALIZE_FLAG.override(active=True):
self.validation_errors_checker(not_normalized_password,
'This password is too short. It must contain at least 2 characters.')
@data(
([create_validator_config('util.password_policy_validators.MinimumLengthValidator', {'min_length': 2})],
'at least 2 characters.'),