Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Handle email based forms and backends #10

Merged
merged 7 commits into from

2 participants

@shabda
Owner

No description provided.

@coveralls

Coverage Status

Coverage remained the same when pulling 412f97f on handle-email-based-forms into 8fe6726 on master.

@coveralls

Coverage Status

Coverage increased (+1.34%) when pulling c3b4474 on handle-email-based-forms into 8fe6726 on master.

@shabda shabda merged commit d2e7932 into master
@shabda shabda deleted the handle-email-based-forms branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1  .gitignore
@@ -2,4 +2,5 @@
db.sqlite3
htmlcov/
*.egg
+dist/
django_secure_login.egg-info/
View
6 .travis.yml
@@ -4,13 +4,17 @@ python:
- 2.7
- 3.3
+cache:
+ directories:
+ - $HOME/.pip-cache/
+
env:
- DJANGO=Django==1.4.2
- DJANGO=Django==1.5.5
- DJANGO=https://github.com/django/django/tarball/stable/1.6.x
install:
- - pip install --use-mirrors $DJANGO coverage coveralls pep8
+ - pip install --use-mirrors $DJANGO coverage coveralls pep8 --download-cache $HOME/.pip-cache
script: coverage run setup.py test
View
25 README.md
@@ -21,11 +21,12 @@ Settings
Features
---------
+* Works with any Backend and Form which has usename-y and password-y attributes.
* Ensure that passwords have a minimum length (default 6)
* Ensure that the password is not in the list of known weak passwords.
* Ensure username is not same as password
* Email user on a failed login attempt for them.
-* Lockout after 10 failed attemps within an hour.
+* Lockout after 10 failed attempts within an hour.
Usage
-----------
@@ -71,11 +72,31 @@ If you have an existing backend `FooBackend`, you can add SecureBackend like thi
class SecureFooLoginBackend(SecureLoginBackendMixin, FooBackend):
pass
+If this backend has `email` as an username like identifier.
+
+ class SecureFooLoginBackend(SecureLoginBackendMixin, FooBackend):
+
+ def username_fieldname(self):
+ return "email"
+
+
Secure Form
============
-Use the `SecureFormMixin` with your usual forms. The forms must have username and password fields.
+Use the `SecureFormMixin` with your usual forms. If you have an existing for `FooForm`
+
+ class SecureFooForm(SecureFormMixin, FooForm):
+ pass
+
+If this form uses email as username lke identifier
+
+ class SecureFooForm(SecureFormMixin, FooForm):
+
+ def username_fieldname(self):
+ return "email"
+
+
`SECURE_LOGIN_CHECKERS` will be tested in the the clean method.
View
27 secure_login/backends.py
@@ -2,12 +2,12 @@
from django.conf import settings
-from .utils import get_callable
+from .utils import get_callable, handle_fieldname
class SecureLoginBackendMixin(object):
- def authenticate(self, username=None, password=None, **kwargs):
+ def authenticate(self, **kwargs):
DEFAULT_CHECKERS = ["secure_login.checkers.no_weak_passwords",
"secure_login.checkers.no_short_passwords",
"secure_login.checkers.no_username_password_same"]
@@ -22,22 +22,33 @@ def authenticate(self, username=None, password=None, **kwargs):
checker_failed = False
for checker in checkers:
- if not get_callable(checker)(username, password, **kwargs):
+ checker_ = self.get_final_callable(checker)
+ if not checker_(**kwargs):
checker_failed = True
break
if checker_failed:
for callable_ in on_fail_callables:
- get_callable(callable_)(username, password, **kwargs)
+ self.get_final_callable(callable_)(**kwargs)
return None
- user = super(SecureLoginBackendMixin, self).authenticate(username,
- password,
- **kwargs)
+ user = super(SecureLoginBackendMixin, self).authenticate(**kwargs)
+
if not user: # Login failed
for callable_ in on_fail_callables:
- get_callable(callable_)(username, password, **kwargs)
+ callable_ = self.get_final_callable(callable_)
+ callable_(**kwargs)
return user
+ def username_fieldname(self):
+ return "username"
+
+ def password_fieldname(self):
+ return "password"
+
+ def get_final_callable(self, checker):
+ checker_ = get_callable(checker)
+ return handle_fieldname(self.username_fieldname(), self.password_fieldname(), checker_)
+
class SecureLoginBackend(SecureLoginBackendMixin, backends.ModelBackend):
pass
View
12 secure_login/checkers.py
@@ -9,20 +9,20 @@
weak_passwords = [el.strip() for el in open(wordlist).read().split()]
-def no_weak_passwords(username=None, password=None, **kwargs):
- if password in weak_passwords:
- return False
- return True
+def no_weak_passwords(username, password, **kwargs):
+ if password in weak_passwords:
+ return False
+ return True
-def no_short_passwords(username=None, password=None, **kwargs):
+def no_short_passwords(username, password, **kwargs):
if (len(password) <
getattr(settings, "SECURE_LOGIN_MIN_PASSWORD_LENGTH", 6)):
return False
return True
-def no_username_password_same(username=None, password=None, **kwargs):
+def no_username_password_same(username, password, **kwargs):
if username == password:
return False
return True
View
9 secure_login/forms.py
@@ -2,7 +2,7 @@
from django.contrib.auth.forms import AuthenticationForm
from django import forms
-from .utils import get_callable
+from .utils import get_callable, handle_fieldname
DEFAULT_ERROR_MESSAGE = "Please review the username and password"
@@ -17,10 +17,17 @@ def clean(self):
checkers = getattr(settings, "SECURE_LOGIN_CHECKERS", DEFAULT_CHECKERS)
for checker in checkers:
checker_ = get_callable(checker)
+ checker_ = handle_fieldname(self.username_fieldname(), self.password_fieldname(), checker_)
if not checker_(**self.cleaned_data):
raise forms.ValidationError(getattr(checker_, "error_message", DEFAULT_ERROR_MESSAGE))
return super(SecureFormMixin, self).clean()
+ def username_fieldname(self):
+ return "username"
+
+ def password_fieldname(self):
+ return "password"
+
class SecureLoginForm(SecureFormMixin, AuthenticationForm):
pass
View
50 secure_login/tests.py
@@ -8,6 +8,7 @@
from .models import FailedLogin
from .forms import SecureLoginForm, SecureFormMixin
+from .backends import SecureLoginBackendMixin
class SecureLoginBackendTest(TestCase):
@@ -89,6 +90,19 @@ def test_lockout(self):
user_ = authenticate(username=username, password=password)
self.assertFalse(user_.is_active)
+ def test_email_based_backend(self):
+
+ username = "hello"
+ password = "albatross"
+ email = "hello@example.com"
+ user = User.objects.create_user(username=username, password=password, email=email)
+
+ with self.settings(AUTHENTICATION_BACKENDS=["secure_login.tests.SecureEmailBackend"], SECURE_LOGIN_CHECKERS=["secure_login.checkers.no_weak_passwords"]):
+ self.assertEqual(authenticate(email=email, password=password), None)
+
+ with self.settings(AUTHENTICATION_BACKENDS=["secure_login.tests.SecureEmailBackend"], SECURE_LOGIN_CHECKERS=[]):
+ self.assertEqual(authenticate(email=email, password=password), user)
+
class FormsTest(TestCase):
@@ -160,3 +174,39 @@ class SecureRegisterForm(SecureFormMixin, RegiterForm):
form = SecureRegisterForm(
data={"username": "hello", "password": bad_password})
self.assertTrue(form.is_valid())
+
+ def test_email_login_form(self):
+ class EmailLoginForm(forms.Form):
+ email = forms.EmailField()
+ password = forms.CharField()
+
+ class SecureRegisterForm(SecureFormMixin, EmailLoginForm):
+ pass
+
+ def username_fieldname(self):
+ return "email"
+
+ bad_password = "albatross"
+
+ with self.settings(SECURE_LOGIN_CHECKERS=["secure_login.checkers.no_weak_passwords", ]):
+ form = SecureRegisterForm(
+ data={"email": "hello@example.com", "password": bad_password})
+ self.assertFalse(form.is_valid())
+
+ with self.settings(SECURE_LOGIN_CHECKERS=[]):
+ form = SecureRegisterForm(
+ data={"email": "hello@example.com", "password": bad_password})
+ self.assertTrue(form.is_valid())
+
+
+class EmailBackend(object):
+ def authenticate(self, email, password, **kwargs):
+ try:
+ return User.objects.get(email=email)
+ except User.DoesNotexist:
+ return None
+
+
+class SecureEmailBackend(SecureLoginBackendMixin, EmailBackend):
+ def username_fieldname(self):
+ return "email"
View
13 secure_login/utils.py
@@ -1,3 +1,6 @@
+from django.conf import settings
+
+
def get_callable(callable_str):
path = callable_str.split(".")
module_name = ".".join(path[:-1])
@@ -5,3 +8,13 @@ def get_callable(callable_str):
module = __import__(module_name, {}, {}, [callable_name])
callable_ = getattr(module, callable_name)
return callable_
+
+
+def handle_fieldname(username_field, password_field, func):
+ def deco(**kwargs):
+ username = kwargs[username_field]
+ password = kwargs[password_field]
+ del kwargs[username_field]
+ del kwargs[password_field]
+ return func(username, password, **kwargs)
+ return deco
Something went wrong with that request. Please try again.