diff --git a/README.md b/README.md index 395418f..d23260a 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,21 @@ Related documentation: ## Overview ## -This repository contains... - -* ungleichotpserver: the reference implementation of the ungleichotp server -* ungleichotpclient.py: a sample implementation of an ungleichotp client +This repository the reference implementation of the ungleichotp +server. -## Verifying a token using ungleichotpclient.py ## + + +## Using the ungleichotpclient ## + +``` +python manage.py ungleichotpclient create \ + --server-url https://otp.ungleich.ch/ungleichotp/ + --name admin + --realm ungleich-admin + --seed AVALIDSEED +``` Assuming you want to verify (name=ipv6only, realm=ungleich-intern, token=498593) is a @@ -40,7 +48,7 @@ UNGLEICHOTPSERVER=http://localhost:8000/ungleichotp/verify/ \ python ungleichotpclient.py -n -r ungleich --token 498593 ``` -You can also veriy using a seed: +You can also verify using a seed: ``` UNGLEICHOTPNAME=info@ungleich.ch \ diff --git a/otpauth/management/__init__.py b/otpauth/management/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/otpauth/management/commands/__init__.py b/otpauth/management/commands/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/otpauth/management/commands/ungleichotpclient.py b/otpauth/management/commands/ungleichotpclient.py new file mode 100644 index 0000000..c7f546a --- /dev/null +++ b/otpauth/management/commands/ungleichotpclient.py @@ -0,0 +1,91 @@ +from django.conf import settings +from django.core.management.base import BaseCommand + +import pyotp +import json +import urllib.request +import urllib.error +import sys + +class Command(BaseCommand): + help = 'Access ungleichotp' + + def add_arguments(self, parser): + parser.add_argument('--server-url', required=True) + + # For creating / verifying + parser.add_argument('--name') + parser.add_argument('--realm') + parser.add_argument('--token') + + # How to authenticate against ungleich-otp + parser.add_argument('--auth-name', required=True) + parser.add_argument('--auth-realm', required=True) + parser.add_argument('--auth-token') + parser.add_argument('--auth-seed') + + parser.add_argument('command', choices=['create', + 'delete', + 'list', + 'verify'], help='Action to take') + + def handle(self, *args, **options): + command_to_verb = { 'create': 'POST', + 'delete': 'DELETE', + 'list': 'GET' } + + if not options['auth_token']: + if not options['auth_seed']: + print("Either token or seed are required") + sys.exit(1) + else: + options['auth_token'] = pyotp.TOTP(options['auth_seed']).now() + + to_send = {} + + # Our credentials + to_send['auth_token'] = options['auth_token'] + to_send['auth_name'] = options['auth_name'] + to_send['auth_realm'] = options['auth_realm'] + + if options['command'] in ["create", "verify"]: + if not options['name'] or not options['realm']: + print("Need to specify --name and --realm") + sys.exit(1) + + if options['command'] == "verify" and not options['token']: + print("Need to specify --token for verify") + sys.exit(1) + + + # Client credentials to be verified + to_send['name'] = options['name'] + to_send['realm'] = options['realm'] + to_send['token'] = options['token'] + + if options['command'] == "verify": + options['server_url'] = "{}/verify".format(options['server_url']) + + print("{} {} {}".format(args, options, to_send)) + + self.rest_send(options['server_url'], to_send) + + # Logically: how can we create if we already send realm/name/token ? + # Need to use auth* (?) + + + @staticmethod + def rest_send(serverurl, to_send): + data = json.dumps(to_send).encode("utf-8") + + req = urllib.request.Request(url=serverurl, + data=data, + headers={'Content-Type': 'application/json'}, + method='POST') + + f = urllib.request.urlopen(req) + + if f.status == 200: + return True + + return False diff --git a/otpauth/models.py b/otpauth/models.py index 1b86f33..6f7cfea 100644 --- a/otpauth/models.py +++ b/otpauth/models.py @@ -16,6 +16,14 @@ class OTPSeed(AbstractUser): def __str__(self): return "'{}'@{}".format(self.name, self.realm) + @property + def auth_name(self): + print("authname: {}".format(self)) + + @auth_name.setter + def auth_name(self): + print("authname: {}".format(self)) + from otpauth.serializer import TokenSerializer class OTPAuthentication(authentication.BaseAuthentication): @@ -26,6 +34,7 @@ class OTPAuthentication(authentication.BaseAuthentication): print("trying to save... {}".format(serializer)) user, token = serializer.save() else: + print("Invalide serialize,") raise exceptions.AuthenticationFailed() return (user, token) diff --git a/otpauth/serializer.py b/otpauth/serializer.py index 08805b7..b2fbbfa 100644 --- a/otpauth/serializer.py +++ b/otpauth/serializer.py @@ -3,7 +3,7 @@ import otpauth from rest_framework import serializers, exceptions from otpauth.models import OTPSeed -# For accessing / modifying the data +# For accessing / modifying the data -- currently unused class OTPSerializer(serializers.ModelSerializer): class Meta: model = OTPSeed @@ -14,32 +14,46 @@ class OTPSerializer(serializers.ModelSerializer): validated_data['seed'] = pyotp.random_base32() return OTPSeed.objects.create(**validated_data) -# For parsing authentication class TokenSerializer(serializers.Serializer): name = serializers.CharField(max_length=128) - token = serializers.CharField(max_length=128) realm = serializers.CharField(max_length=128) - token_name = 'token' - name_name = 'name' - realm_name = 'realm' + auth_name = serializers.CharField(max_length=128) + auth_token = serializers.CharField(max_length=128) + auth_realm = serializers.CharField(max_length=128) + + def create(self, validated_data): + validated_data['seed'] = pyotp.random_base32() + return OTPSeed.objects.create(**validated_data) + + def update(self, instance, validated_data): + instance.name = validated_data.get('name', instance.name) + instance.realm = validated_data.get('realm', instance.realm) + instance.save() + + return instance def save(self): - token_in = self.validated_data.get(self.token_name) - name_in = self.validated_data.get(self.name_name) - realm_in = self.validated_data.get(self.realm_name) + name_in = self.validated_data.get("name") + realm_in = self.validated_data.get("realm") - print("auth: {} {} {} ({} {} {} -- {})".format(token_in, name_in, realm_in, self.token_name, self.name_name, self.realm_name, self.validated_data)) + auth_token = self.validated_data.get("auth_token") + auth_name = self.validated_data.get("auth_name") + auth_realm = self.validated_data.get("auth_realm") + + print("auth: {}@'{}' {} + {})".format(auth_name, auth_realm, auth_token, self.validated_data)) # 1. Verify that the connection might authenticate try: - db_instance = otpauth.models.OTPSeed.objects.get(name=name_in, realm=realm_in) + db_instance = otpauth.models.OTPSeed.objects.get(name=auth_name, realm=auth_realm) except (OTPSeed.MultipleObjectsReturned, OTPSeed.DoesNotExist): + print("does not exist") raise exceptions.AuthenticationFailed() totp = pyotp.TOTP(db_instance.seed) + print("calculated token = {}".format(totp.now())) - if not totp.verify(token_in, valid_window=3): + if not totp.verify(auth_token, valid_window=3): raise exceptions.AuthenticationFailed() return (db_instance, token_in) @@ -53,3 +67,12 @@ class VerifySerializer(TokenSerializer): token_name = 'verifytoken' name_name = 'verifyname' realm_name = 'verifyrealm' + + + # token_name = 'token' + # name_name = 'name' + # realm_name = 'realm' + + # auth_token_name = 'auth_token' + # auth_name_name = 'auth_name' + # auth_realm_name = 'auth_realm' diff --git a/otpauth/views.py b/otpauth/views.py index 707a7af..44b8c0a 100644 --- a/otpauth/views.py +++ b/otpauth/views.py @@ -5,14 +5,46 @@ from rest_framework.decorators import action from rest_framework.response import Response from django.http import JsonResponse -from otpauth.serializer import VerifySerializer, OTPSerializer +from otpauth.serializer import VerifySerializer, OTPSerializer, TokenSerializer from otpauth.models import OTPSeed +# class OTPVerifyViewSet(viewsets.ModelViewSet): +# serializer_class = OTPSerializer +# queryset = OTPSeed.objects.all() + +# @action(detail=False, methods=['post']) +# def verify(self, request): +# """the standard serializer above already verified that +# (name, realm, token) is valid. + +# Now we inspect the verify-prefixed names and return ok, +# if they also verify +# """ + +# serializer = VerifySerializer(data=request.data) +# if serializer.is_valid(): +# serializer.save() +# return Response({'status': 'OK'}) + +# return JsonResponse(serializer.errors, status=400) + class OTPVerifyViewSet(viewsets.ModelViewSet): - serializer_class = OTPSerializer + serializer_class = TokenSerializer queryset = OTPSeed.objects.all() + def list(self, request): + print("Liiiiiiiisting") + data = serializers.serialize('json', self.get_queryset()) + return HttpResponse(data, content_type="application/json") + + obj = [o.name for o in OTPSeed.objects.all()] + obj = OTPSeed.objects.all() + return Response(obj) +# return Response({'LISTstatus': 'OK'}) + + + @action(detail=False, methods=['post']) def verify(self, request): """the standard serializer above already verified that diff --git a/ungleichotpserver/settings.py b/ungleichotpserver/settings.py index 85b5e64..6828a5a 100644 --- a/ungleichotpserver/settings.py +++ b/ungleichotpserver/settings.py @@ -143,7 +143,6 @@ DATABASES = { # Static files BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -print(BASE_DIR) STATIC_ROOT = os.path.join(BASE_DIR, "static") STATIC_URL = '/static/'