This commit is contained in:
Wolfgang Hottgenroth 2021-01-26 13:43:09 +01:00
parent b88dec12d3
commit 0d898061c6
Signed by: wn
GPG Key ID: E49AF3B9EF6DD469
5 changed files with 186 additions and 55 deletions

View File

@ -3,9 +3,7 @@
export DB_HOST="172.16.10.18"
export DB_USER="hausverwaltung-ui"
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_LIFETIME_SECONDS=60
export JWT_ALGORITHM='HS256'

101
auth.py
View File

@ -1,13 +1,11 @@
import time
import connexion
from jose import JWTError, jwt
import werkzeug
import os
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_PASS = os.environ["DB_PASS"]
@ -15,7 +13,14 @@ DB_HOST = os.environ["DB_HOST"]
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
cur = None
try:
@ -24,13 +29,35 @@ def getUserEntryFromDB(login, password):
conn.autocommit = False
cur = conn.cursor(dictionary=True)
cur.execute("SELECT id FROM users WHERE login = ? AND password = ?", [login, password])
userEntry = cur.next()
if not userEntry:
raise Exception("No user entry found")
# print("DEBUG: getUserEntryFromDB: login: <{}>, password: <{}>".format(login, password))
cur.execute("SELECT id, issuer, secret, expiry FROM user_and_issuer WHERE login = ? AND password = ?",
[login, password])
resObj = cur.next()
print("DEBUG: getUserEntryFromDB: resObj: {}".format(resObj))
if not resObj:
raise NoUserException()
invObj = cur.next()
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
except mariadb.Error as err:
@ -43,34 +70,38 @@ def getUserEntryFromDB(login, password):
conn.close()
def getUserEntry(login, password):
def getUserEntry(login: str, password: str) -> UserEntry:
return getUserEntryFromDB(login, password)
def generateToken(login, password):
userEntry = getUserEntryFromDB(login, password)
userId = userEntry["id"]
timestamp = int(time.time())
payload = {
"iss": JWT_ISSUER,
"iat": int(timestamp),
"exp": int(timestamp + JWT_LIFETIME_SECONDS),
"sub": str(userId),
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
def decodeToken(token):
def generateToken(**args):
try:
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
except JWTError as e:
return "Unauthorized ({})".format(str(e)), 401
body = args["body"]
login = body["login"]
password = body["password"]
userEntry = getUserEntryFromDB(login, password)
timestamp = int(time.time())
payload = {
"iss": userEntry.issuer,
"iat": int(timestamp),
"exp": int(timestamp + userEntry.expiry),
"sub": str(userEntry.id)
}
for claim in userEntry.claims.items():
# print("DEBUG: generateToken: add claim {} -> {}".format(claim[0], claim[1]))
payload["x-{}".format(claim[0])] = claim[1]
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"
paths:
/auth/{login}:
/auth:
post:
tags: [ "JWT" ]
summary: Return JWT token
operationId: auth.generateToken
parameters:
- name: login
description: Login
in: path
required: true
schema:
type: string
- name: password
description: Password
in: body
required: true
schema:
type: string
requestBody:
content:
'application/json':
schema:
$ref: '#/components/schemas/User'
responses:
'200':
description: JWT token
@ -33,7 +25,7 @@ paths:
get:
tags: [ "JWT" ]
summary: Return secret string
operationId: auth.getSecret
operationId: test.getSecret
responses:
'200':
description: secret response
@ -50,4 +42,13 @@ components:
type: http
scheme: bearer
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)