From 5465d81a11d3ef4269414abc9998e17ae4c81350 Mon Sep 17 00:00:00 2001 From: haniffm Date: Wed, 29 May 2019 15:01:11 +0200 Subject: [PATCH] OTP, not working yet! This implementation is influenced by: https://tag1consulting.com/blog/building-api-django-20-part-i Created some endpoints to: 1. login 2. create totp 3. verify totp 4. disable totp 5. delete totp Main part that is still not working is that we don't want to authenticate the user if it has a otp configured but not filled in. To try the current solution: 1. Login with admin/admin 2. Enable the OTP by navigating to http://localhost:8000/otp/create/ 3. Copy the response that starts with "otpauth://" 4. Create a QR code with help of google (there are other ways) by appending the above response to: https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl= So it looks something like this: https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=otpauth://totp/admin?secret=H27UTUREIAIWDXF6FVV6X4NGNC7VFATO&algorithm=SHA1&digits=6&period=30 5. Open Google Authenticator (or some other similar tool) in your phone and scan the above generated image. You should get timed autogenerated numbers in your phone. 6. To verify, go to: http://localhost:8000/otp/verify/ and fill in the number you have in your phone a the json token like this: { "token": 123456 } If the token is correct you should get Status code 201 7. To delete the otp, go to: http://localhost:8000/otp/delete/ and POST the request. --- ESSArch_TP/config/settings.py | 4 + ESSArch_TP/config/twoFactorViews.py | 176 ++++++++++++++++++++++++++++ ESSArch_TP/config/urls.py | 7 ++ 3 files changed, 187 insertions(+) create mode 100644 ESSArch_TP/config/twoFactorViews.py diff --git a/ESSArch_TP/config/settings.py b/ESSArch_TP/config/settings.py index 62cb1763..65275dfa 100644 --- a/ESSArch_TP/config/settings.py +++ b/ESSArch_TP/config/settings.py @@ -147,6 +147,9 @@ 'ESSArch_Core.tags', 'ESSArch_Core.WorkflowEngine', 'guardian', + 'django_otp', + 'django_otp.plugins.otp_totp', + 'django_otp.plugins.otp_static', ] AUTHENTICATION_BACKENDS = [ @@ -190,6 +193,7 @@ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django_otp.middleware.OTPMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] diff --git a/ESSArch_TP/config/twoFactorViews.py b/ESSArch_TP/config/twoFactorViews.py new file mode 100644 index 00000000..d0155a61 --- /dev/null +++ b/ESSArch_TP/config/twoFactorViews.py @@ -0,0 +1,176 @@ +import django_otp +from ESSArch_Core.auth.serializers import LoginSerializer +from django_otp import devices_for_user, user_has_device +from django_otp.plugins.otp_totp.models import TOTPDevice +from rest_framework.response import Response +from rest_framework import serializers, views, permissions, status +from rest_auth.views import ( + LoginView as rest_auth_LoginView, +) + +from django_otp import _user_is_anonymous + +from django.contrib.auth import get_user_model +User = get_user_model() + + +class TwoFactorUserSerializer(serializers.ModelSerializer): + + def get_url(self, obj): + return "otp_dummy_url" + + def get_permissions(self, obj): + return obj.get_all_permissions() + + def update(self, instance, validated_data): + print("in UserLoggedInSerializer.update") + + return super().update(instance, validated_data) + + class Meta: + model = User + fields = ( + 'url', 'id', 'username', 'first_name', 'last_name', 'email', + 'is_staff', 'is_active', 'is_superuser', 'last_login', + 'date_joined', 'user_permissions', + ) + read_only_fields = ( + 'id', 'username', 'last_login', 'date_joined', 'organizations', + 'is_staff', 'is_active', 'is_superuser', + ) + + +class OTPLogin(rest_auth_LoginView): + serializer_class = LoginSerializer + + def process_login(self): + print(f"#### in process_login.....") + user = self.request.user + + if _user_is_anonymous(user): + print(" User is annonymous") + elif user.is_authenticated(): + device = get_user_totp_device(user=user, confirmed=True) + print(" User is authenticated") + if device: + print(f" User got device: '{device}', TODO: WE SHOULD HANDLE OTP here!!!") + else: + print(f" User got no device...") + else: + print(f" in else in get_response...") + + super().process_login() + + def get_response(self): + print("#### in OPTLogin.get_response") + print(f" token: '{self.token}'") + user = self.request.user + + if _user_is_anonymous(user): + print(" User is annonymous") + elif user.is_authenticated(): + device = get_user_totp_device(user=user, confirmed=True) + print(" User is authenticated") + if device: + print(f" User got device: '{device}', doing otp_login!!!") + django_otp.login(self.request, device) + else: + print(f" User got no device...") + else: + print(f" in else in get_response...") + + serializer = TwoFactorUserSerializer(instance=self.user, context={'request': self.request}) + + return Response(serializer.data) + + +# Backwards compatibility. +login = OTPLogin.as_view() + + +def get_user_totp_device(user, confirmed=None): + devices = devices_for_user(user, confirmed=confirmed) + for device in devices: + if isinstance(device, TOTPDevice): + return device + + +class TOTPVerifyView(views.APIView): + """ + Use this endpoint to verify/enable a TOTP device + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request, format=None): + user = request.user + token = request.data.get('token') + device = get_user_totp_device(user) + if device is not None and device.verify_token(token): + if not device.confirmed: + device.confirmed = True + device.save() + return Response(status=status.HTTP_201_CREATED) + return Response(status=status.HTTP_400_BAD_REQUEST) + + +class IsOtpVerified(permissions.BasePermission): + """ + If user has verified TOTP device, require TOTP OTP. + """ + message = "You do not have permission to perform this action until you verify your OTP device." + + def otp_is_verified(self, request): + user = request.user + return hasattr(user, 'otp_device') and user.is_verified() + + def has_permission(self, request, view): + if user_has_device(request.user): + return self.otp_is_verified(request) + else: + return True + + +class TOTPCreateView(views.APIView): + """ + Use this endpoint to set up a new TOTP device + """ + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, format=None): + user = request.user + device = get_user_totp_device(user) + if not device: + device = user.totpdevice_set.create(confirmed=False) + url = device.config_url + return Response(url, status=status.HTTP_201_CREATED) + + +class TOTPDeleteView(views.APIView): + """ + Use this endpoint to delete a TOTP device + """ + permission_classes = [permissions.IsAuthenticated, IsOtpVerified] + + def post(self, request, format=None): + user = request.user + devices = devices_for_user(user) + for device in devices: + device.delete() + user.save() + return Response(status=status.HTTP_200_OK) + + +# TODO: delete this class, this is only for ease the deletion of TOTP under development +class TOTPFreeDeleteView(views.APIView): + """ + Use this endpoint to delete a TOTP device + """ + permission_classes = [permissions.IsAuthenticated] + + def post(self, request, format=None): + user = request.user + devices = devices_for_user(user) + for device in devices: + device.delete() + user.save() + return Response(status=status.HTTP_200_OK) diff --git a/ESSArch_TP/config/urls.py b/ESSArch_TP/config/urls.py index 8173d710..0ddc372e 100644 --- a/ESSArch_TP/config/urls.py +++ b/ESSArch_TP/config/urls.py @@ -94,6 +94,8 @@ router.register(r'parameters', ParameterViewSet) router.register(r'paths', PathViewSet) +from .twoFactorViews import OTPLogin, TOTPVerifyView, TOTPDeleteView, TOTPCreateView, TOTPFreeDeleteView + urlpatterns = [ url(r'^', include('ESSArch_Core.frontend.urls'), name='home'), url(r'^admin/', admin.site.urls), @@ -110,6 +112,11 @@ url(r'^accounts/login/$', auth_views.LoginView.as_view()), url(r'^rest-auth/', include('ESSArch_Core.auth.urls')), url(r'^rest-auth/registration/', include('rest_auth.registration.urls')), + url(r'^otp/login/$', OTPLogin.as_view(), name='otp_login'), + url(r'^otp/create/$', TOTPCreateView.as_view(), name='otp_create'), + url(r'^otp/verify/$', TOTPVerifyView.as_view(), name='otp_verify'), + url(r'^otp/disable/$', TOTPDeleteView.as_view(), name='otp_disable'), + url(r'^otp/delete/$', TOTPFreeDeleteView.as_view(), name='otp_detele'), #TODO: delete this line, its only for development purpose ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)