Compare commits

..

No commits in common. "master" and "0.6" have entirely different histories.
master ... 0.6

29 changed files with 526 additions and 594 deletions

View file

@ -1,4 +0,0 @@
SECRET_KEY=ldskjflkdsnejnjsdnf
DEBUG=False
ENABLE_DEBUG_LOG=True
ALLOWED_HOSTS=localhost,.ungleich.ch

4
.gitignore vendored
View file

@ -1,6 +1,2 @@
.idea/
venv/
db.sqlite3
aux/
__pycache__/
static/

261
README.md
View file

@ -1,87 +1,33 @@
# ungleichotp #
# ungleich-otp
ungleich-otp is a full blown authentication and authorisation service
made for micro services.
The basic idea is that every micro service has a (long term) triple
constisting of (name, realm, seed) and creates time based tokens.
The basic idea is that every micro service has a (long term) seed and
creates time based tokens (See python pyotp, RFC4226, RFC6238).
This basically revamps Kerberos in a simple way into the web area.
ungleichotp has been created and is maintained by [ungleich](https://ungleich.ch/).
Related documentation:
* [Python pyotp](https://pyotp.readthedocs.io/)
* [RFC6238, TOTP](https://tools.ietf.org/html/rfc6238)
* [RFC4120, Kerberos](https://tools.ietf.org/html/rfc4120)
## Overview ##
This repository the reference implementation of the ungleichotp server.
## Using the ungleichotpclient ##
The client can be used to test the ungleich-otp-server.
All client commands need the parameters --auth-name and --auth-realm.
Also either --auth-seed or --auth-token needs to be specified.
```
python manage.py ungleichotpclient create \
--server-url https://otp.ungleich.ch/ungleichotp/
--auth-name admin
--auth-realm ungleich-admin
[--auth-seed THESEEDFORADMIN]
[--auth-token THECURRENTTOKEN]
```
### Creating new users
```
--name USERNAME --realm REALMOFUSER create
```
The seed is randomly created.
### Listing users
```
list
```
### Deleting users
```
--name USERNAME --realm REALMOFUSER delete
```
### Verifying a token is correct
Verify using:
```
--name USERNAME --realm REALMOFUSER --token TOKENTOBEVERIFIED verify
```
You can also verify using a seed:
```
--name USERNAME --realm REALMOFUSER --seed SEEDOFUSER verify
```
## Server Setup instructions ##
## Setup instructions ##
This is a standard django project and thus can be easily setup using
```
pip install -r requirements.txt
python manage.py createsuperuser
```
To bootstrap the application, you need your very first trusted seed to
access the application. You can generate it using
```
to be filled in
```
After that, you can run the application using
```
python manage.py runserver
```
The usual instructions on how to setup an https proxy should be followed.
## Realms ##
@ -100,30 +46,33 @@ All micro services that are trusted to authenticate another micro
service should have an entry in the ungleich-auth realm, which allows
them to verify a token of somebody else.
```
| Name | Capabilities |
|------------------+--------------------------------------------|
| ungleich-admin | authenticate, create, delete, list, update |
| ungleich-auth | authenticate, verify |
| all other realms | authenticate |
```
| ungleich-auth | authenticate |
| all other realms | NO ACCESS |
## Verify using http POST ##
## Usage: REST ##
Post a JSON object to the server at /ungleichotp/verify/ that
contains the following elements:
- Use an existing token to connect to the service
- All REST based messages: JSON
### POST: /ungleichotp/verify
Request JSON object:
```
{
auth_name: "auth-name",
auth_realm: "auth-realm",
auth_token: "current time based token",
name: "name that wants to be authenticated",
realm: "realm that wants to be authenticated",
token: "token that wants to be authenticated"
version: "1",
name: "your-name",
realm: "your-realm",
token: "current time based token",
verifyname: "name that wants to be authenticated",
verifyrealm: "realm that wants to be authenticated",
verifytoken: "token that wants to be authenticated",
}
```
@ -150,72 +99,118 @@ OR return code 403:
{"detail":"You do not have permission to perform this action."}
```
## Authorize the request ##
### GET, POST, ... /ungleichotp/
From the ungleichotp-server, you get a validated information that a
name on a realm authenticated successfully. The associated permissions
("authorization") is application specific and needs to be decided by
your application.
Standard django rest framework behaviour for updating / listing
objects.
## Limitations ##
## Usage: OTP
The seeds that you receive can be used for TOTP to authenticate your
apps.
## Database
The database saves a list of appuuids with their seeds and the user
assignments as well as whether the appuuid might use the BUS interface.
Fields:
- appuuid (a random UUID)
- appname (name chosen by the user)
- username (who this appuuid belongs to)
- seed (a random base32 string)
- trusted (boolean, whether app is allowed to use the BUS and the
verify method)
## Environment / Configuration
- POSTGRES_USERNAME
- SECRET_KEY -- random
## Random notes / stuff
django.db.backends.postgresql
django.contrib.admin
```
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydatabase',
'USER': 'mydatabaseuser',
'PASSWORD': 'mypassword',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
```
Custom auth
```
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions
class ExampleAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
username = request.META.get('X_USERNAME')
if not username:
return None
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed('No such user')
return (user, None)
```
Custom user
Dont forget to point AUTH_USER_MODEL to it. Do this before creating any migrations or running manage.py migrate for the first time.
* Name, Realm and seed are hard coded to 128 bytes length.
This can be changed, if necessary.
* Only python3 support for ungleichotp
## TODOs
- [x] (server) Serialize / input request
- [x] (server) Make seed read only
- [x] (server) Implement registering of new entries
- [x] (server) OTPSerializer: allow to read seed for admin
- [x] (server) Implement deleting entry
- [x] (server) Include verify in ModelSerializer
- [x] (server) Map name+realm == User (?)
- [x] serialize / input request
- [x] Make seed read only
- [x] Implement registering of new entries
- [x] OTPSerializer: allow to read seed for admin
- [x] Implement deleting entry
- [x] Include verify in ModelSerializer
- [x] Maybe we map name+realm == User (?)
- name == name@realm
- password is used for admin login (?)
- seed
- custom auth method
- [n] (server) Try to fake username for django based on name+realm (?)
- No need
- [n] (server) maybe overwrite get_username()
- No need
- [x] (server) Use Custom authentication - needs to have a user!
- [x] (server) Implement creating new "User" by POST / Model based
- [n] (server) Remove hard coded JSON in /verify (no - good enough for the moment)
- [x] (server) Fully rename server from ungleichotp to ungleichotpserver
- [x] (security) Ensure that only the right realms can verify
- [x] (security) Ensure that only the right realms can manage
- [ ] (doc) Add proper documentation
- [ ] (server) Add tests for verify
- [ ] (server) Add tests for authentication
- [ ] (server) move totp constants into settings
- [ ] (server) move field lengths into settings
- [ ] (server) Document how admin vs. rest works
- [ ] (server, client) Make settings adjustable by environment - k8s/docker compatible
- [ ] (server, client) Read DB from outside (?) (fallback to sqlite)
- [x] (client) Establish auth using urllib
- [ ] (client) Bootstrap Django + DRF (including an object for CRUD)
- [ ] (client) Add custom authentication / remote auth
- [ ] (client) Show case: any realm vs. specific realm
- [x] (library) Write a "client library" that can use ungleichotp
- [x] (library) extract generic parts from server
- [ ] (library) upload to pypi
- [n] try to fake username for django based on name+realm (?)
- [n] maybe overwrite get_username() (?)
- [x] Use Custom authentication - needs to have a user!
- [x] Implement creating new "User"
- by POST / Model based
- [ ] Add tests for verify
- [ ] Add tests for authentication
- [ ] Add proper documentation
- [ ] move totp constants into settings
- [ ] move field lengths into settings
- [ ] make settings adjustable by environment (?)
- [ ] Remove hard coded JSON (?)
### To document
* Login via username password interactively
* Login via name/realm/token rest
## Changelog
### 0.8, 2019-02-08
* Verify needed to call super()
### 0.6, 2018-11-18
* Reuse TokenSerializer for VerifySerializer logic
### 0.5, 2018-11-18
* Require authentication on all rest endpoints by token

2
logs/.gitignore vendored
View file

@ -1,2 +0,0 @@
*
!.gitignore

122
nameko1.py Normal file
View file

@ -0,0 +1,122 @@
from nameko.events import EventDispatcher, event_handler
from nameko.rpc import rpc
import json
from nameko.web.handlers import http
from nameko.timer import timer
from nameko.rpc import rpc, RpcProxy
import pyotp
class ServiceA:
""" Event dispatching service. """
name = "service_a"
dispatch = EventDispatcher()
@rpc
def dispatching_method(self, payload):
self.dispatch("event_type", payload)
class ServiceB:
""" Event listening service. """
name = "service_b"
@event_handler("service_a", "event_type")
def handle_event(self, payload):
print("service b received:", payload)
class HttpService:
name = "http_service"
@http('GET', '/get/<int:value>')
def get_method(self, request, value):
return json.dumps({'value': value})
@http('POST', '/post')
def do_post(self, request):
return u"received: {}".format(request.get_data(as_text=True))
@http('GET,PUT,POST,DELETE', '/multi')
def do_multi(self, request):
return request.method
class ServiceTimer:
name ="servicetimer"
dispatch = EventDispatcher()
@timer(interval=3)
def ping(self):
# method executed every second
print("pong")
self.dispatch("ping", "pong")
class LoggerService:
name = "loggerpoint"
@event_handler("servicetimer", "ping")
def handle_event(self, payload):
print("timing receive in logger: ", payload)
class OTPClient:
name = "generic-service-using-otp"
totp = pyotp.TOTP("JBSWY3DPEHPK3PXP")
otp = RpcProxy("otp")
@timer(interval=3)
def auth(self):
token = self.totp.now()
print("Verifying using {}".format(token))
print("Auth1: {}".format(self.otp.verify("app1", token)))
print("Auth-wrongapp: {}".format(self.otp.verify("app2", token)))
print("Auth-noapp: {}".format(self.otp.verify("appNOAPP", token)))
class OTPSeed:
name = "generic-service-using-otp-seed"
otp = RpcProxy("otp")
@timer(interval=10)
def auth(self):
seed = self.otp.get_seed("app1")
totp = pyotp.TOTP(seed)
token = totp.now()
res = self.otp.verify("app1", token)
print("seed / token / res {} {} {}".format(seed, token, res))
class OTPService:
name = "otp"
otp_tokens = {
'app1': 'JBSWY3DPEHPK3PXP',
'app2': 'AIEIU3IAAA'
}
@rpc
def get_seed(self, appid):
if appid in self.otp_tokens:
return self.otp_tokens[appid]
else:
return "NO SEED"
@rpc
def verify(self, appid, token):
if not appid in self.otp_tokens:
return "NO SUCH APP {}".format(appid)
totp = pyotp.TOTP(self.otp_tokens[appid])
if totp.verify(token, valid_window=3):
return "OK"
else:
return "FAIL"

View file

@ -1,97 +0,0 @@
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', help="Name to create/verify")
parser.add_argument('--realm', help="Realm for create/verify")
parser.add_argument('--token', help="Token for create/verify")
parser.add_argument('--seed', help="Seed for create/verify")
# How to authenticate against ungleich-otp
parser.add_argument('--auth-name', required=True, help="Name for auth")
parser.add_argument('--auth-realm', required=True, help="Realm for auth")
parser.add_argument('--auth-token', help="Token for auth")
parser.add_argument('--auth-seed', help="Seed for auth")
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' }
method = 'POST'
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 ["list", "get"]:
method = 'GET'
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']:
if not options['seed']:
print("Need to specify --token or --seed for verify")
sys.exit(1)
else:
options['token'] = pyotp.TOTP(options['seed']).now()
# 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'])
self.rest_send(options['server_url'], to_send, method=method)
@staticmethod
def rest_send(serverurl, to_send, method='POST'):
data = json.dumps(to_send).encode("utf-8")
req = urllib.request.Request(url=serverurl,
data=data,
headers={'Content-Type': 'application/json'},
method=method)
f = urllib.request.urlopen(req)
if f.status == 200:
print("Response: {}: {}".format(f.msg, f.read()))
return True
return False

View file

@ -1,57 +0,0 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
from rest_framework import exceptions
from rest_framework import authentication
import json
import logging
import pyotp
logger = logging.getLogger(__name__)
class OTPSeed(AbstractUser):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=128)
realm = models.CharField(max_length=128)
seed = models.CharField(max_length=128)
class Meta:
unique_together = (('name', 'realm'),)
def save(self, *args, **kwargs):
"""
inject username to ensure it stays unique / is setup at all
"""
if not self.is_superuser:
self.username = "{}@{}".format(self.name, self.realm)
else:
self.name = self.username
self.realm = "ungleich-admin"
self.seed = pyotp.random_base32()
super().save(*args, **kwargs)
def __str__(self):
return "'{}'@{} -- {}".format(self.name, self.realm, self.username)
from otpauth.serializer import TokenSerializer
class OTPAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
logger.debug("in authenticate {}".format(json.dumps(request.data)))
serializer = TokenSerializer(data=request.data)
if serializer.is_valid():
instance, token = serializer.save()
else:
logger.error("serializer is invalid")
raise exceptions.AuthenticationFailed()
# not dealing with admin realm -> can only be auth [see serializer]
if not instance.realm == "ungleich-admin":
if not request.path == "/ungleichotp/verify/":
logger.debug("request.path is not /ungleichotp/verify/")
raise exceptions.AuthenticationFailed()
logger.debug("AUTH DONE: {} - {}".format(request.path, instance))
return (instance, token)

View file

@ -1,87 +0,0 @@
import logging
import pyotp
import otpauth
from rest_framework import serializers, exceptions
from otpauth.models import OTPSeed
logger = logging.getLogger(__name__)
# For accessing / modifying the data -- currently unused
class OTPSerializer(serializers.ModelSerializer):
class Meta:
model = OTPSeed
fields = ('name', 'realm', 'seed')
read_only_fields = ('seed',)
def create(self, validated_data):
validated_data['seed'] = pyotp.random_base32()
return OTPSeed.objects.create(**validated_data)
class TokenSerializer(serializers.Serializer):
""" This class is mainly / only used for authentication"""
auth_name = serializers.CharField(max_length=128)
auth_token = serializers.CharField(max_length=128)
auth_realm = serializers.CharField(max_length=128)
token_name = 'auth_token'
name_name = 'auth_name'
realm_name = 'auth_realm'
def save(self):
auth_token = self.validated_data.get(self.token_name)
auth_name = self.validated_data.get(self.name_name)
auth_realm = self.validated_data.get(self.realm_name)
# only 2 special realms can login
# if not auth_realm in ["ungleich-admin", "ungleich-auth" ]:
# logger.error("Auth-realm is neither ungleich-admin "
# "nor ungleich-auth".format()
# )
# raise exceptions.AuthenticationFailed()
logger.debug("auth: [{}]{}@'{}' {} + {})".format(
self.name_name, auth_name, auth_realm,
auth_token, self.validated_data
))
# 1. Verify that the connection might authenticate
try:
logger.debug("Checking in db for name:{} & realm:{}".format(
auth_name, auth_realm
))
db_instance = otpauth.models.OTPSeed.objects.get(name=auth_name, realm=auth_realm)
except (OTPSeed.MultipleObjectsReturned, OTPSeed.DoesNotExist):
logger.error("OTPSeed name: {}, realm: {} does not exist".format(
auth_name, auth_realm
))
raise exceptions.AuthenticationFailed()
logger.debug("Found seed: {}".format(db_instance.seed))
totp = pyotp.TOTP(db_instance.seed)
logger.debug("calculated token = {}".format(totp.now()))
if not totp.verify(auth_token, valid_window=3):
logger.error("totp not verified")
raise exceptions.AuthenticationFailed()
return (db_instance, auth_token)
# For verifying somebody else's token
class VerifySerializer(TokenSerializer):
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'
def save(self):
auth_realm = self.validated_data.get("auth_realm")
if not auth_realm == "ungleich-auth":
logger.error("Auth-realm is not ungleich-auth")
raise exceptions.AuthenticationFailed()
# Do the authentication part
super().save()

View file

@ -1,34 +0,0 @@
from django.shortcuts import render
from rest_framework import viewsets, serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from django.http import JsonResponse
from otpauth.serializer import VerifySerializer, OTPSerializer, TokenSerializer
from otpauth.models import OTPSeed
import json
import logging
logger = logging.getLogger(__name__)
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 payload and return ok,
if they also verify
"""
logger.debug("in verify {}".format(json.dumps(request.data)))
serializer = VerifySerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response({'status': 'OK'})
return JsonResponse(serializer.errors, status=400)

22
requests/gendata.py Normal file
View file

@ -0,0 +1,22 @@
import json
import pyotp
totp=pyotp.TOTP("PZKBPTHDGSLZBKIZ")
request={}
request['name'] = "info@ungleich.ch"
request['verifyname'] = request['name']
request['token'] = totp.now()
request['verifytoken'] = request['token']
request['realm'] = "ungleich-admin"
request['verifyrealm'] = request['realm']
print(json.dumps(request))
data = json.dumps(request)
with open("outdata", "w") as fd:
fd.write(data)
fd.write("\n")

View file

@ -1,10 +1,6 @@
pyotp>=2.2.6
django==2.2.16
django>=2.1.2
djangorestframework
python-decouple>=3.1
# DB
psycopg2>=2.8,<2.9
# Recommended
markdown

View file

@ -3,7 +3,7 @@ import os
import sys
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ungleichotpserver.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ungleichotp.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

View file

@ -1,5 +1,6 @@
from django.contrib import admin
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import OTPSeed
@ -7,4 +8,5 @@ from .models import OTPSeed
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
# admin.site.register(OTPSeed, UserAdmin)
admin.site.register(OTPSeed)

View file

@ -0,0 +1,46 @@
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class OTPSeed(AbstractUser):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=128)
realm = models.CharField(max_length=128)
seed = models.CharField(max_length=128)
class Meta:
unique_together = (('name', 'realm'),)
def __str__(self):
return "'{}'@{}".format(self.name, self.realm)
# @classmethod
# def get_username(cls):
# pass
# @classmethod
# def check_password(cls, raw_password):
# """ receives a time based token"""
# pass
# @classmethod
# def has_usable_password(cls):
# pass
from rest_framework import exceptions
from rest_framework import authentication
from otpauth.models import OTPSeed
from otpauth.serializer import TokenSerializer
class OTPAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
serializer = TokenSerializer(data=request.data)
if serializer.is_valid():
print("trying to save... {}".format(serializer))
user, token = serializer.save()
else:
raise exceptions.AuthenticationFailed()
return (user, token)

View file

@ -0,0 +1,103 @@
from rest_framework import serializers, exceptions
from otpauth.models import OTPSeed
import pyotp
import otpauth
class OTPSerializer(serializers.ModelSerializer):
class Meta:
model = OTPSeed
fields = ('name', 'realm', 'seed')
read_only_fields = ('seed',)
def create(self, validated_data):
validated_data['seed'] = pyotp.random_base32()
return OTPSeed.objects.create(**validated_data)
class VerifySerializerV1(serializers.Serializer):
"""
This is the first version of the serializer that would authenticate the request
itself. This is not necessary anymore starting from version 0.5
Code to be removed prior to 1.0
"""
name = serializers.CharField(max_length=128)
token = serializers.CharField(max_length=128)
realm = serializers.CharField(max_length=128)
verifyname = serializers.CharField(max_length=128)
verifytoken = serializers.CharField(max_length=128)
verifyrealm = serializers.CharField(max_length=128)
def save(self):
token_in = self.validated_data.get('token')
name_in = self.validated_data.get('name')
realm_in = self.validated_data.get('realm')
verifytoken = self.validated_data.get('verifytoken')
verifyname = self.validated_data.get('verifyname')
verifyrealm = self.validated_data.get('verifyrealm')
# 1. Verify that the connection might authenticate
try:
db_instance = otpauth.models.OTPSeed.objects.get(name=name_in, realm=realm_in)
except (OTPSeed.MultipleObjectsReturned, OTPSeed.DoesNotExist):
raise exceptions.AuthenticationFailed()
totp = pyotp.TOTP(db_instance.seed)
if not totp.verify(token_in, valid_window=3):
raise exceptions.AuthenticationFailed()
# 2. Verify the requested data
try:
verifyinstance = otpauth.models.OTPSeed.objects.get(name=verifyname, realm=verifyrealm)
except (OTPSeed.MultipleObjectsReturned, OTPSeed.DoesNotExist):
raise exceptions.PermissionDenied()
totp = pyotp.TOTP(verifyinstance.seed)
if not totp.verify(verifytoken, valid_window=3):
raise exceptions.PermissionDenied()
print("All verified!")
return verifyinstance
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'
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)
print("auth: {} {} {} ({} {} {} -- {})".format(token_in, name_in, realm_in, self.token_name, self.name_name, self.realm_name, self.validated_data))
# 1. Verify that the connection might authenticate
try:
db_instance = otpauth.models.OTPSeed.objects.get(name=name_in, realm=realm_in)
except (OTPSeed.MultipleObjectsReturned, OTPSeed.DoesNotExist):
raise exceptions.AuthenticationFailed()
totp = pyotp.TOTP(db_instance.seed)
if not totp.verify(token_in, valid_window=3):
raise exceptions.AuthenticationFailed()
return (db_instance, token_in)
class VerifySerializer(TokenSerializer):
verifyname = serializers.CharField(max_length=128)
verifytoken = serializers.CharField(max_length=128)
verifyrealm = serializers.CharField(max_length=128)
token_name = 'verifytoken'
name_name = 'verifyname'
realm_name = 'verifyrealm'

View file

@ -0,0 +1,46 @@
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.parsers import JSONParser
from rest_framework.decorators import action
from rest_framework.response import Response
from django.http import HttpResponse, JsonResponse
from otpauth.serializer import VerifySerializer, OTPSerializer
from otpauth.models import OTPSeed
# Version 2 model - model based ++ verify action
class OTPVerifyViewSet(viewsets.ModelViewSet):
serializer_class = OTPSerializer
queryset = OTPSeed.objects.all()
@action(detail=False, methods=['post'])
def verify(self, request):
serializer = VerifySerializer(data=request.data)
if serializer.is_valid():
print(serializer)
serializer.save()
return Response({'status': 'OK'})
return JsonResponse(serializer.errors, status=400)
# Version 1 model - should be removed
class VerifyViewSet(viewsets.ViewSet):
serializer_class = VerifySerializer
def create(self, request):
data = JSONParser().parse(request)
serializer = VerifySerializer(data=data)
if serializer.is_valid():
print("is valid")
print(serializer)
#serializer.save()
return JsonResponse(serializer.data, status=201)
return JsonResponse(serializer.errors, status=400)
def get_queryset(self):
return []

View file

@ -10,8 +10,6 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.1/ref/settings/
"""
from decouple import config, Csv
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@ -22,9 +20,13 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY')
SECRET_KEY = 'h^*!&u7yaac_6t02kk4de%$aagp6_j#+_wnw3@rqu6os0tlv#r'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
@ -49,7 +51,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'ungleichotpserver.urls'
ROOT_URLCONF = 'ungleichotp.urls'
TEMPLATES = [
{
@ -67,7 +69,18 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = 'ungleichotpserver.wsgi.application'
WSGI_APPLICATION = 'ungleichotp.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation
@ -105,6 +118,7 @@ REST_FRAMEWORK = {
# Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/
@ -123,72 +137,3 @@ USE_TZ = True
# https://docs.djangoproject.com/en/2.1/howto/static-files/
STATIC_URL = '/static/'
DEBUG_DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
DEBUG = config('DEBUG', False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost', cast=Csv())
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'app',
}
}
# Static files
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATIC_URL = '/static/'
LOGGING = {
'disable_existing_loggers': False,
'version': 1,
'formatters': {
'standard': {
'format': '%(asctime)s %(levelname)s %(name)s: %(message)s'
}
},
'handlers': {
'default': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'logs/debug.log',
'maxBytes': 1024*1024*5,
'backupCount': 10,
'formatter': 'standard',
},
'console': {
'class': 'logging.StreamHandler',
},
},
}
if config('ENABLE_DEBUG_LOG', cast=bool, default=False):
loggers_dict = {}
modules_to_log_list = config(
'MODULES_TO_LOG', default='django', cast=Csv()
)
for custom_module in modules_to_log_list:
logger_item = {
custom_module: {
'handlers': ['default'],
'level': 'DEBUG',
'propagate': True
}
}
loggers_dict.update(logger_item)
LOGGING['loggers'] = loggers_dict
if "DEBUG" in os.environ:
DEBUG = True
ALLOWED_HOSTS = []
DATABASES = DEBUG_DATABASES

View file

@ -0,0 +1,35 @@
"""ungleichotp URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/2.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path
from django.conf.urls import url, include
from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets
from otpauth.models import OTPSeed
from otpauth.views import OTPVerifyViewSet, VerifyViewSet
router = routers.DefaultRouter()
router.register(r'ungleichotp', VerifyViewSet, basename='ungleichotp')
router.register(r'ungleichotpv2', OTPVerifyViewSet, basename='ungleichotpv2')
urlpatterns = [
path('admin/', admin.site.urls),
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

View file

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ungleichotpserver.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ungleichotp.settings')
application = get_wsgi_application()

View file

@ -1,81 +0,0 @@
import pyotp
import json
import urllib.request
import urllib.error
class UngleichOTPClient(object):
token_name = 'token'
name_name = 'name'
realm_name = 'realm'
def __init__(self, name, realm, seed, serverurl):
self.name = name
self.realm = realm
self.seed = seed
self.serverurl = serverurl
def verify(self, name, realm, token):
to_send = {}
# Client credentials to be verified
to_send['verifyname'] = name
to_send['verifyrealm'] = realm
to_send['verifytoken'] = token
# Our credentials
to_send['token'] = pyotp.TOTP(self.seed).now()
to_send['name'] = self.name
to_send['realm'] = self.realm
data = json.dumps(to_send).encode("utf-8")
req = urllib.request.Request(url=self.serverurl,
data=data,
headers={'Content-Type': 'application/json'},
method='POST')
f = urllib.request.urlopen(req)
if f.status == 200:
return True
return False
if __name__ == '__main__':
import argparse
import os
import sys
parser = argparse.ArgumentParser(description='ungleichotp-client')
parser.add_argument('-n', '--name', help="Name (for verification)", required=True)
parser.add_argument('-r', '--realm', help="Realm (for verification)", required=True)
g = parser.add_mutually_exclusive_group(required=True)
g.add_argument('--token', help="Token (for verification)")
g.add_argument('--seed', help="Seed (for verification)")
args = parser.parse_args(sys.argv[1:])
UNGLEICHOTP={}
for env in ['UNGLEICHOTPREALM', 'UNGLEICHOTPNAME', 'UNGLEICHOTPSEED', 'UNGLEICHOTPSERVER' ]:
if not env in os.environ:
raise Exception("Required environment variable missing: {}".format(env))
client = UngleichOTPClient(os.environ['UNGLEICHOTPNAME'],
os.environ['UNGLEICHOTPREALM'],
os.environ['UNGLEICHOTPSEED'],
os.environ['UNGLEICHOTPSERVER'])
if args.seed:
token = pyotp.TOTP(args.seed).now()
else:
token = args.token
try:
if client.verify(args.name, args.realm, token) == True:
print("Verify ok")
except urllib.error.HTTPError as e:
print("Failed to verify: {}".format(e))

View file

@ -1,14 +0,0 @@
from django.contrib import admin
from django.urls import path
from django.conf.urls import url, include
from rest_framework import routers
from otpauth.views import OTPVerifyViewSet
router = routers.DefaultRouter()
router.register(r'ungleichotp', OTPVerifyViewSet, basename='ungleichotp')
urlpatterns = [
path('admin/', admin.site.urls),
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]