From 0d898061c697faf8ae92c2007f4f2c940de6344f Mon Sep 17 00:00:00 2001 From: Wolfgang Hottgenroth Date: Tue, 26 Jan 2021 13:43:09 +0100 Subject: [PATCH] changes --- ENV.tmpl | 6 +-- auth.py | 101 +++++++++++++++++++++++++++++---------------- initial-schema.sql | 81 ++++++++++++++++++++++++++++++++++++ openapi.yaml | 33 ++++++++------- test.py | 20 +++++++++ 5 files changed, 186 insertions(+), 55 deletions(-) create mode 100644 initial-schema.sql create mode 100644 test.py diff --git a/ENV.tmpl b/ENV.tmpl index e5dbece..3a88512 100644 --- a/ENV.tmpl +++ b/ENV.tmpl @@ -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' diff --git a/auth.py b/auth.py index d42c527..d2c44e1 100755 --- a/auth.py +++ b/auth.py @@ -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() diff --git a/initial-schema.sql b/initial-schema.sql new file mode 100644 index 0000000..ee4576a --- /dev/null +++ b/initial-schema.sql @@ -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; + diff --git a/openapi.yaml b/openapi.yaml index 1894c74..1ec7e63 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -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 diff --git a/test.py b/test.py new file mode 100644 index 0000000..3c80ff0 --- /dev/null +++ b/test.py @@ -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) +