Commit d6593f47 authored by Linus Lewandowski's avatar Linus Lewandowski

Add django_passwords for HTTP basic auth and password copying support.

parent b5b525ea
Pipeline #7911148 passed with stages
in 17 minutes and 45 seconds
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
UserModel = get_user_model()
class BetterAuthBackend(ModelBackend):
def authenticate(self, request=None, user=None, user_id=None, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(UserModel.USERNAME_FIELD)
# We always want to do all queries (if everything is provided) so timing attacks won't be possible.
if username:
user = UserModel._default_manager.get_by_natural_key(username)
except UserModel.DoesNotExist:
if user_id:
user = UserModel._default_manager.get(id=user_id)
except UserModel.DoesNotExist:
if user:
if user.check_password(password) and self.user_can_authenticate(user):
return user
# Run the default password hasher once to reduce the timing
# difference between an existing and a nonexistent user (#20760).
......@@ -83,12 +83,13 @@ INSTALLED_APPS = [
default_app_config = 'django_passwords.apps.PasswordsConfig'
from django.contrib import admin
from .models import *
class PasswordBackendAdmin(admin.ModelAdmin):
list_display = ('__str__', 'order', 'url', 'copy_passwords_to')
list_editable = ('order', 'url', 'copy_passwords_to'), PasswordBackendAdmin)
from django.apps import AppConfig
class PasswordsConfig(AppConfig):
name = 'django_passwords'
verbose_name = 'Password authentication'
from django.contrib.auth import get_user_model
User = get_user_model()
class Backend:
def __init__(self, url):
def check_password(self, user, password):
if user and user.has_usable_password():
return user.check_password(password)
return None
def set_password(self, user, password):
from .https import Backend
import requests
from requests.auth import HTTPBasicAuth
class Backend:
def __init__(self, url):
self.url = url
def check_password(self, user, password):
res = requests.get(self.url, auth=HTTPBasicAuth(user.username, password))
return False
return True
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from .models import PasswordBackend
User = get_user_model()
# True - allow
# False - deny (all following backends will get called with 'fakepassword', and their return values will be ignored)
# None - no opinion, continue to the next rule
# Note: We always need to execute the same full codepath, so that timing attacks won't be possible.
def _check_password(user, password):
final_result = None
for backend in PasswordBackend.objects.all():
result = backend.check_password(user, password)
if final_result == False:
result = False
if result == True:
return True, backend
if result == False:
password = 'fakepassword'
final_result = False
return False, None
def check_password(user, password):
result, backend = _check_password(user, password)
if not result:
return False
if backend.copy_passwords_to:
backend.copy_passwords_to.set_password(user, password)
return True
class DjangoBackend(ModelBackend):
def authenticate(self, request=None, user=None, user_id=None, username=None, password=None, **kwargs):
if username is None:
username = kwargs.get(User.USERNAME_FIELD)
if username is not None:
user = User._default_manager.get_by_natural_key(username)
except User.DoesNotExist:
if user_id is not None:
user = User._default_manager.get(id=user_id)
except User.DoesNotExist:
if check_password(user, password):
if user and self.user_can_authenticate(user):
return user
import django.db.models.deletion
from django.db import migrations, models
def create_default_backend(apps, schema_editor):
PasswordBackend = apps.get_model("django_passwords", "PasswordBackend")
db_alias = schema_editor.connection.alias
if not len(PasswordBackend.objects.using(db_alias).all()):
PasswordBackend(order=1, url="db://"),
class Migration(migrations.Migration):
initial = True
dependencies = [
operations = [
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order', models.IntegerField(verbose_name='order')),
('url', models.URLField(verbose_name='URL')),
('copy_passwords_to', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_passwords.PasswordBackend', verbose_name='Copy passwords to')),
'ordering': ['order'],
migrations.RunPython(create_default_backend, migrations.RunPython.noop),
from importlib import import_module
from urllib.parse import urlsplit
from django.db import models
from django.utils.translation import ugettext_lazy as _
class PasswordBackend(models.Model):
class Meta:
ordering = ['order']
order = models.IntegerField(verbose_name=_("order"))
url = models.URLField(verbose_name=_("URL"))
copy_passwords_to = models.ForeignKey('self', verbose_name=_("Copy passwords to"), blank=True, null=True)
def backend(self):
scheme = urlsplit(self.url).scheme
cls = import_module('django_passwords.backends.' + scheme).Backend
return cls(self.url)
def __str__(self):
return "{}. {}".format(self.order, self.url)
def check_password(self, user, password):
return self.backend.check_password(user, password)
def set_password(self, user, password):
return self.backend.set_password(user, password)
......@@ -9,3 +9,4 @@ find aiakos -type f -and -not -path './.git/*' -exec grep -Iq . {} \; -and -prin
isort -rc aiakos
isort -rc django_extauth
isort -rc django_passwords
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment