├── django-dual-authentication ├── __init__.py └── backends.py ├── testproject ├── testproject │ ├── __init__.py │ ├── dual-authentication │ │ ├── __init_.py │ │ └── backends.py │ ├── urls.py │ ├── README.md │ ├── wsgi.py │ └── settings.py └── manage.py ├── setup.cfg ├── .gitignore ├── ChangeLog.txt ├── how-to-upload-to-pypi.md ├── .github └── FUNDING.yml ├── setup.py ├── LICENSE ├── .travis.yml └── README.rst /django-dual-authentication/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testproject/testproject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst -------------------------------------------------------------------------------- /testproject/testproject/dual-authentication/__init_.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.__pycache__/ 2 | *.pyc 3 | *.egg-info/ 4 | .idea/ 5 | dist/ 6 | testproject/db.sqlite3 -------------------------------------------------------------------------------- /testproject/testproject/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = patterns('', 5 | url(r'^admin/', include(admin.site.urls)), 6 | ) 7 | -------------------------------------------------------------------------------- /testproject/testproject/README.md: -------------------------------------------------------------------------------- 1 | Just open a terminal and run: 2 | 3 | python manage.py syncdb 4 | python manage.py runserver 5 | 6 | Now you should be able to [open admin](http://localhost:8000/admin) and login using your username or email. 7 | -------------------------------------------------------------------------------- /testproject/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /ChangeLog.txt: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | 0.5 4 | * New feature: Allow authentication using username, email, or both. 5 | * New feature: Enable or disable case sensitive for any authentication method. 6 | * Fixed: Bug where users cannot login if exists a username equal to an email address. 7 | 8 | 0.4 9 | * Fixed bug where usernames containing '@' only can login using email. 10 | * Fixed testproject. -------------------------------------------------------------------------------- /how-to-upload-to-pypi.md: -------------------------------------------------------------------------------- 1 | Follow the [official intructions](https://packaging.python.org/tutorials/packaging-projects/). 2 | 3 | TROUBLESHOOTING 4 | ==================== 5 | 6 | If you experience 403 errors, try: 7 | 8 | touch ~/.pypirc 9 | echo " 10 | [server-login] 11 | repository: https://pypi.python.org/pypi 12 | username: 13 | password: 14 | " > ~/.pypirc 15 | python3 -m twine upload dist/* 16 | -------------------------------------------------------------------------------- /testproject/testproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for testproject project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Zeioth 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | 6 | setup( 7 | name='django-dual-authentication', 8 | version='1.2.1', 9 | packages=['django-dual-authentication'], 10 | license='MIT', 11 | author='Zeioth', 12 | author_email='test@gmail.com', 13 | description='Allows authentication with either a username or an email address.', 14 | long_description='Allows authentication with either a username or an email address.', 15 | install_requires='', 16 | include_package_data=True, 17 | url='https://github.com/Zeioth/django-dual-authentication', 18 | classifiers=[ 19 | 'Development Status :: 5 - Production/Stable', 20 | 'Framework :: Django', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: MIT License', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python', 25 | 'Programming Language :: Python :: 2.7', 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Zeioth 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | - "3.5" 8 | - "3.6" 9 | env: 10 | - DJANGO=1.4 DJANGO_VERSION_MIN=1.4 DJANGO_VERSION_MAX=1.5 11 | - DJANGO=1.6 DJANGO_VERSION_MIN=1.6 DJANGO_VERSION_MAX=1.7 12 | - DJANGO=1.7 DJANGO_VERSION_MIN=1.7 DJANGO_VERSION_MAX=1.8 13 | - DJANGO=1.11 DJANGO_VERSION_MIN=1.11 DJANGO_VERSION_MAX=2.0 14 | - DJANGO=2.2 DJANGO_VERSION_MIN=2.2 DJANGO_VERSION_MAX=2.3 15 | matrix: 16 | exclude: 17 | - python: "2.6" 18 | env: DJANGO=1.7 DJANGO_VERSION_MIN=1.7 DJANGO_VERSION_MAX=1.8 19 | - python: "2.6" 20 | env: DJANGO=1.11 DJANGO_VERSION_MIN=1.11 DJANGO_VERSION_MAX=2.0 21 | - python: "2.6" 22 | env: DJANGO=2.2 DJANGO_VERSION_MIN=2.2 DJANGO_VERSION_MAX=2.3 23 | - python: "2.7" 24 | env: DJANGO=2.2 DJANGO_VERSION_MIN=2.2 DJANGO_VERSION_MAX=2.3 25 | - python: "3.3" 26 | env: DJANGO=1.4 DJANGO_VERSION_MIN=1.4 DJANGO_VERSION_MAX=1.5 27 | - python: "3.4" 28 | env: DJANGO=1.4 DJANGO_VERSION_MIN=1.4 DJANGO_VERSION_MAX=1.5 29 | - python: "3.4" 30 | env: DJANGO=1.6 DJANGO_VERSION_MIN=1.6 DJANGO_VERSION_MAX=1.7 31 | - python: "3.4" 32 | env: DJANGO=2.2 DJANGO_VERSION_MIN=2.2 DJANGO_VERSION_MAX=2.3 33 | 34 | install: 35 | - pip install -q "Django>=$DJANGO_VERSION_MIN,<$DJANGO_VERSION_MAX" 36 | - pip install . 37 | script: nosetests 38 | -------------------------------------------------------------------------------- /testproject/testproject/settings.py: -------------------------------------------------------------------------------- 1 | ######################################################### 2 | """ TEST settings for dual-authentication testproject """ 3 | ######################################################### 4 | 5 | import os 6 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 7 | 8 | 9 | SECRET_KEY = '_' 10 | DEBUG = True 11 | TEMPLATE_DEBUG = True 12 | ALLOWED_HOSTS = ['localhost'] 13 | 14 | 15 | 16 | 17 | ############################### 18 | """ APPLICATION DEFINITION """ 19 | ############################### 20 | 21 | INSTALLED_APPS = ( 22 | 'django.contrib.admin', 23 | 'django.contrib.auth', 24 | 'django.contrib.contenttypes', 25 | 'django.contrib.sessions', 26 | 'django.contrib.messages', 27 | 'django.contrib.staticfiles', 28 | ) 29 | 30 | MIDDLEWARE_CLASSES = ( 31 | 'django.contrib.sessions.middleware.SessionMiddleware', 32 | 'django.middleware.common.CommonMiddleware', 33 | 'django.middleware.csrf.CsrfViewMiddleware', 34 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 35 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 36 | 'django.contrib.messages.middleware.MessageMiddleware', 37 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 38 | ) 39 | 40 | 41 | ROOT_URLCONF = 'testproject.urls' 42 | TEMPLATE_DIRS = ((os.path.join(BASE_DIR, 'testproject/templates')),) 43 | WSGI_APPLICATION = 'testproject.wsgi.application' 44 | AUTHENTICATION_BACKENDS = ['django-dual-authentication.backends.DualAuthentication'] 45 | 46 | 47 | 48 | ############################### 49 | """ DATABASE """ 50 | ############################### 51 | 52 | DATABASES = { 53 | 'default': { 54 | 'ENGINE': 'django.db.backends.sqlite3', 55 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 56 | } 57 | } 58 | 59 | 60 | 61 | 62 | ############################### 63 | """ STATIC MEDIA """ 64 | ############################### 65 | 66 | STATIC_URL = '/static/' 67 | 68 | 69 | 70 | 71 | ############################### 72 | """ DUAL AUTHENTICATION """ 73 | ############################### 74 | 75 | # Options: username, email, both 76 | # Default: both 77 | AUTHENTICATION_METHOD = 'both' 78 | 79 | # Options: username, email, both, none 80 | # Default: both 81 | AUTHENTICATION_CASE_SENSITIVE = 'both' 82 | -------------------------------------------------------------------------------- /testproject/testproject/dual-authentication/backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.backends import ModelBackend 2 | from django.contrib.auth import get_user_model 3 | from django.conf import settings 4 | 5 | ################################### 6 | """ DEFAULT SETTINGS + ALIAS """ 7 | ################################### 8 | 9 | 10 | try: 11 | am = settings.AUTHENTICATION_METHOD 12 | except: 13 | am = 'both' 14 | try: 15 | cs = settings.AUTHENTICATION_CASE_SENSITIVE 16 | except: 17 | cs = 'both' 18 | 19 | ##################### 20 | """ EXCEPTIONS """ 21 | ##################### 22 | 23 | 24 | VALID_AM = ['username', 'email', 'both'] 25 | VALID_CS = ['username', 'email', 'both', 'none'] 26 | 27 | if (am not in VALID_AM): 28 | raise Exception("Invalid value for AUTHENTICATION_METHOD in project " 29 | "settings. Use 'username','email', or 'both'.") 30 | 31 | if (cs not in VALID_CS): 32 | raise Exception("Invalid value for AUTHENTICATION_CASE_SENSITIVE in project " 33 | "settings. Use 'username','email', 'both' or 'none'.") 34 | 35 | ############################ 36 | """ OVERRIDDEN METHODS """ 37 | ############################ 38 | 39 | 40 | class DualAuthentication(ModelBackend): 41 | """ 42 | This is a ModelBacked that allows authentication 43 | with either a username or an email address. 44 | """ 45 | 46 | def authenticate(self, username=None, password=None): 47 | UserModel = get_user_model() 48 | try: 49 | if ((am == 'email') or (am == 'both')): 50 | if ((cs == 'email') or cs == 'both'): 51 | kwargs = {'email': username} 52 | else: 53 | kwargs = {'email__iexact': username} 54 | 55 | user = UserModel.objects.get(**kwargs) 56 | else: 57 | raise 58 | except: 59 | if ((am == 'username') or (am == 'both')): 60 | if ((cs == 'username') or cs == 'both'): 61 | kwargs = {'username': username} 62 | else: 63 | kwargs = {'username__iexact': username} 64 | 65 | user = UserModel.objects.get(**kwargs) 66 | finally: 67 | try: 68 | if user.check_password(password): 69 | return user 70 | except: 71 | # Run the default password hasher once to reduce the timing 72 | # difference between an existing and a non-existing user. 73 | UserModel().set_password(password) 74 | return None 75 | 76 | def get_user(self, username): 77 | UserModel = get_user_model() 78 | try: 79 | return UserModel.objects.get(pk=username) 80 | except UserModel.DoesNotExist: 81 | return None 82 | -------------------------------------------------------------------------------- /django-dual-authentication/backends.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.contrib.auth.backends import ModelBackend 3 | from django.contrib.auth import get_user_model 4 | from django.conf import settings 5 | 6 | ################################### 7 | """ DEFAULT SETTINGS + ALIAS """ 8 | ################################### 9 | 10 | 11 | try: 12 | am = settings.AUTHENTICATION_METHOD 13 | except: 14 | am = 'both' 15 | try: 16 | cs = settings.AUTHENTICATION_CASE_SENSITIVE 17 | except: 18 | cs = 'username' 19 | 20 | ##################### 21 | """ EXCEPTIONS """ 22 | ##################### 23 | 24 | 25 | VALID_AM = ['username', 'email', 'both'] 26 | VALID_CS = ['username', 'email', 'both', 'none'] 27 | 28 | if (am not in VALID_AM): 29 | raise Exception("Invalid value for AUTHENTICATION_METHOD in project " 30 | "settings. Use 'username','email', or 'both'.") 31 | 32 | if (cs not in VALID_CS): 33 | raise Exception("Invalid value for AUTHENTICATION_CASE_SENSITIVE in project " 34 | "settings. Use 'username','email', 'both' or 'none'.") 35 | 36 | ############################ 37 | """ OVERRIDDEN METHODS """ 38 | ############################ 39 | 40 | 41 | class DualAuthentication(ModelBackend): 42 | """ 43 | This is a ModelBacked that allows authentication 44 | with either a username or an email address. 45 | """ 46 | 47 | if django.VERSION[0] == 1: 48 | def authenticate(self, username=None, password=None): 49 | return self._authenticate(username, password) 50 | else: 51 | def authenticate(self, request, username=None, password=None): 52 | return self._authenticate(username, password) 53 | 54 | def _authenticate(self, username=None, password=None): 55 | UserModel = get_user_model() 56 | try: 57 | if ((am == 'email') or (am == 'both')): 58 | if ((cs == 'email') or cs == 'both'): 59 | kwargs = {'email': username} 60 | else: 61 | kwargs = {'email__iexact': username} 62 | 63 | user = UserModel.objects.get(**kwargs) 64 | else: 65 | raise 66 | except: 67 | if ((am == 'username') or (am == 'both')): 68 | if ((cs == 'username') or cs == 'both'): 69 | kwargs = {'username': username} 70 | else: 71 | kwargs = {'username__iexact': username} 72 | 73 | user = UserModel.objects.get(**kwargs) 74 | finally: 75 | try: 76 | if user.check_password(password): 77 | return user 78 | except: 79 | # Run the default password hasher once to reduce the timing 80 | # difference between an existing and a non-existing user. 81 | UserModel().set_password(password) 82 | return None 83 | 84 | def get_user(self, username): 85 | UserModel = get_user_model() 86 | try: 87 | return UserModel.objects.get(pk=username) 88 | except UserModel.DoesNotExist: 89 | return None 90 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | `Django-dual-authentication `__ 2 | ========================== 3 | 4 | .. image:: https://img.shields.io/pypi/v/django-dual-authentication.svg 5 | :target: https://pypi.python.org/pypi/django-dual-authentication/ 6 | 7 | .. image:: https://img.shields.io/badge/license-MIT-blue.svg 8 | :target: https://github.com/Zeioth/django-dual-authentication/blob/master/LICENSE 9 | 10 | This package allows to authenticate a user with either a username an 11 | email address, or both. It overrides 12 | `Django `__ authenticate method, so it 13 | should work in almost any case of use, without touch anything else. 14 | 15 | Supported versions: 16 | 17 | - Python >= 2.7 18 | - Django >= 1.5 19 | 20 | Installation 21 | ------------ 22 | 23 | Run:: 24 | 25 | pip install django-dual-authentication 26 | 27 | Then, add this line to your settings.py:: 28 | 29 | AUTHENTICATION_BACKENDS = ['django-dual-authentication.backends.DualAuthentication'] 30 | 31 | Quick and painless, right? 32 | 33 | Settings 34 | -------- 35 | 36 | - ``AUTHENTICATION_METHOD``: You can authenticate your users by 37 | ``'username'``, ``'email'``, ``'both'``. Default: ``'both'``. 38 | - ``AUTHENTICATION_CASE_SENSITIVE``: You can choose ``'username'``, 39 | ``'email'``, ``'both'``, ``'none'``. Default: ``'username'``. 40 | 41 | Common issues 42 | ------------- 43 | 44 | We've been reported about users having problems with MySQL and 45 | dual-authentication case sensitive option. This is because `mysql is 46 | case-insensitive by 47 | default `__. 48 | So, if you need case sensitive authentication, probably you'd prefer 49 | avoid this database engine. 50 | 51 | Also, note that if you combine certain options like 52 | ``AUTHENTICATION_METHOD = 'username'`` and 53 | ``AUTHENTICATION_CASE_SENSITIVE = 'username'``, then might be a good 54 | idea check if a not case sensitive user already exists, for your 55 | registation form's username field. Other way, users having the same 56 | username with different capital letters, will not be able to login, for 57 | obvious reasons. 58 | 59 | Finally, note that ``'email'`` and ``'both'``, are meant for very specific border cases. All email adresses of the internet are case insensitive, so it's recommended to use the values ``'none'`` or ``'username'``. 60 | 61 | Testing 62 | ------- 63 | 64 | - Clone this repository. 65 | - Open testproject directory. 66 | - Run syncdb or migrate depending your django version, and runserver. 67 | - Open http://localhost:8000/admin/ and try to login. 68 | 69 | Updates 70 | ----------- 71 | 72 | - Dec 2014: Stable release 73 | - Dec 2015: All it's working fine. No changes. 74 | - Dec 2016: All it's working fine. No changes. 75 | - Dec 2017: All it's working fine. No changes. 76 | - Dec 2018: All it's working fine. No changes. 77 | - Apr 2019: Added support for django 2.0+ and Python 3.7. 78 | - Jul 2020: All it's working fine. No changes. 79 | - sep 2021: All it's working fine. No changes. 80 | - Jun 2022: All it's working fine. No changes. 81 | - Jan 2023: All it's working fine. No changes. 82 | - Jan 2024: All it's working fine. No changes. 83 | --------------------------------------------------------------------------------