Compare commits
65 commits
Author | SHA1 | Date | |
---|---|---|---|
|
6ba71545a1 | ||
|
ae3c156df7 | ||
|
1efe6ee078 | ||
|
9703eb6538 | ||
|
273a1acf01 | ||
|
3f37fe4826 | ||
79458d54cb | |||
|
0301e1a7e8 | ||
|
2f2d0c592e | ||
9e3aad1316 | |||
7a581e8357 | |||
84afaaa56d | |||
|
d38b5378b0 | ||
|
1b4107306b | ||
|
d598b9584e | ||
|
636b3d3052 | ||
|
fd0f0b56bd | ||
|
e45e5989db | ||
|
5890d95c59 | ||
|
27b880ef77 | ||
|
27ba06ce26 | ||
|
1a54de525b | ||
|
97b612e626 | ||
|
8bd256a1d7 | ||
|
0eb09c31d8 | ||
|
1e2d834c59 | ||
|
7ab29862f6 | ||
|
1b85b28935 | ||
|
2de270859a | ||
|
952ff50cbb | ||
|
11f3c5bcd9 | ||
|
6012eab88d | ||
|
d9ee4ffc80 | ||
|
dabe6a08ac | ||
|
d969399423 | ||
|
fc9e14dd5d | ||
|
6544fccb9a | ||
|
f157cf2539 | ||
|
6377187004 | ||
|
8636d3f81a | ||
|
45394fa59c | ||
|
11ab190ebc | ||
|
78de133e16 | ||
|
2e228f3a0d | ||
|
963585806a | ||
|
3ada914040 | ||
|
d53b980ebf | ||
|
f5f5024981 | ||
|
f59fbf1180 | ||
|
f55498f314 | ||
|
0b73e1f5e0 | ||
|
1b42652bd6 | ||
|
d0a3cdce52 | ||
|
b16d484406 | ||
|
9dc6e02029 | ||
|
26789ff11b | ||
|
cbd2446243 | ||
|
aea92f9d85 | ||
|
2fb8c91415 | ||
|
a0d15ecf23 | ||
|
2d147d961c | ||
|
6f7d02f7fc | ||
|
d95c8dbd9c | ||
|
cdb45bd1f0 | ||
|
cd75870e42 |
31 changed files with 685 additions and 548 deletions
4
.env.sample
Normal file
4
.env.sample
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
SECRET_KEY=ldskjflkdsnejnjsdnf
|
||||||
|
DEBUG=False
|
||||||
|
ENABLE_DEBUG_LOG=True
|
||||||
|
ALLOWED_HOSTS=localhost,.ungleich.ch
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,2 +1,6 @@
|
||||||
|
.idea/
|
||||||
venv/
|
venv/
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
aux/
|
||||||
|
__pycache__/
|
||||||
|
static/
|
||||||
|
|
349
README.md
349
README.md
|
@ -1,33 +1,87 @@
|
||||||
# ungleich-otp
|
# ungleichotp #
|
||||||
|
|
||||||
ungleich-otp is a full blown authentication and authorisation service
|
ungleich-otp is a full blown authentication and authorisation service
|
||||||
made for micro services.
|
made for micro services.
|
||||||
|
|
||||||
The basic idea is that every micro service has a (long term) seed and
|
The basic idea is that every micro service has a (long term) triple
|
||||||
creates time based tokens (See python pyotp, RFC4226, RFC6238).
|
constisting of (name, realm, seed) and creates time based tokens.
|
||||||
|
|
||||||
## Setup instructions ##
|
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 ##
|
||||||
|
|
||||||
This is a standard django project and thus can be easily setup using
|
This is a standard django project and thus can be easily setup using
|
||||||
|
|
||||||
```
|
```
|
||||||
pip install -r requirements.txt
|
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
|
python manage.py runserver
|
||||||
```
|
```
|
||||||
|
|
||||||
The usual instructions on how to setup an https proxy should be followed.
|
|
||||||
|
|
||||||
## Realms ##
|
## Realms ##
|
||||||
|
|
||||||
|
@ -46,221 +100,122 @@ All micro services that are trusted to authenticate another micro
|
||||||
service should have an entry in the ungleich-auth realm, which allows
|
service should have an entry in the ungleich-auth realm, which allows
|
||||||
them to verify a token of somebody else.
|
them to verify a token of somebody else.
|
||||||
|
|
||||||
|
```
|
||||||
| Name | Capabilities |
|
| Name | Capabilities |
|
||||||
|------------------+--------------------------------------------|
|
|------------------+--------------------------------------------|
|
||||||
| ungleich-admin | authenticate, create, delete, list, update |
|
| ungleich-admin | authenticate, create, delete, list, update |
|
||||||
| ungleich-auth | authenticate |
|
| ungleich-auth | authenticate, verify |
|
||||||
| all other realms | NO ACCESS |
|
| all other realms | authenticate |
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Usage: REST ##
|
## Verify using http POST ##
|
||||||
|
|
||||||
- Use an existing token to connect to the service
|
Post a JSON object to the server at /ungleichotp/verify/ that
|
||||||
- All REST based messages: JSON
|
contains the following elements:
|
||||||
|
|
||||||
### POST: /verify
|
|
||||||
|
|
||||||
Request JSON object:
|
Request JSON object:
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
version: "1",
|
auth_name: "auth-name",
|
||||||
name: "your-name",
|
auth_realm: "auth-realm",
|
||||||
realm: "your-realm",
|
auth_token: "current time based token",
|
||||||
token: "current time based token",
|
name: "name that wants to be authenticated",
|
||||||
verifyname: "name that wants to be authenticated",
|
realm: "realm that wants to be authenticated",
|
||||||
verifyrealm: "realm that wants to be authenticated",
|
token: "token that wants to be authenticated"
|
||||||
verifytoken: "token that wants to be authenticated",
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Response JSON object:
|
Response JSON object:
|
||||||
|
|
||||||
Either
|
Either HTTP 200 with
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
status: "OK",
|
status: "OK",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
OR
|
OR return code 403:
|
||||||
|
|
||||||
|
* If token for authenticating is wrong, you get
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{"detail":"Incorrect authentication credentials."}
|
||||||
status: "FAIL",
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### POST /register
|
* If token that is being verified is wrong, you get
|
||||||
|
|
||||||
Register a new seed. Returns an app ID.
|
|
||||||
|
|
||||||
Request JSON object:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{"detail":"You do not have permission to perform this action."}
|
||||||
version: "1",
|
|
||||||
appuuid: "your-app-uuid",
|
|
||||||
token: "current time based token",
|
|
||||||
username: "user this app belongs to",
|
|
||||||
appname: "name of your web app"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Response JSON object:
|
## Authorize the request ##
|
||||||
|
|
||||||
```
|
From the ungleichotp-server, you get a validated information that a
|
||||||
{
|
name on a realm authenticated successfully. The associated permissions
|
||||||
status: "OK",
|
("authorization") is application specific and needs to be decided by
|
||||||
appuuid: "UUID of your app",
|
your application.
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
OR
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
status: "FAIL",
|
|
||||||
error: "Reason for failure"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### POST /app/register
|
|
||||||
|
|
||||||
Register a new app. Returns an app ID.
|
|
||||||
|
|
||||||
Request JSON object:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
version: "1",
|
|
||||||
appuuid: "your-app-uuid",
|
|
||||||
token: "current time based token",
|
|
||||||
username: "user this app belongs to",
|
|
||||||
appname: "name of your web app"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response JSON object:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
status: "OK",
|
|
||||||
appuuid: "UUID of your app",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
OR
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
status: "FAIL",
|
|
||||||
error: "Reason for failure"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GET /app
|
|
||||||
|
|
||||||
List all registered apps for the current user.
|
|
||||||
|
|
||||||
Request JSON object:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
version: "1",
|
|
||||||
appuuid: "your-app-uuid",
|
|
||||||
token: "current time based token"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response JSON object:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
status: "OK",
|
|
||||||
apps: [
|
|
||||||
{
|
|
||||||
name: "name of your web app"
|
|
||||||
appuuid: "UUID of your app",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "name of your second web app"
|
|
||||||
appuuid: "UUID of your second app",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### GET /app/UUID
|
|
||||||
|
|
||||||
Get seed for APP to be used as a token
|
|
||||||
|
|
||||||
Request JSON object:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
version: "1",
|
|
||||||
appuuid: "your-app-uuid",
|
|
||||||
token: "current time based token"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Response JSON object:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
status: "OK",
|
|
||||||
seed: "seed of your app"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Usage: OTP
|
## Limitations ##
|
||||||
|
|
||||||
The seeds that you receive can be used for TOTP to authenticate your
|
* Name, Realm and seed are hard coded to 128 bytes length.
|
||||||
apps.
|
This can be changed, if necessary.
|
||||||
|
* Only python3 support for ungleichotp
|
||||||
|
|
||||||
|
|
||||||
## 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',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|
||||||
- [ ] serialize / input request
|
- [x] (server) Serialize / input request
|
||||||
- [ ] Remove hard coded JSON
|
- [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 (?)
|
||||||
|
- 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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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
Normal file
2
logs/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
|
@ -3,7 +3,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ungleichotp.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ungleichotpserver.settings')
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
122
nameko1.py
122
nameko1.py
|
@ -1,122 +0,0 @@
|
||||||
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"
|
|
10
otpauth/admin.py
Normal file
10
otpauth/admin.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from .models import OTPSeed
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
admin.site.register(OTPSeed)
|
0
ungleichotp/otpauth/migrations/__init__.py → otpauth/management/__init__.py
Normal file → Executable file
0
ungleichotp/otpauth/migrations/__init__.py → otpauth/management/__init__.py
Normal file → Executable file
0
ungleichotp/ungleichotp/__init__.py → otpauth/management/commands/__init__.py
Normal file → Executable file
0
ungleichotp/ungleichotp/__init__.py → otpauth/management/commands/__init__.py
Normal file → Executable file
97
otpauth/management/commands/ungleichotpclient.py
Normal file
97
otpauth/management/commands/ungleichotpclient.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
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
|
46
otpauth/migrations/0001_initial.py
Normal file
46
otpauth/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Generated by Django 2.1.3 on 2018-11-17 22:01
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0009_alter_user_last_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OTPSeed',
|
||||||
|
fields=[
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=128)),
|
||||||
|
('realm', models.CharField(max_length=128)),
|
||||||
|
('seed', models.CharField(max_length=128)),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='otpseed',
|
||||||
|
unique_together={('name', 'realm')},
|
||||||
|
),
|
||||||
|
]
|
0
otpauth/migrations/__init__.py
Normal file
0
otpauth/migrations/__init__.py
Normal file
57
otpauth/models.py
Normal file
57
otpauth/models.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
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)
|
87
otpauth/serializer.py
Normal file
87
otpauth/serializer.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
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()
|
34
otpauth/views.py
Normal file
34
otpauth/views.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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)
|
|
@ -1,22 +0,0 @@
|
||||||
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")
|
|
|
@ -1,6 +1,10 @@
|
||||||
pyotp>=2.2.6
|
pyotp>=2.2.6
|
||||||
django>=2.1.2
|
django==2.2.16
|
||||||
djangorestframework
|
djangorestframework
|
||||||
|
python-decouple>=3.1
|
||||||
|
|
||||||
|
# DB
|
||||||
|
psycopg2>=2.8,<2.9
|
||||||
|
|
||||||
# Recommended
|
# Recommended
|
||||||
markdown
|
markdown
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from .models import OTPSeed
|
|
||||||
|
|
||||||
admin.site.register(OTPSeed)
|
|
|
@ -1,27 +0,0 @@
|
||||||
# Generated by Django 2.1.3 on 2018-11-17 09:01
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='OTPSeed',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
('name', models.CharField(max_length=128)),
|
|
||||||
('realm', models.CharField(max_length=128)),
|
|
||||||
('seed', models.CharField(max_length=128)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AlterUniqueTogether(
|
|
||||||
name='otpseed',
|
|
||||||
unique_together={('name', 'realm')},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,14 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
class OTPSeed(models.Model):
|
|
||||||
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)
|
|
|
@ -1,69 +0,0 @@
|
||||||
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')
|
|
||||||
|
|
||||||
# token = serializers.CharField(max_length=128)
|
|
||||||
|
|
||||||
# verifyname = serializers.CharField(max_length=128)
|
|
||||||
# verifytoken = serializers.CharField(max_length=128)
|
|
||||||
# verifyrealm = serializers.CharField(max_length=128)
|
|
||||||
|
|
||||||
|
|
||||||
# class VerifySerializer(serializers.ModelSerializer):
|
|
||||||
# class Meta:
|
|
||||||
# model = OTPSeed
|
|
||||||
# fields = ('name', 'realm', 'token', 'verifyname', 'verifytoken', 'verifyrealm')
|
|
||||||
|
|
||||||
class VerifySerializer(serializers.Serializer):
|
|
||||||
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 create(self, validated_data):
|
|
||||||
token_in = validated_data.get('token')
|
|
||||||
name_in = validated_data.get('name')
|
|
||||||
realm_in = validated_data.get('realm')
|
|
||||||
|
|
||||||
verifytoken = validated_data.get('verifytoken')
|
|
||||||
verifyname = validated_data.get('verifyname')
|
|
||||||
verifyrealm = 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()
|
|
||||||
|
|
||||||
print("serializer found object")
|
|
||||||
|
|
||||||
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
|
|
|
@ -1,31 +0,0 @@
|
||||||
from django.shortcuts import render
|
|
||||||
from rest_framework import viewsets
|
|
||||||
from rest_framework.parsers import JSONParser
|
|
||||||
from otpauth.serializer import VerifySerializer
|
|
||||||
from django.http import HttpResponse, JsonResponse
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
class VerifyViewSetV1(viewsets.ModelViewSet):
|
|
||||||
serializer_class = VerifySerializer
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
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 []
|
|
|
@ -1,36 +0,0 @@
|
||||||
"""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 VerifyViewSet
|
|
||||||
|
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
|
||||||
router.register(r'ungleichotp', VerifyViewSet, basename='ungleichotp')
|
|
||||||
|
|
||||||
print(router.urls)
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('admin/', admin.site.urls),
|
|
||||||
url(r'^', include(router.urls)),
|
|
||||||
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
|
|
||||||
]
|
|
81
ungleichotpclient.py
Normal file
81
ungleichotpclient.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
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))
|
0
ungleichotpserver/__init__.py
Normal file
0
ungleichotpserver/__init__.py
Normal file
|
@ -10,6 +10,8 @@ For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/2.1/ref/settings/
|
https://docs.djangoproject.com/en/2.1/ref/settings/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from decouple import config, Csv
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
@ -20,13 +22,9 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# 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!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
|
@ -51,7 +49,7 @@ MIDDLEWARE = [
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'ungleichotp.urls'
|
ROOT_URLCONF = 'ungleichotpserver.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
|
@ -69,18 +67,7 @@ TEMPLATES = [
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'ungleichotp.wsgi.application'
|
WSGI_APPLICATION = 'ungleichotpserver.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
|
# Password validation
|
||||||
|
@ -102,6 +89,22 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Customer user model so that we can use our tokens for authentication!
|
||||||
|
AUTH_USER_MODEL = 'otpauth.OTPSeed'
|
||||||
|
|
||||||
|
# Custom authentication so we can use tokens ourselves
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
'otpauth.models.OTPAuthentication'
|
||||||
|
),
|
||||||
|
'DEFAULT_PERMISSION_CLASSES': (
|
||||||
|
'rest_framework.permissions.IsAuthenticated',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/2.1/topics/i18n/
|
# https://docs.djangoproject.com/en/2.1/topics/i18n/
|
||||||
|
|
||||||
|
@ -120,3 +123,72 @@ USE_TZ = True
|
||||||
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
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
|
14
ungleichotpserver/urls.py
Normal file
14
ungleichotpserver/urls.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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'))
|
||||||
|
]
|
|
@ -11,6 +11,6 @@ import os
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ungleichotp.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ungleichotpserver.settings')
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
Loading…
Reference in a new issue