Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dependencies = [
"boto3>=1.35.97",
"dj-database-url>=2.3.0",
"dj-rest-auth[with-social]>=7.0.1",
"django-allauth>=65.1.0",
"django-allauth[mfa]>=65.1.0",
"django-cors-headers>=4.6.0",
"django-storages>=1.14.4",
"django==4.2",
Expand Down
94 changes: 91 additions & 3 deletions backend/src/apps/auth/serializers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import logging

from dj_rest_auth.registration.serializers import (
RegisterSerializer as BaseRegisterSerializer,
)
from dj_rest_auth.serializers import LoginSerializer as BaseLoginSerializer
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions, serializers
from rest_framework import exceptions
from rest_framework import serializers
from urllib.parse import quote, urlencode
from allauth.mfa.models import Authenticator
from allauth.mfa.adapter import get_adapter
from apps.auth.utils import generate_otp, convert_to_base64

from apps.users.models import User

Expand Down Expand Up @@ -70,3 +73,88 @@ def validate(self, attrs):

attrs["user"] = user
return attrs


class MFASetupSerializer(serializers.Serializer):
def create(self, validated_data):
user = self.context['request'].user
adapter = get_adapter()

existing_totp = Authenticator.objects.filter(
user=user,
type=Authenticator.Type.TOTP
).first()

if existing_totp:
raise serializers.ValidationError({
'mfa': 'Totp is already exists'
})

number = generate_otp()
secret = convert_to_base64(number)

authenticator = Authenticator.objects.create(
user=user,
type=Authenticator.Type.TOTP,
data={
'secret': adapter.encrypt(secret),
'digits': settings.MFA_TOTP_DIGITS,
'period': settings.MFA_TOTP_PERIOD,
'algorithm': 'SHA1',
'confirmed': False
}
)

totp_url = adapter.build_totp_url(user, authenticator.data['secret'])

return {
'secret': secret,
'qr_url': totp_url
}

def to_representation(self, instance):
return {
'secret': instance['secret'],
'qr_url': instance['qr_url']
}


class MFAVerifySerializer(serializers.Serializer):
code = serializers.CharField()

def validate(self, attrs):
user = self.context['request'].user
try:
authenticator = Authenticator.objects.get(
user=user,
type=Authenticator.Type.TOTP
)

adapter = get_adapter()
secret = adapter.decrypt(authenticator.data['secret'])

if secret != attrs['code']:
raise serializers.ValidationError({
'code': 'Wrong code'
})

authenticator.data['confirmed'] = True
authenticator.save()

except Authenticator.DoesNotExist:
raise serializers.ValidationError({
'mfa': 'MFA is not enabled'
})

return attrs


class MFAStatusSerializer(serializers.Serializer):
mfa_enabled = serializers.BooleanField(read_only=True)
type = serializers.CharField(read_only=True)

def to_representation(self, instance):
return {
'mfa_enabled': instance['mfa_enabled'],
'type': instance['type']
}
8 changes: 7 additions & 1 deletion backend/src/apps/auth/urls.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
from dj_rest_auth.views import LogoutView
from django.urls import include, path
from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView, TokenVerifyView

from apps.auth.views import (
FacebookConnectView,
GitHubConnectView,
LoginView,
RegisterView,
MFASetupView,
MFAStatusView,
MFAVerifyView,
)

urlpatterns = [
path("mfa/setup/", MFASetupView.as_view(), name="mfa_setup"),
path("mfa/verify/", MFAVerifyView.as_view(), name="mfa_verify"),
path("mfa/status/", MFAStatusView.as_view(), name="mfa_status"),
path("register/", RegisterView.as_view(), name="rest_register"),
path("login/", LoginView.as_view(), name="rest_login"),
path("logout/", LogoutView.as_view(), name="rest_logout"),
Expand Down
11 changes: 11 additions & 0 deletions backend/src/apps/auth/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import random
import base64


def generate_otp():
number = random.randrange(1, 1000000)
return str(number).zfill(6)

def convert_to_base64(number: str):
bytes_data = number.encode('utf-8')
return base64.b32encode(bytes_data).decode('utf-8').rstrip('=')
32 changes: 32 additions & 0 deletions backend/src/apps/auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
from dj_rest_auth.registration.views import SocialConnectView
from dj_rest_auth.views import LoginView as BaseLoginView
from drf_spectacular.utils import OpenApiExample, extend_schema
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from apps.auth.serializers import MFASetupSerializer, MFAVerifySerializer, MFAStatusSerializer
from allauth.mfa.adapter import get_adapter

@extend_schema(
tags=["auth"],
Expand Down Expand Up @@ -71,3 +76,30 @@ class GitHubConnectView(SocialConnectView):
)
class FacebookConnectView(SocialConnectView):
adapter_class = FacebookOAuth2Adapter


class MFASetupView(generics.CreateAPIView):
permission_classes = [IsAuthenticated]
serializer_class = MFASetupSerializer


class MFAVerifyView(generics.GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = MFAVerifySerializer

def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
return Response({'status': 'verified'})


class MFAStatusView(generics.RetrieveAPIView):
permission_classes = [IsAuthenticated]
serializer_class = MFAStatusSerializer

def get_object(self):
adapter = get_adapter()
return {
'mfa_enabled': adapter.is_mfa_enabled(self.request.user),
'type': 'totp'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2 on 2025-02-20 20:38

import datetime
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('threads', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='review',
name='time_spent',
field=models.TimeField(blank=True, default=datetime.time(0, 0), help_text='HH:MM:SS', null=True),
),
]
8 changes: 8 additions & 0 deletions backend/src/core/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"rest_framework_simplejwt.token_blacklist",
"allauth",
"allauth.account",
"allauth.mfa",
"allauth.socialaccount",
"allauth.socialaccount.providers.facebook",
"allauth.socialaccount.providers.github",
Expand Down Expand Up @@ -105,6 +106,9 @@
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_EMAIL_VERIFICATION = "none"
ACCOUNT_AUTHENTICATION_METHOD = "email"
MFA_ADAPTER = "allauth.mfa.adapter.DefaultMFAAdapter"
MFA_TOTP_DIGITS = 6
MFA_TOTP_PERIOD = 30

# REST AUTH
REST_AUTH = {
Expand All @@ -114,6 +118,10 @@
"LOGIN_SERIALIZER": "apps.auth.serializers.LoginSerializer",
"JWT_AUTH_COOKIE": "access-token",
"JWT_AUTH_REFRESH_COOKIE": "refresh-token",
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
}

# JWT
Expand Down
32 changes: 30 additions & 2 deletions backend/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.