Compare commits

...

23 Commits
0.7 ... master

Author SHA1 Message Date
PCoder 6ba71545a1 Fix psycopg2 bug
Internal Server Error: /admin/login/
Traceback (most recent call last):
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/contrib/admin/sites.py", line 399, in login
    return LoginView.as_view(**defaults)(request)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/views/generic/base.py", line 71, in view
    return self.dispatch(request, *args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/views/decorators/debug.py", line 76, in sensitive_post_parameters_wrapper
    return view(request, *args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/utils/decorators.py", line 142, in _wrapped_view
    response = view_func(request, *args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/utils/decorators.py", line 45, in _wrapper
    return bound_method(*args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/views/decorators/cache.py", line 44, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/contrib/auth/views.py", line 61, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/views/generic/base.py", line 97, in dispatch
    return handler(request, *args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/views/generic/edit.py", line 141, in post
    if form.is_valid():
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/forms/forms.py", line 185, in is_valid
    return self.is_bound and not self.errors
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/forms/forms.py", line 180, in errors
    self.full_clean()
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/forms/forms.py", line 382, in full_clean
    self._clean_form()
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/forms/forms.py", line 409, in _clean_form
    cleaned_data = self.clean()
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/contrib/auth/forms.py", line 205, in clean
    self.user_cache = authenticate(self.request, username=username, password=password)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/contrib/auth/__init__.py", line 73, in authenticate
    user = backend.authenticate(request, **credentials)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/contrib/auth/backends.py", line 20, in authenticate
    user = UserModel._default_manager.get_by_natural_key(username)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/contrib/auth/base_user.py", line 44, in get_by_natural_key
    return self.get(**{self.model.USERNAME_FIELD: username})
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/models/query.py", line 402, in get
    num = len(clone)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/models/query.py", line 256, in __len__
    self._fetch_all()
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/models/query.py", line 1242, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/models/query.py", line 55, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1175, in execute_sql
    return list(result)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1554, in cursor_iter
    for rows in iter((lambda: cursor.fetchmany(itersize)), sentinel):
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/models/sql/compiler.py", line 1554, in <lambda>
    for rows in iter((lambda: cursor.fetchmany(itersize)), sentinel):
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/utils.py", line 96, in inner
    return func(*args, **kwargs)
  File "/home/app/pyvenv/lib/python3.9/site-packages/django/db/backends/postgresql/utils.py", line 6, in utc_tzinfo_factory
    raise AssertionError("database connection isn't set to UTC")

See: https://stackoverflow.com/a/68025007
2021-11-03 19:48:37 +05:30
PCoder ae3c156df7 Update Django from 2.1.4 to 2.2.16 2020-09-02 20:48:34 +05:30
PCoder 1efe6ee078 Add missing parenthesis 2019-09-26 15:45:54 +05:30
PCoder 9703eb6538 Set name, realm and seed for superusers also 2019-09-26 15:26:10 +05:30
PCoder 273a1acf01 Set ALLOWED_HOSTS from .env 2019-09-26 15:16:07 +05:30
PCoder 3f37fe4826 Set username for non-superusers only 2019-09-26 15:10:16 +05:30
nico14571 79458d54cb Merge branch 'patch-1' into 'master'
Update README.md (Verify using http POST Section)

See merge request ungleich-public/ungleich-otp!3
2019-06-08 12:52:27 +02:00
Ahmed Bilal 0301e1a7e8 Update README.md 2019-06-08 09:36:55 +02:00
Ahmed Bilal 2f2d0c592e Update README.md 2019-06-07 20:06:17 +02:00
wcolmenares 9e3aad1316 updated readme 2019-03-13 19:56:49 -04:00
wcolmenares 7a581e8357 fix verify someone else token 2019-03-11 23:05:13 -04:00
nico14571 84afaaa56d Merge branch 'master' into 'master'
Decouple config and add logging

See merge request ungleich-public/ungleich-otp!1
2019-02-11 11:12:53 +01:00
PCoder d38b5378b0 More logging 2019-02-11 01:06:21 +01:00
PCoder 1b4107306b Add logging 2019-02-10 23:52:52 +01:00
PCoder d598b9584e Add logs directory 2019-02-10 23:37:15 +01:00
PCoder 636b3d3052 Log errors/debug messages 2019-02-10 23:33:28 +01:00
PCoder fd0f0b56bd Load configs from .env and add basic logging config 2019-02-10 23:32:59 +01:00
PCoder e45e5989db Add python-decouple requirement 2019-02-10 23:31:48 +01:00
PCoder 5890d95c59 Update .gitignore 2019-02-10 23:31:26 +01:00
PCoder 27b880ef77 Add values to .env.sample 2019-02-10 23:30:17 +01:00
PCoder 27ba06ce26 Add .env.sample 2019-02-10 23:29:53 +01:00
Nico Schottelius 1a54de525b Cleanup docs, remove debug print 2019-02-08 20:00:28 +01:00
Nico Schottelius 97b612e626 Update doc, run actual authentication on verify 2019-02-08 19:25:07 +01:00
10 changed files with 166 additions and 83 deletions

4
.env.sample Normal file
View File

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

3
.gitignore vendored
View File

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

112
README.md
View File

@ -18,63 +18,57 @@ Related documentation:
## Overview ##
This repository the reference implementation of the ungleichotp
server.
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/
--name admin
--realm ungleich-admin
--seed AVALIDSEED
--auth-name admin
--auth-realm ungleich-admin
[--auth-seed THESEEDFORADMIN]
[--auth-token THECURRENTTOKEN]
```
Assuming you want to verify
(name=ipv6only, realm=ungleich-intern, token=498593) is a
valid triple and you do have credentials to access ungleich-otp
(name=info@ungleich.ch, realm=ungleich-admin, seed=PZKBPTHDGSLZBKIZ),
then the following call will verify the token:
### Creating new users
```
UNGLEICHOTPNAME=info@ungleich.ch \
UNGLEICHOTPREALM=ungleich-admin \
UNGLEICHOTPSEED=PZKBPTHDGSLZBKIZ \
UNGLEICHOTPSERVER=http://localhost:8000/ungleichotp/verify/ \
python ungleichotpclient.py -n -r ungleich --token 498593
--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:
```
UNGLEICHOTPNAME=info@ungleich.ch \
UNGLEICHOTPREALM=ungleich-admin \
UNGLEICHOTPSEED=PZKBPTHDGSLZBKIZ \
UNGLEICHOTPSERVER=http://localhost:8000/ungleichotp/verify/ \
python ungleichotpclient.py -n -r ungleich --seed CEKXVG3235PO2HDW
```
The client requires pyotp.
## Sample 2018-12-30
create:
(venv) [23:07] line:ungleich-otp% python manage.py ungleichotpclient create --server-url http://localhost:8000/ungleichotp/ --auth-name info@ungleich.ch --auth-realm ungleich-admin --auth-seed PZKBPTHDGSLZBKIZ --name nico$(date +%s) --realm ungleich-admin
verify:
```
(venv) [23:07] line:ungleich-otp% python manage.py ungleichotpclient verify --server-url http://localhost:8000/ungleichotp/ --auth-name info@ungleich.ch --auth-realm ungleich-admin --auth-seed PZKBPTHDGSLZBKIZ --name nico1546206660 --realm ungleich-admin --seed IXTARIU4H2F574M3
```
list:
```
(venv) [23:14] line:ungleich-otp% python manage.py ungleichotpclient list --server-url http://localhost:8000/ungleichotp/ --auth-name info@ungleich.ch --auth-realm ungleich-admin --auth-seed PZKBPTHDGSLZBKIZ
--name USERNAME --realm REALMOFUSER --seed SEEDOFUSER verify
```
@ -106,13 +100,13 @@ 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 |
| all other realms | NO ACCESS |
| ungleich-auth | authenticate, verify |
| all other realms | authenticate |
```
## Verify using http POST ##
@ -124,12 +118,12 @@ Request JSON object:
```
{
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",
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"
}
```
@ -166,8 +160,8 @@ your application.
## Limitations ##
* Name, Realm and seed are hard coded to 128 bytes length. This can be
changed, if necessary.
* Name, Realm and seed are hard coded to 128 bytes length.
This can be changed, if necessary.
* Only python3 support for ungleichotp
@ -192,8 +186,8 @@ your application.
- [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
- [ ] (security) Ensure that only the right realms can verify
- [ ] (security) Ensure that only the right realms can manage
- [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
@ -206,14 +200,18 @@ your application.
- [ ] (client) Bootstrap Django + DRF (including an object for CRUD)
- [ ] (client) Add custom authentication / remote auth
- [ ] (client) Show case: any realm vs. specific realm
- [ ] (library) Write a "client library" that can use ungleichotp
- [ ] (library) extract generic parts from server
- [x] (library) Write a "client library" that can use ungleichotp
- [x] (library) extract generic parts from server
- [ ] (library) upload to pypi
## Changelog
### 0.8, 2019-02-08
* Verify needed to call super()
### 0.6, 2018-11-18
* Reuse TokenSerializer for VerifySerializer logic

2
logs/.gitignore vendored Normal file
View File

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

View File

@ -14,16 +14,16 @@ class Command(BaseCommand):
parser.add_argument('--server-url', required=True)
# For creating / verifying
parser.add_argument('--name')
parser.add_argument('--realm')
parser.add_argument('--token')
parser.add_argument('--seed')
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)
parser.add_argument('--auth-realm', required=True)
parser.add_argument('--auth-token')
parser.add_argument('--auth-seed')
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',

View File

@ -2,6 +2,11 @@ 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):
@ -17,7 +22,13 @@ class OTPSeed(AbstractUser):
"""
inject username to ensure it stays unique / is setup at all
"""
self.username = "{}@{}".format(self.name, self.realm)
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):
@ -27,19 +38,20 @@ 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():
print("trying to save... {}".format(serializer))
instance, token = serializer.save()
else:
print("Invalide serialize,")
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()
print("AUTH DONE: {} - {}".format(request.path, instance))
logger.debug("AUTH DONE: {} - {}".format(request.path, instance))
return (instance, token)

View File

@ -1,8 +1,11 @@
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:
@ -31,22 +34,34 @@ class TokenSerializer(serializers.Serializer):
auth_realm = self.validated_data.get(self.realm_name)
# only 2 special realms can login
if not auth_realm in ["ungleich-admin", "ungleich-auth" ]:
raise exceptions.AuthenticationFailed()
# if not auth_realm in ["ungleich-admin", "ungleich-auth" ]:
# logger.error("Auth-realm is neither ungleich-admin "
# "nor ungleich-auth".format()
# )
# raise exceptions.AuthenticationFailed()
print("auth: [{}]{}@'{}' {} + {})".format(self.name_name, auth_name, auth_realm, auth_token, self.validated_data))
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):
print("does not exist")
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)
print("calculated token = {}".format(totp.now()))
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)
@ -65,4 +80,8 @@ class VerifySerializer(TokenSerializer):
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

@ -7,6 +7,11 @@ 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
@ -20,7 +25,7 @@ class OTPVerifyViewSet(viewsets.ModelViewSet):
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()

View File

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

View File

@ -10,6 +10,8 @@ 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, ...)
@ -20,7 +22,7 @@ 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 = 'h^*!&u7yaac_6t02kk4de%$aagp6_j#+_wnw3@rqu6os0tlv#r'
SECRET_KEY = config('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
@ -129,10 +131,8 @@ DEBUG_DATABASES = {
}
}
DEBUG = False
ALLOWED_HOSTS = [
".ungleich.ch"
]
DEBUG = config('DEBUG', False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost', cast=Csv())
DATABASES = {
'default': {
@ -146,6 +146,45 @@ 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: