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);
initdb.sql

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

Last modified: Tuesday, 31 January 2023, 8:15 PM