diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index fea8fd127ae19f9925291a9236c2d57de8d4b83b..fc52c5156e43f114c2d73a36482c595877537ddc 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -333,6 +333,8 @@ assessors to edit the original submitter's work.
 LMS: Fixed a bug that caused links from forum user profile pages to
 threads to lead to 404s if the course id contained a '-' character.
 
+Studio/LMS: Add password policy enforcement to new account creation
+
 Studio/LMS: Added ability to set due date formatting through Studio's Advanced
 Settings.  The key is due_date_display_format, and the value should be a format
 supported by Python's strftime function.
diff --git a/cms/envs/aws.py b/cms/envs/aws.py
index 555fa39ec0615d48d606469821eb42a02d393777..a2c95be367f6009058078d2b66a3ad41e6c32606 100644
--- a/cms/envs/aws.py
+++ b/cms/envs/aws.py
@@ -238,3 +238,10 @@ if len(MICROSITE_CONFIGURATION.keys()) > 0:
         VIRTUAL_UNIVERSITIES,
         microsites_root=path(MICROSITE_ROOT_DIR)
     )
+
+#### PASSWORD POLICY SETTINGS #####
+PASSWORD_MIN_LENGTH = ENV_TOKENS.get("PASSWORD_MIN_LENGTH")
+PASSWORD_MAX_LENGTH = ENV_TOKENS.get("PASSWORD_MAX_LENGTH")
+PASSWORD_COMPLEXITY = ENV_TOKENS.get("PASSWORD_COMPLEXITY", {})
+PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = ENV_TOKENS.get("PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD")
+PASSWORD_DICTIONARY = ENV_TOKENS.get("PASSWORD_DICTIONARY", [])
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 4cde93669ef38c5423db1fd8f8bc5359cd4d1a7e..7417ed458992ba3a2a8079a677643bc348e15f4e 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -63,6 +63,9 @@ FEATURES = {
     # edX has explicitly added them to the course creator group.
     'ENABLE_CREATOR_GROUP': False,
 
+    # whether to use password policy enforcement or not
+    'ENFORCE_PASSWORD_POLICY': False,
+
     # If set to True, Studio won't restrict the set of advanced components
     # to just those pre-approved by edX
     'ALLOW_ALL_ADVANCED_COMPONENTS': False,
@@ -477,6 +480,14 @@ TRACKING_BACKENDS = {
     }
 }
 
+#### PASSWORD POLICY SETTINGS #####
+
+PASSWORD_MIN_LENGTH = None
+PASSWORD_MAX_LENGTH = None
+PASSWORD_COMPLEXITY = {}
+PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = None
+PASSWORD_DICTIONARY = []
+
 # We're already logging events, and we don't want to capture user
 # names/passwords.  Heartbeat events are likely not interesting.
 TRACKING_IGNORE_URL_PATTERNS = [r'^/event', r'^/login', r'^/heartbeat']
diff --git a/cms/urls.py b/cms/urls.py
index 8b6b18ef71bf59860bab9880f181ec8dd08477c5..d21f0a22227b404f928bce6352c53e4ecd9ae398 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -42,7 +42,7 @@ urlpatterns = patterns('',  # nopep8
 urlpatterns += patterns(
     '',
 
-    url(r'^create_account$', 'student.views.create_account'),
+    url(r'^create_account$', 'student.views.create_account', name='create_account'),
     url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name='activate'),
 
     # ajax view that actually does the work
diff --git a/common/djangoapps/student/tests/test_password_policy.py b/common/djangoapps/student/tests/test_password_policy.py
new file mode 100644
index 0000000000000000000000000000000000000000..eaf296f7c278ba9733e7a90c82102bac95e0d9fc
--- /dev/null
+++ b/common/djangoapps/student/tests/test_password_policy.py
@@ -0,0 +1,239 @@
+# -*- coding: utf-8 -*-
+"""
+This test file will verify proper password policy enforcement, which is an option feature
+"""
+import json
+import uuid
+
+from django.test import TestCase
+from django.core.urlresolvers import reverse
+from mock import patch
+from django.test.utils import override_settings
+
+
+@patch.dict("django.conf.settings.FEATURES", {'ENFORCE_PASSWORD_POLICY': True})
+class TestPasswordPolicy(TestCase):
+    """
+    Go through some password policy tests to make sure things are properly working
+    """
+    def setUp(self):
+        super(TestPasswordPolicy, self).setUp()
+        self.url = reverse('create_account')
+        self.url_params = {
+            'username': 'foo_bar' + uuid.uuid4().hex,
+            'email': 'foo' + uuid.uuid4().hex + '@bar.com',
+            'name': 'username',
+            'terms_of_service': 'true',
+            'honor_code': 'true',
+        }
+
+    @override_settings(PASSWORD_MIN_LENGTH=6)
+    def test_password_length_too_short(self):
+        self.url_params['password'] = 'aaa'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Invalid Length (must be 6 characters or more)",
+        )
+
+    @override_settings(PASSWORD_MIN_LENGTH=6)
+    def test_password_length_long_enough(self):
+        self.url_params['password'] = 'ThisIsALongerPassword'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
+
+    @override_settings(PASSWORD_MAX_LENGTH=12)
+    def test_password_length_too_long(self):
+        self.url_params['password'] = 'ThisPasswordIsWayTooLong'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Invalid Length (must be 12 characters or less)",
+        )
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'UPPER': 3})
+    def test_password_not_enough_uppercase(self):
+        self.url_params['password'] = 'thisshouldfail'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Must be more complex (must contain 3 or more uppercase characters)",
+        )
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'UPPER': 3})
+    def test_password_enough_uppercase(self):
+        self.url_params['password'] = 'ThisShouldPass'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'LOWER': 3})
+    def test_password_not_enough_lowercase(self):
+        self.url_params['password'] = 'THISSHOULDFAIL'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Must be more complex (must contain 3 or more lowercase characters)",
+        )
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'LOWER': 3})
+    def test_password_not_enough_lowercase(self):
+        self.url_params['password'] = 'ThisShouldPass'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'DIGITS': 3})
+    def test_not_enough_digits(self):
+        self.url_params['password'] = 'thishasnodigits'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Must be more complex (must contain 3 or more digits)",
+        )
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'DIGITS': 3})
+    def test_enough_digits(self):
+        self.url_params['password'] = 'Th1sSh0uldPa88'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'PUNCTUATION': 3})
+    def test_not_enough_punctuations(self):
+        self.url_params['password'] = 'thisshouldfail'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Must be more complex (must contain 3 or more punctuation characters)",
+        )
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'PUNCTUATION': 3})
+    def test_enough_punctuations(self):
+        self.url_params['password'] = 'Th!sSh.uldPa$*'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'WORDS': 3})
+    def test_not_enough_words(self):
+        self.url_params['password'] = 'thisshouldfail'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Must be more complex (must contain 3 or more unique words)",
+        )
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'WORDS': 3})
+    def test_enough_wordss(self):
+        self.url_params['password'] = u'this should pass'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {
+        'PUNCTUATION': 3,
+        'WORDS': 3,
+        'DIGITS': 3,
+        'LOWER': 3,
+        'UPPER': 3,
+    })
+    def test_multiple_errors_fail(self):
+        self.url_params['password'] = 'thisshouldfail'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        errstring = ("Password: Must be more complex ("
+            "must contain 3 or more uppercase characters, "
+            "must contain 3 or more digits, "
+            "must contain 3 or more punctuation characters, "
+            "must contain 3 or more unique words"
+            ")")
+        self.assertEqual(obj['value'], errstring)
+
+    @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {
+        'PUNCTUATION': 3,
+        'WORDS': 3,
+        'DIGITS': 3,
+        'LOWER': 3,
+        'UPPER': 3,
+    })
+    def test_multiple_errors_pass(self):
+        self.url_params['password'] = u'tH1s Sh0u!d P3#$'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
+
+    @override_settings(PASSWORD_DICTIONARY=['foo', 'bar'])
+    @override_settings(PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD=1)
+    def test_dictionary_similarity_fail1(self):
+        self.url_params['password'] = 'foo'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Too similar to a restricted dictionary word.",
+        )
+
+    @override_settings(PASSWORD_DICTIONARY=['foo', 'bar'])
+    @override_settings(PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD=1)
+    def test_dictionary_similarity_fail2(self):
+        self.url_params['password'] = 'bar'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Too similar to a restricted dictionary word.",
+        )
+
+    @override_settings(PASSWORD_DICTIONARY=['foo', 'bar'])
+    @override_settings(PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD=1)
+    def test_dictionary_similarity_fail3(self):
+        self.url_params['password'] = 'fo0'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 400)
+        obj = json.loads(response.content)
+        self.assertEqual(
+            obj['value'],
+            "Password: Too similar to a restricted dictionary word.",
+        )
+
+    @override_settings(PASSWORD_DICTIONARY=['foo', 'bar'])
+    @override_settings(PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD=1)
+    def test_dictionary_similarity_pass(self):
+        self.url_params['password'] = 'this_is_ok'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
+
+    def test_with_unicode(self):
+        self.url_params['password'] = u'四節比分和七年前'
+        response = self.client.post(self.url, self.url_params)
+        self.assertEqual(response.status_code, 200)
+        obj = json.loads(response.content)
+        self.assertTrue(obj['success'])
diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index b745e2da671cffac9e2846741bf035c9decf5b77..007b894900da0b4f1fa2f1d20b85f930beb12efb 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -73,6 +73,11 @@ from util.json_request import JsonResponse
 
 from microsite_configuration.middleware import MicrositeConfiguration
 
+from util.password_policy_validators import (
+    validate_password_length, validate_password_complexity,
+    validate_password_dictionary
+)
+
 log = logging.getLogger("edx.student")
 AUDIT_LOG = logging.getLogger("audit")
 
@@ -973,6 +978,19 @@ def create_account(request, post_override=None):
         js['field'] = 'username'
         return JsonResponse(js, status=400)
 
+    # enforce password complexity as an optional feature
+    if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
+        try:
+            password = post_vars['password']
+
+            validate_password_length(password)
+            validate_password_complexity(password)
+            validate_password_dictionary(password)
+        except ValidationError, err:
+            js['value'] = _('Password: ') + '; '.join(err.messages)
+            js['field'] = 'password'
+            return JsonResponse(js, status=400)
+
     # Ok, looks like everything is legit.  Create the account.
     ret = _do_create_account(post_vars)
     if isinstance(ret, HttpResponse):  # if there was an error then return that
diff --git a/common/djangoapps/util/password_policy_validators.py b/common/djangoapps/util/password_policy_validators.py
new file mode 100644
index 0000000000000000000000000000000000000000..987ec30a0294f61b286ba4a413b51e9a312ec245
--- /dev/null
+++ b/common/djangoapps/util/password_policy_validators.py
@@ -0,0 +1,92 @@
+# pylint: disable=E1101
+"""
+This file exposes a number of password complexity validators which can be optionally added to
+account creation
+
+This file was inspired by the django-passwords project at https://github.com/dstufft/django-passwords
+authored by dstufft (https://github.com/dstufft)
+"""
+from __future__ import division
+import string  # pylint: disable=W0402
+
+from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+
+import nltk
+
+
+def validate_password_length(value):
+    """
+    Validator that enforces minimum length of a password
+    """
+    message = _("Invalid Length ({0})")
+    code = "length"
+
+    min_length = getattr(settings, 'PASSWORD_MIN_LENGTH', None)
+    max_length = getattr(settings, 'PASSWORD_MAX_LENGTH', None)
+
+    if min_length and len(value) < min_length:
+        raise ValidationError(message.format(_("must be {0} characters or more").format(min_length)), code=code)
+    elif max_length and len(value) > max_length:
+        raise ValidationError(message.format(_("must be {0} characters or less").format(max_length)), code=code)
+
+
+def validate_password_complexity(value):
+    """
+    Validator that enforces minimum complexity
+    """
+    message = _("Must be more complex ({0})")
+    code = "complexity"
+
+    complexities = getattr(settings, "PASSWORD_COMPLEXITY", None)
+
+    if complexities is None:
+        return
+
+    uppercase, lowercase, digits, non_ascii, punctuation = set(), set(), set(), set(), set()
+
+    for character in value:
+        if character.isupper():
+            uppercase.add(character)
+        elif character.islower():
+            lowercase.add(character)
+        elif character.isdigit():
+            digits.add(character)
+        elif character in string.punctuation:
+            punctuation.add(character)
+        else:
+            non_ascii.add(character)
+
+    words = set(value.split())
+
+    errors = []
+    if len(uppercase) < complexities.get("UPPER", 0):
+        errors.append(_("must contain {0} or more uppercase characters").format(complexities["UPPER"]))
+    if len(lowercase) < complexities.get("LOWER", 0):
+        errors.append(_("must contain {0} or more lowercase characters").format(complexities["LOWER"]))
+    if len(digits) < complexities.get("DIGITS", 0):
+        errors.append(_("must contain {0} or more digits").format(complexities["DIGITS"]))
+    if len(punctuation) < complexities.get("PUNCTUATION", 0):
+        errors.append(_("must contain {0} or more punctuation characters").format(complexities["PUNCTUATION"]))
+    if len(non_ascii) < complexities.get("NON ASCII", 0):
+        errors.append(_("must contain {0} or more non ascii characters").format(complexities["NON ASCII"]))
+    if len(words) < complexities.get("WORDS", 0):
+        errors.append(_("must contain {0} or more unique words").format(complexities["WORDS"]))
+
+    if errors:
+        raise ValidationError(message.format(u', '.join(errors)), code=code)
+
+
+def validate_password_dictionary(value):
+    """
+    Insures that the password is not too similar to a defined set of dictionary words
+    """
+    password_max_edit_distance = getattr(settings, "PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD", None)
+    password_dictionary = getattr(settings, "PASSWORD_DICTIONARY", None)
+
+    if password_max_edit_distance and password_dictionary:
+        for word in password_dictionary:
+            distance = nltk.metrics.distance.edit_distance(value, word)
+            if distance <= password_max_edit_distance:
+                raise ValidationError(_("Too similar to a restricted dictionary word."), code="dictionary_word")
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index 10ba3d70bb9acf6ffe39ead82f293eca0325f687..f759f9173d455be0617b9bea76b8bbd13483d124 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -357,3 +357,10 @@ if MICROSITE_CONFIGURATION:
         VIRTUAL_UNIVERSITIES,
         microsites_root=path(MICROSITE_ROOT_DIR)
     )
+
+#### PASSWORD POLICY SETTINGS #####
+PASSWORD_MIN_LENGTH = ENV_TOKENS.get("PASSWORD_MIN_LENGTH")
+PASSWORD_MAX_LENGTH = ENV_TOKENS.get("PASSWORD_MAX_LENGTH")
+PASSWORD_COMPLEXITY = ENV_TOKENS.get("PASSWORD_COMPLEXITY", {})
+PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = ENV_TOKENS.get("PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD")
+PASSWORD_DICTIONARY = ENV_TOKENS.get("PASSWORD_DICTIONARY", [])
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 000b7c87a59e38615dfb8556b5cc44d937b562b2..24e6d26936a87ad3545640023b118506dd25e006 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -203,6 +203,9 @@ FEATURES = {
     # grades CSV files to S3 and give links for downloads.
     'ENABLE_S3_GRADE_DOWNLOADS': False,
 
+    # whether to use password policy enforcement or not
+    'ENFORCE_PASSWORD_POLICY': False,
+
     # Give course staff unrestricted access to grade downloads (if set to False,
     # only edX superusers can perform the downloads)
     'ALLOW_COURSE_STAFF_GRADE_DOWNLOADS': False,
@@ -1188,6 +1191,14 @@ GRADES_DOWNLOAD = {
     'ROOT_PATH': '/tmp/edx-s3/grades',
 }
 
+#### PASSWORD POLICY SETTINGS #####
+
+PASSWORD_MIN_LENGTH = None
+PASSWORD_MAX_LENGTH = None
+PASSWORD_COMPLEXITY = {}
+PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = None
+PASSWORD_DICTIONARY = []
+
 ##################### LinkedIn #####################
 INSTALLED_APPS += ('django_openid_auth',)