Skip to content

Commit

Permalink
Merge branch 'hawi74-webauthn-clean'
Browse files Browse the repository at this point in the history
  • Loading branch information
dekoza committed Jan 12, 2022
2 parents 589aab6 + 813bbc7 commit ede06f2
Show file tree
Hide file tree
Showing 26 changed files with 1,129 additions and 19 deletions.
4 changes: 4 additions & 0 deletions .isort.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[settings]
multi_line_output=3
include_trailing_comma=True
line_length=88
11 changes: 11 additions & 0 deletions djoser/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ def __getattribute__(self, item):
"token_destroy": ["rest_framework.permissions.IsAuthenticated"],
}
),
"WEBAUTHN": ObjDict(
{
"RP_NAME": "localhost",
"RP_ID": "localhost",
"ORIGIN": "http://localhost:8000",
"CHALLENGE_LENGTH": 32,
"UKEY_LENGTH": 20,
"SIGNUP_SERIALIZER": "djoser.webauthn.serializers.WebauthnCreateUserSerializer",
"LOGIN_SERIALIZER": "djoser.webauthn.serializers.WebauthnLoginSerializer",
}
),
}

SETTINGS_TO_IMPORT = ["TOKEN_MODEL", "SOCIAL_AUTH_TOKEN_STRATEGY"]
Expand Down
36 changes: 19 additions & 17 deletions djoser/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,25 @@ def update(self, instance, validated_data):
return super().update(instance, validated_data)


class UserCreateSerializer(serializers.ModelSerializer):
class UserCreateMixin:
def create(self, validated_data):
try:
user = self.perform_create(validated_data)
except IntegrityError:
self.fail("cannot_create_user")

return user

def perform_create(self, validated_data):
with transaction.atomic():
user = User.objects.create_user(**validated_data)
if settings.SEND_ACTIVATION_EMAIL:
user.is_active = False
user.save(update_fields=["is_active"])
return user


class UserCreateSerializer(UserCreateMixin, serializers.ModelSerializer):
password = serializers.CharField(style={"input_type": "password"}, write_only=True)

default_error_messages = {
Expand Down Expand Up @@ -63,22 +81,6 @@ def validate(self, attrs):

return attrs

def create(self, validated_data):
try:
user = self.perform_create(validated_data)
except IntegrityError:
self.fail("cannot_create_user")

return user

def perform_create(self, validated_data):
with transaction.atomic():
user = User.objects.create_user(**validated_data)
if settings.SEND_ACTIVATION_EMAIL:
user.is_active = False
user.save(update_fields=["is_active"])
return user


class UserCreatePasswordRetypeSerializer(UserCreateSerializer):
default_error_messages = {
Expand Down
4 changes: 2 additions & 2 deletions djoser/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ def perform_update(self, serializer):
sender=self.__class__, user=user, request=self.request
)

# Only send activation email when email is changed
if user.email_changed:
# should we send activation email after update?
if settings.SEND_ACTIVATION_EMAIL and not user.is_active:
context = {"user": user}
to = [get_user_email(user)]
settings.EMAIL.activation(self.request, context).send(to)
Expand Down
Empty file added djoser/webauthn/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions djoser/webauthn/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class WebauthnConfig(AppConfig):
name = "djoser.webauthn"
45 changes: 45 additions & 0 deletions djoser/webauthn/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 2.2.4 on 2019-09-05 05:11

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]

operations = [
migrations.CreateModel(
name="CredentialOptions",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("challenge", models.TextField()),
("username", models.TextField(unique=True)),
("display_name", models.TextField()),
("ukey", models.TextField(unique=True)),
("credential_id", models.TextField()),
("sign_count", models.IntegerField(null=True)),
("public_key", models.TextField()),
(
"user",
models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="credential_options",
to=settings.AUTH_USER_MODEL,
),
),
],
)
]
Empty file.
18 changes: 18 additions & 0 deletions djoser/webauthn/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.conf import settings
from django.db import models


class CredentialOptions(models.Model):
challenge = models.TextField()
username = models.TextField(unique=True)
display_name = models.TextField()
ukey = models.TextField(unique=True)
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
related_name="credential_options",
null=True,
on_delete=models.CASCADE,
)
credential_id = models.TextField()
sign_count = models.IntegerField(null=True)
public_key = models.TextField()
69 changes: 69 additions & 0 deletions djoser/webauthn/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from django.contrib.auth import get_user_model
from django.db import IntegrityError, transaction
from rest_framework import serializers

from djoser.conf import settings
from djoser.serializers import UserCreateMixin

from .models import CredentialOptions
from .utils import create_challenge, create_ukey

User = get_user_model()


class WebauthnSignupSerializer(serializers.ModelSerializer):
class Meta:
model = CredentialOptions
fields = ("username", "display_name")

def create(self, validated_data):
validated_data.update(
{
"challenge": create_challenge(
length=settings.WEBAUTHN["CHALLENGE_LENGTH"]
),
"ukey": create_ukey(length=settings.WEBAUTHN["UKEY_LENGTH"]),
}
)
return super().create(validated_data)

def validate_username(self, username):
if User.objects.filter(username=username).exists():
raise serializers.ValidationError(
"User {} already exists.".format(username)
)
return username


class WebauthnCreateUserSerializer(UserCreateMixin, serializers.ModelSerializer):
class Meta:
model = User
fields = tuple(User.REQUIRED_FIELDS) + (
settings.LOGIN_FIELD,
User._meta.pk.name,
)


class WebauthnLoginSerializer(serializers.Serializer):
default_error_messages = {
"invalid_credentials": settings.CONSTANTS.messages.INVALID_CREDENTIALS_ERROR
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields[settings.LOGIN_FIELD] = serializers.CharField(required=True)

def validate_username(self, username):
try:
search_kwargs = {
settings.LOGIN_FIELD: username,
"credential_options__isnull": False,
}
self.user = user = User.objects.get(**search_kwargs)
except User.DoesNotExist:
self.fail("invalid_credentials")

if not user.is_active:
self.fail("invalid_credentials")

return username
18 changes: 18 additions & 0 deletions djoser/webauthn/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.conf.urls import url

from . import views

urlpatterns = [
url(
r"^signup_request/$",
views.SingupRequestView.as_view(),
name="webauthn_signup_request",
),
url(r"^signup/(?P<ukey>.+)/$", views.SignupView.as_view(), name="webauthn_signup"),
url(
r"^login_request/$",
views.LoginRequestView.as_view(),
name="webauthn_login_request",
),
url(r"^login/$", views.LoginView.as_view(), name="webauthn_login"),
]
13 changes: 13 additions & 0 deletions djoser/webauthn/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import string
from random import SystemRandom

random = SystemRandom()
challenge_characters = string.ascii_letters + string.digits


def create_challenge(length):
return "".join(random.choices(challenge_characters, k=length))


def create_ukey(length):
return create_challenge(length)
Loading

0 comments on commit ede06f2

Please sign in to comment.