In between hacking commit
Trying to rip out the auth part
This commit is contained in:
parent
2de270859a
commit
1b85b28935
8 changed files with 183 additions and 21 deletions
20
README.md
20
README.md
|
@ -18,13 +18,21 @@ Related documentation:
|
||||||
|
|
||||||
## Overview ##
|
## Overview ##
|
||||||
|
|
||||||
This repository contains...
|
This repository the reference implementation of the ungleichotp
|
||||||
|
server.
|
||||||
* ungleichotpserver: the reference implementation of the ungleichotp server
|
|
||||||
* ungleichotpclient.py: a sample implementation of an ungleichotp client
|
|
||||||
|
|
||||||
|
|
||||||
## 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
|
Assuming you want to verify
|
||||||
(name=ipv6only, realm=ungleich-intern, token=498593) is a
|
(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
|
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 \
|
UNGLEICHOTPNAME=info@ungleich.ch \
|
||||||
|
|
0
otpauth/management/__init__.py
Executable file
0
otpauth/management/__init__.py
Executable file
0
otpauth/management/commands/__init__.py
Executable file
0
otpauth/management/commands/__init__.py
Executable file
91
otpauth/management/commands/ungleichotpclient.py
Normal file
91
otpauth/management/commands/ungleichotpclient.py
Normal file
|
@ -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
|
|
@ -16,6 +16,14 @@ class OTPSeed(AbstractUser):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "'{}'@{}".format(self.name, self.realm)
|
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
|
from otpauth.serializer import TokenSerializer
|
||||||
|
|
||||||
class OTPAuthentication(authentication.BaseAuthentication):
|
class OTPAuthentication(authentication.BaseAuthentication):
|
||||||
|
@ -26,6 +34,7 @@ class OTPAuthentication(authentication.BaseAuthentication):
|
||||||
print("trying to save... {}".format(serializer))
|
print("trying to save... {}".format(serializer))
|
||||||
user, token = serializer.save()
|
user, token = serializer.save()
|
||||||
else:
|
else:
|
||||||
|
print("Invalide serialize,")
|
||||||
raise exceptions.AuthenticationFailed()
|
raise exceptions.AuthenticationFailed()
|
||||||
|
|
||||||
return (user, token)
|
return (user, token)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import otpauth
|
||||||
from rest_framework import serializers, exceptions
|
from rest_framework import serializers, exceptions
|
||||||
from otpauth.models import OTPSeed
|
from otpauth.models import OTPSeed
|
||||||
|
|
||||||
# For accessing / modifying the data
|
# For accessing / modifying the data -- currently unused
|
||||||
class OTPSerializer(serializers.ModelSerializer):
|
class OTPSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OTPSeed
|
model = OTPSeed
|
||||||
|
@ -14,32 +14,46 @@ class OTPSerializer(serializers.ModelSerializer):
|
||||||
validated_data['seed'] = pyotp.random_base32()
|
validated_data['seed'] = pyotp.random_base32()
|
||||||
return OTPSeed.objects.create(**validated_data)
|
return OTPSeed.objects.create(**validated_data)
|
||||||
|
|
||||||
# For parsing authentication
|
|
||||||
class TokenSerializer(serializers.Serializer):
|
class TokenSerializer(serializers.Serializer):
|
||||||
name = serializers.CharField(max_length=128)
|
name = serializers.CharField(max_length=128)
|
||||||
token = serializers.CharField(max_length=128)
|
|
||||||
realm = serializers.CharField(max_length=128)
|
realm = serializers.CharField(max_length=128)
|
||||||
|
|
||||||
token_name = 'token'
|
auth_name = serializers.CharField(max_length=128)
|
||||||
name_name = 'name'
|
auth_token = serializers.CharField(max_length=128)
|
||||||
realm_name = 'realm'
|
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):
|
def save(self):
|
||||||
token_in = self.validated_data.get(self.token_name)
|
name_in = self.validated_data.get("name")
|
||||||
name_in = self.validated_data.get(self.name_name)
|
realm_in = self.validated_data.get("realm")
|
||||||
realm_in = self.validated_data.get(self.realm_name)
|
|
||||||
|
|
||||||
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
|
# 1. Verify that the connection might authenticate
|
||||||
try:
|
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):
|
except (OTPSeed.MultipleObjectsReturned, OTPSeed.DoesNotExist):
|
||||||
|
print("does not exist")
|
||||||
raise exceptions.AuthenticationFailed()
|
raise exceptions.AuthenticationFailed()
|
||||||
|
|
||||||
totp = pyotp.TOTP(db_instance.seed)
|
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()
|
raise exceptions.AuthenticationFailed()
|
||||||
|
|
||||||
return (db_instance, token_in)
|
return (db_instance, token_in)
|
||||||
|
@ -53,3 +67,12 @@ class VerifySerializer(TokenSerializer):
|
||||||
token_name = 'verifytoken'
|
token_name = 'verifytoken'
|
||||||
name_name = 'verifyname'
|
name_name = 'verifyname'
|
||||||
realm_name = 'verifyrealm'
|
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'
|
||||||
|
|
|
@ -5,14 +5,46 @@ from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from otpauth.serializer import VerifySerializer, OTPSerializer
|
from otpauth.serializer import VerifySerializer, OTPSerializer, TokenSerializer
|
||||||
from otpauth.models import OTPSeed
|
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):
|
class OTPVerifyViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = OTPSerializer
|
serializer_class = TokenSerializer
|
||||||
queryset = OTPSeed.objects.all()
|
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'])
|
@action(detail=False, methods=['post'])
|
||||||
def verify(self, request):
|
def verify(self, request):
|
||||||
"""the standard serializer above already verified that
|
"""the standard serializer above already verified that
|
||||||
|
|
|
@ -143,7 +143,6 @@ DATABASES = {
|
||||||
|
|
||||||
# Static files
|
# Static files
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
print(BASE_DIR)
|
|
||||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||||
STATIC_URL = '/static/'
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue