Lösung: SimpleAuthService mit Datenbank
initdb.sql
Folgende SQL-Script dient der Initialisierung der MySQL/MariaDB-Datenbank. Achtung! Vorhandene Daten gehen dabei verloren!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
-- phpMyAdmin SQL init testdb -- CAUTION: all existing data will be lost CREATE USER IF NOT EXISTS 'testUser'@'localhost' IDENTIFIED BY 'testPwd'; GRANT ALL PRIVILEGES ON *.* TO 'testUser'@'localhost'; DROP DATABASE IF EXISTS testDb; CREATE DATABASE testDb; USE testDb; CREATE TABLE tblRole ( roleID tinyint(4) NOT NULL AUTO_INCREMENT, rolename varchar(20) NOT NULL, PRIMARY KEY(roleID) ); INSERT INTO tblRole (roleID, rolename) VALUES (1, 'Administrator'), (2, 'Viewer'); CREATE TABLE tblUser ( userID int(11) NOT NULL AUTO_INCREMENT, username varchar(50) DEFAULT NULL, pwd varchar(128) DEFAULT NULL, token bigint(64) NOT NULL, tokenExpiry timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY(userID) ); -- password 'testPwd' hashed with argon2 INSERT INTO tblUser (userID, username, pwd, token) VALUES (1, 'testUser', '$argon2id$v=19$m=65536,t=3,p=4$AO8XsjZEt2aaGL7Xh/4DeQ$vTplDqP6zL3Kk8uCUu9rPV1+dT3hSmo8Si8DDH3nhQU', 123456); CREATE TABLE tblRoleUser ( userID int(11) NOT NULL, roleID int(11) NOT NULL ); INSERT INTO tblRoleUser (userID, roleID) VALUES (1, 1); |
Hash-Wert
Aus Sicherheitsgründen werden Kennworter niemals im Klartext in der Datenbank, sondern nur als Hash-Wert gespeichert.
Wenn Kennwörter
als Hash-Wert abgespeichert werden, kann man kein nicht auf das Kennwort rückschließen. (Ein verschlüsseltes Kennwort könnte dagegen entschlüsselt werden, was nicht gewollt ist.)
Der Name hash (dt. zerhacken) bzw. Hashwert oder Streuwert zeigt an, dass die Daten quasi „verstreut“ bzw. „zerhackt“ werden, was Rückschlüsse auf die Originalnachricht verhindern soll. Allerdings gelten bestimmte Hash-Algorithmen heute diesbezüglich nicht mehr als sicher, so z.B. md5- und sha256-Hashwerte. Diese veralteten und einfacheren Hash-Algorithmen sind tatsächlich zu schnell, was Brute-Force-Angriffe erleichtert.
Weitere Informationen dazu in der Dokumentation der verwendeten Bibliothek hashlib:
https://docs.python.org/3/library/hashlib.html (02.11.2022)
hash.py
Mit folgendem Programm wurde daher der Hashwert mit individuellem, zufällig erzeugten Salt für das Initialisierungsscript der Datenbank erzeugt:
1 2 3 4 5 6 7 |
from argon2 import PasswordHasher ph = PasswordHasher() hash = ph.hash("testPwd") print(hash) print(ph.verify(hash, "testPwd")) |
hash.py
Sicherheitshinweis:
Dieses Hashing verwendet hier ein empfohlenes individuelles "Salt", das für jeden Benutzer zufällig generiert und mit dem Hash-Wert gemeinsam in der Datenbank gespeichert werd. Das hat den sicherheitsrelevanten Vorteil, dass zu einfache oder Standardkennwörter aus Hashwert-Tabellen (sogenannten Rainbow-Tabellen), die im Internet für die gängigen Hash-Algorithmen ohne Salt angeboten werden, nicht einfach abgelesen werden können. Trotzdem sollte die Anwendung sicherstellen, dass nur ausreichend lange und komplexe Kennwörter gewählt werden dürfen.
Weitere Informationen finden sich im Exkurs: Salted (and peppered) Hashing
settings.py
Hier werden notwendige Einstellungen vorgenommen. Diese Datei darf nicht in ein öffentliches zentrales Repository hochgeladen werden. Sie muss in der entsprichenden .gitignore-Datei ausgeschlossen werden.
1 2 3 4 5 6 7 8 9 |
# database connection parameters # CAUTION: exclude settings.py from versioning in .gitignore HOST = "localhost" USER = "testUser" PASSWD = "testPwd" DB = "testDb" DEBUG_MODE = True # disable DEBUG_MODE in productive environment |
settings.py
Hier werden Verbindungsdaten für die Datenbank zentral definiert. Der DEBUG-MODE ermöglicht eine angepasste Funktionalität zur Entwicklungszeit.
db.py
Wrapper-Klasse für Datenbankfunktionen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import mysql.connector as mc import sys import settings # Wrapper class for database class Db: def __init__(self): try: self.__connection = mc.connect(host=settings.HOST, user=settings.USER, password=settings.PASSWD, db=settings.DB) except mc.Error as e: print("Error %d: %s" % (e.args[0], e.args[1])) sys.exit(1) self.__cursor = self.__connection.cursor() def execute(self, sql): self.__cursor.execute(sql) return self.__cursor.fetchall() def commit(self): self.__connection.commit() # actually executes SQL command # (required after INSERT, UPDATE etc.) def __del__(self): # destructor closes db connection self.__cursor.close() self.__connection.close() |
db.py
simpleAuthService.py
Der um die Datenbankfunktionalität erweiterte Mikroservice:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
from flask import Flask, request import json from argon2 import PasswordHasher import secrets import db import settings # make cross-origin AJAX possible because of using swagger editor # https://flask-cors.readthedocs.io/en/latest/#using-json-with-cors from flask_cors import CORS, cross_origin def generateToken(): if settings.DEBUG_MODE: return "123456" # testing environment else: return secrets.token_urlsafe(64) # production app = Flask(__name__) if settings.DEBUG_MODE: CORS(app) # make cross-origin AJAX possible @app.route('/auth/user/login', methods=['POST']) def loginUser(): content_type = request.headers.get('Content-Type') if content_type == 'application/json': print(request) username = request.json['username'] password = request.json['password'] elif (content_type == 'application/x-www-form-urlencoded'): # regular html form data username = request.form['username'] password = request.form['password'] else: return 'Content-Type not supported: ' + content_type, 400 # Bad request db1 = db.Db() ph = PasswordHasher() hashedPW = ph.hash(str(password)) query = "SELECT userId, pwd FROM tblUser WHERE username='%s'" %(username) result = db1.execute(query) if(result): for row in result: # more than one user with this username possible try: # verify hashed password fail -> throws exception if ph.verify(row[1], password) == True: # check hashed password userId = row[0] token = generateToken() query = "UPDATE tblUser SET token = '%s' WHERE userID=%d" %(token, userId) result = db1.execute(query) db1.commit() # actually execute return json.dumps({ "token": token }) # 200 OK except: pass return json.dumps({ "token": "-1" }), 403 # 403 forbidden - wrong password else: return json.dumps({ "token": "-1" }), 403 # 403 forbidden - no user with this username del db1 # close db connection @app.route('/auth/user/<token>', methods=['DELETE']) def logoutUser(token): db1 = db.Db() query = "UPDATE tblUser SET token = '-1' WHERE token=%s" %(token) result = db1.execute(query) db1.commit() del db1 return "{ \"token\": \"-1\" }" # 200 logout sucessful if __name__ == '__main__': app.run() |
simpleAuthService.py
commit()
Der obige Code demonstriert die Verwendung der Wrapper-Klasse Db. Damit ein Update-Kommando auf der Datenbank unmittelbar ausgeführt wird, muss ein commit() auf die Verbindung gemacht werden.
Hash des Kennworts überprüfen
Das Hashing der Kennworter mit Salting erfordert, dass die Hashes mit dem Salt aus der Datenbank abgerufen werden müssen, um sie hier mit der Methode verify() überprüfen zu können, den die Hash-Werte sind durch jeweils ein individuelles Salt auch bei gleichem Kennwort unterschiedlich. (Hash-Werte ohne Salt wären dagegen für gleiche Kennworte auch gleich und es wäre möglich, mit SELECT auf username und Hash-Wert des dazugehörigen Kennwortes zu authentifizieren.)
Token
Das Token könnte auch von der Datenbank generiert werden. Hier z.B. mit dem MySQL-Kommando
SELECT UUID_SHORT();Um auch andere Datenbankmanagementsysteme verwenden zu können, wurde das Token allerdings mit Python generiert. Weitere Informationen zur verwendeten Bibliothek secure findet man hier:https://blog.miguelgrinberg.com/post/the-new-way-to-generate-secure-tokens-in-python (02.11.2022)
Alle anderen Dateien
- openapi.yaml
- index.html
- testSimpleAuthService.py
bleiben unverändert.
Quellcode auf GitHub:
https://github.com/ateachment/SimpleAuthService/tree/develop-db