This commit is contained in:
2021-01-26 13:43:09 +01:00
parent b88dec12d3
commit 0d898061c6
5 changed files with 186 additions and 55 deletions

View File

@ -3,9 +3,7 @@
export DB_HOST="172.16.10.18" export DB_HOST="172.16.10.18"
export DB_USER="hausverwaltung-ui" export DB_USER="hausverwaltung-ui"
export DB_PASS="test123" export DB_PASS="test123"
export DB_NAME="hausverwaltung" export DB_NAME="authservice"
export JWT_ISSUER='de.hottis.hausverwaltung' # only required for decoding, on client side
export JWT_SECRET='streng_geheim' export JWT_SECRET='streng_geheim'
export JWT_LIFETIME_SECONDS=60
export JWT_ALGORITHM='HS256'

93
auth.py
View File

@ -1,13 +1,11 @@
import time import time
import connexion import connexion
from jose import JWTError, jwt from jose import JWTError, jwt
import werkzeug
import os import os
import mariadb import mariadb
from collections import namedtuple
JWT_ISSUER = os.environ['JWT_ISSUER']
JWT_SECRET = os.environ['JWT_SECRET']
JWT_LIFETIME_SECONDS = int(os.environ['JWT_LIFETIME_SECONDS'])
JWT_ALGORITHM = os.environ['JWT_ALGORITHM']
DB_USER = os.environ["DB_USER"] DB_USER = os.environ["DB_USER"]
DB_PASS = os.environ["DB_PASS"] DB_PASS = os.environ["DB_PASS"]
@ -15,7 +13,14 @@ DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"] DB_NAME = os.environ["DB_NAME"]
def getUserEntryFromDB(login, password): class NoUserException(Exception): pass
class ManyUsersException(Exception): pass
UserEntry = namedtuple('UserEntry', ['id', 'login', 'issuer', 'secret', 'expiry', 'claims'])
def getUserEntryFromDB(login: str, password: str) -> UserEntry:
conn = None conn = None
cur = None cur = None
try: try:
@ -24,13 +29,35 @@ def getUserEntryFromDB(login, password):
conn.autocommit = False conn.autocommit = False
cur = conn.cursor(dictionary=True) cur = conn.cursor(dictionary=True)
cur.execute("SELECT id FROM users WHERE login = ? AND password = ?", [login, password]) # print("DEBUG: getUserEntryFromDB: login: <{}>, password: <{}>".format(login, password))
userEntry = cur.next() cur.execute("SELECT id, issuer, secret, expiry FROM user_and_issuer WHERE login = ? AND password = ?",
if not userEntry: [login, password])
raise Exception("No user entry found") resObj = cur.next()
print("DEBUG: getUserEntryFromDB: resObj: {}".format(resObj))
if not resObj:
raise NoUserException()
invObj = cur.next() invObj = cur.next()
if invObj: if invObj:
raise Exception("Too many user entries found") raise ManyUsersException()
userId = resObj["id"]
cur.execute("SELECT user, `key`, `value` FROM claims_for_user where user = ?",
[userId])
claims = {}
for claimObj in cur:
print("DEBUG: getUserEntryFromDB: add claim {} -> {}".format(claimObj["key"], claimObj["value"]))
if claimObj["key"] in claims:
if isinstance(claimObj["key"], list):
claims[claimObj["key"]].append(claimObj["value"])
else:
claims[claimObj["key"]] = [ claims[claimObj["key"]] ]
claims[claimObj["key"]].append(claimObj["value"])
else:
claims[claimObj["key"]] = claimObj["value"]
userEntry = UserEntry(id=userId, login=login, secret=resObj["secret"],
issuer=resObj["issuer"], expiry=resObj["expiry"],
claims=claims)
return userEntry return userEntry
except mariadb.Error as err: except mariadb.Error as err:
@ -43,34 +70,38 @@ def getUserEntryFromDB(login, password):
conn.close() conn.close()
def getUserEntry(login, password): def getUserEntry(login: str, password: str) -> UserEntry:
return getUserEntryFromDB(login, password) return getUserEntryFromDB(login, password)
def generateToken(login, password): def generateToken(**args):
try:
body = args["body"]
login = body["login"]
password = body["password"]
userEntry = getUserEntryFromDB(login, password) userEntry = getUserEntryFromDB(login, password)
userId = userEntry["id"]
timestamp = int(time.time()) timestamp = int(time.time())
payload = { payload = {
"iss": JWT_ISSUER, "iss": userEntry.issuer,
"iat": int(timestamp), "iat": int(timestamp),
"exp": int(timestamp + JWT_LIFETIME_SECONDS), "exp": int(timestamp + userEntry.expiry),
"sub": str(userId), "sub": str(userEntry.id)
} }
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM) for claim in userEntry.claims.items():
# print("DEBUG: generateToken: add claim {} -> {}".format(claim[0], claim[1]))
payload["x-{}".format(claim[0])] = claim[1]
def decodeToken(token):
try:
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
except JWTError as e:
return "Unauthorized ({})".format(str(e)), 401
def getSecret(user, token_info):
return '''
You are user_id {user} and the secret is 'wbevuec'.
Decoded token claims: {token_info}.
'''.format(user=user, token_info=token_info)
return jwt.encode(payload, userEntry.secret)
except NoUserException:
print("ERROR: generateToken: no user found, login or password wrong")
raise werkzeug.exceptions.Unauthorized()
except ManyUsersException:
print("ERROR: generateToken: too many users found")
raise werkzeug.exceptions.Unauthorized()
except KeyError:
print("ERROR: generateToken: login or password missing")
raise werkzeug.exceptions.Unauthorized()
except Exception as e:
print("ERROR: generateToken: unspecific exception: {}".format(str(e)))
raise werkzeug.exceptions.Unauthorized()

81
initial-schema.sql Normal file
View File

@ -0,0 +1,81 @@
CREATE TABLE `issuers` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL,
`secret` varchar(128) NOT NULL,
`max_expiry` int(10) NOT NULL,
CONSTRAINT PRIMARY KEY (`id`),
CONSTRAINT UNIQUE KEY `uk_issuers_name` (`name`)
) ENGINE=InnoDB;
ALTER TABLE `issuers`
MODIFY COLUMN `max_expiry` int(10) unsigned NOT NULL;
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`login` varchar(64) NOT NULL,
`password` varchar(64) NOT NULL,
CONSTRAINT PRIMARY KEY (`id`),
CONSTRAINT UNIQUE KEY `uk_users_login` (`login`)
) ENGINE=InnoDB;
ALTER TABLE `users`
ADD COLUMN issuer int(10) unsigned;
ALTER TABLE `users`
MODIFY COLUMN issuer int(10) unsigned NOT NULL;
ALTER TABLE `users`
ADD CONSTRAINT FOREIGN KEY `fk_users_issuer` (`issuer`)
REFERENCES `issuers` (`id`);
ALTER TABLE `users`
DROP CONSTRAINT `uk_users_login`;
ALTER TABLE `users`
ADD CONSTRAINT UNIQUE KEY `uk_users_login_issuer` (`login`, `issuer`);
ALTER TABLE `users`
ADD COLUMN expiry int(10) unsigned;
ALTER TABLE `users`
MODIFY COLUMN expiry int(10) unsigned NOT NULL;
CREATE TABLE `claims` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(64) NOT NULL,
`value` varchar(1024) NOT NULL,
CONSTRAINT PRIMARY KEY (`id`),
CONSTRAINT UNIQUE KEY `uk_claims_key_value` (`key`, `value`)
) ENGINE=InnoDB;
CREATE TABLE `user_claims_mapping` (
`user` int(10) unsigned NOT NULL,
`claim` int(10) unsigned NOT NULL,
CONSTRAINT UNIQUE KEY `uk_user_claims_mapping` (`user`, `claim` ),
CONSTRAINT FOREIGN KEY `fk_user_claims_mapping_user` (`user`)
REFERENCES `users`(`id`),
CONSTRAINT FOREIGN KEY `fk_user_claims_mapping_claim` (`claim`)
REFERENCES `claims`(`id`)
) ENGINE=InnoDB;
CREATE OR REPLACE VIEW claims_for_user AS
SELECT u.id AS user,
c.`key` AS `key`,
c.`value` AS `value`
FROM users u,
claims c,
user_claims_mapping m
WHERE m.user = u.id AND
m.claim = c.id;
CREATE OR REPLACE VIEW user_and_issuer AS
SELECT u.login AS login,
u.password AS password,
u.id AS id,
i.name AS issuer,
i.secret AS secret,
least(u.expiry, i.max_expiry) AS expiry
FROM users u,
issuers i
WHERE u.issuer = i.id;

View File

@ -4,24 +4,16 @@ info:
version: "0.1" version: "0.1"
paths: paths:
/auth/{login}: /auth:
post: post:
tags: [ "JWT" ] tags: [ "JWT" ]
summary: Return JWT token summary: Return JWT token
operationId: auth.generateToken operationId: auth.generateToken
parameters: requestBody:
- name: login content:
description: Login 'application/json':
in: path
required: true
schema: schema:
type: string $ref: '#/components/schemas/User'
- name: password
description: Password
in: body
required: true
schema:
type: string
responses: responses:
'200': '200':
description: JWT token description: JWT token
@ -33,7 +25,7 @@ paths:
get: get:
tags: [ "JWT" ] tags: [ "JWT" ]
summary: Return secret string summary: Return secret string
operationId: auth.getSecret operationId: test.getSecret
responses: responses:
'200': '200':
description: secret response description: secret response
@ -50,4 +42,13 @@ components:
type: http type: http
scheme: bearer scheme: bearer
bearerFormat: JWT bearerFormat: JWT
x-bearerInfoFunc: auth.decodeToken x-bearerInfoFunc: test.decodeToken
schemas:
User:
description: Login/Password tuple
type: object
properties:
login:
type: string
password:
type: string

20
test.py Normal file
View File

@ -0,0 +1,20 @@
from jose import JWTError, jwt
import os
import werkzeug
JWT_SECRET = os.environ['JWT_SECRET']
def decodeToken(token):
try:
return jwt.decode(token, JWT_SECRET)
except JWTError as e:
print("ERROR: decodeToken: {}".format(e))
raise werkzeug.exceptions.Unauthorized()
def getSecret(user, token_info):
return '''
You are user_id {user} and the secret is 'wbevuec'.
Decoded token claims: {token_info}.
'''.format(user=user, token_info=token_info)