17 Commits
0.0.1 ... 0.1.4

8 changed files with 216 additions and 71 deletions

View File

@ -24,7 +24,8 @@ RUN \
pip3 install uwsgi && \ pip3 install uwsgi && \
pip3 install flask-cors && \ pip3 install flask-cors && \
pip3 install six && \ pip3 install six && \
pip3 install python-jose[cryptography] pip3 install python-jose[cryptography] && \
pip3 install pbkdf2
RUN \ RUN \
mkdir -p ${APP_DIR} && \ mkdir -p ${APP_DIR} && \

View File

@ -1,9 +1,10 @@
# copy to ENV and adjust values # copy to ENV and adjust values
export DB_HOST="172.16.10.18" export DB_HOST="172.16.10.18"
export DB_USER="hausverwaltung-ui" export DB_USER="authservice-ui"
export DB_PASS="test123" export DB_PASS="test123"
export DB_NAME="authservice" export DB_NAME="authservice"
# only required for decoding, on client side # only required for decoding, on client side
export JWT_SECRET='streng_geheim' export JWT_SECRET='streng_geheim'
export JWT_ISSUER='de.hottis.authservice'

59
asadduser.py Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/python
import mariadb
from pbkdf2 import crypt
import argparse
import os
parser = argparse.ArgumentParser(description='asadduser')
parser.add_argument('--user', '-u',
help='Login',
required=True)
parser.add_argument('--password', '-p',
help='Password',
required=True)
parser.add_argument('--application', '-a',
help='Application',
required=True)
args = parser.parse_args()
user = args.user
password = args.password
application = args.application
DB_USER = os.environ["DB_USER"]
DB_PASS = os.environ["DB_PASS"]
DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"]
pwhash = crypt(password, iterations=100000)
conn = None
cur = None
try:
conn = mariadb.connect(user = DB_USER, password = DB_PASS,
host = DB_HOST, database = DB_NAME)
conn.autocommit = False
cur = conn.cursor()
cur.execute("""
INSERT INTO users (login, pwhash)
VALUES(?, ?)
""", [user, pwhash])
cur.execute("""
INSERT INTO user_applications_mapping (application, user)
VALUES(
(SELECT id FROM applications WHERE name = ?),
(SELECT id FROM users WHERE login = ?)
)
""", [application, user])
conn.commit()
finally:
if cur:
cur.close()
if conn:
conn.rollback()
conn.close()

76
auth.py
View File

@ -5,22 +5,41 @@ import werkzeug
import os import os
import mariadb import mariadb
from collections import namedtuple from collections import namedtuple
from pbkdf2 import crypt
DB_USER = os.environ["DB_USER"] DB_USER = os.environ["DB_USER"]
DB_PASS = os.environ["DB_PASS"] DB_PASS = os.environ["DB_PASS"]
DB_HOST = os.environ["DB_HOST"] DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"] DB_NAME = os.environ["DB_NAME"]
JWT_ISSUER = os.environ["JWT_ISSUER"]
class NoUserException(Exception): pass
class ManyUsersException(Exception): pass
UserEntry = namedtuple('UserEntry', ['id', 'login', 'issuer', 'secret', 'expiry', 'claims'])
def getUserEntryFromDB(login: str, password: str) -> UserEntry:
class NoUserException(Exception):
pass
class ManyUsersException(Exception):
pass
class PasswordMismatchException(Exception):
pass
UserEntry = namedtuple('UserEntry', ['id', 'login', 'expiry', 'claims'])
JWT_PRIV_KEY = ""
with open('/opt/app/config/authservice.key', 'r') as f:
JWT_PRIV_KEY = f.read()
JWT_PUB_KEY = ""
with open('/opt/app/config/authservice.pub', 'r') as f:
JWT_PUB_KEY = f.read()
def getUserEntryFromDB(application: str, login: str):
conn = None conn = None
cur = None cur = None
try: try:
@ -29,9 +48,9 @@ def getUserEntryFromDB(login: str, password: str) -> UserEntry:
conn.autocommit = False conn.autocommit = False
cur = conn.cursor(dictionary=True) cur = conn.cursor(dictionary=True)
# print("DEBUG: getUserEntryFromDB: login: <{}>, password: <{}>".format(login, password)) cur.execute("SELECT id, pwhash, expiry FROM user_application" +
cur.execute("SELECT id, issuer, secret, expiry FROM user_and_issuer WHERE login = ? AND password = ?", " WHERE application = ? AND login = ?",
[login, password]) [application, login])
resObj = cur.next() resObj = cur.next()
print("DEBUG: getUserEntryFromDB: resObj: {}".format(resObj)) print("DEBUG: getUserEntryFromDB: resObj: {}".format(resObj))
if not resObj: if not resObj:
@ -47,7 +66,7 @@ def getUserEntryFromDB(login: str, password: str) -> UserEntry:
for claimObj in cur: for claimObj in cur:
print("DEBUG: getUserEntryFromDB: add claim {} -> {}".format(claimObj["key"], claimObj["value"])) print("DEBUG: getUserEntryFromDB: add claim {} -> {}".format(claimObj["key"], claimObj["value"]))
if claimObj["key"] in claims: if claimObj["key"] in claims:
if isinstance(claimObj["key"], list): if isinstance(claims[claimObj["key"]], list):
claims[claimObj["key"]].append(claimObj["value"]) claims[claimObj["key"]].append(claimObj["value"])
else: else:
claims[claimObj["key"]] = [ claims[claimObj["key"]] ] claims[claimObj["key"]] = [ claims[claimObj["key"]] ]
@ -55,11 +74,9 @@ def getUserEntryFromDB(login: str, password: str) -> UserEntry:
else: else:
claims[claimObj["key"]] = claimObj["value"] claims[claimObj["key"]] = claimObj["value"]
userEntry = UserEntry(id=userId, login=login, secret=resObj["secret"], userEntry = UserEntry(id=userId, login=login, expiry=resObj["expiry"], claims=claims)
issuer=resObj["issuer"], expiry=resObj["expiry"],
claims=claims)
return userEntry return userEntry, resObj["pwhash"]
except mariadb.Error as err: except mariadb.Error as err:
raise Exception("Error when connecting to database: {}".format(err)) raise Exception("Error when connecting to database: {}".format(err))
finally: finally:
@ -69,39 +86,48 @@ def getUserEntryFromDB(login: str, password: str) -> UserEntry:
conn.rollback() conn.rollback()
conn.close() conn.close()
def getUserEntry(application, login, password):
def getUserEntry(login: str, password: str) -> UserEntry: userEntry, pwhash = getUserEntryFromDB(application, login)
return getUserEntryFromDB(login, password) if pwhash != crypt(password, pwhash):
raise PasswordMismatchException()
return userEntry
def generateToken(**args): def generateToken(**args):
try: try:
body = args["body"] body = args["body"]
application = body["application"]
login = body["login"] login = body["login"]
password = body["password"] password = body["password"]
userEntry = getUserEntryFromDB(login, password)
userEntry = getUserEntry(application, login, password)
timestamp = int(time.time()) timestamp = int(time.time())
payload = { payload = {
"iss": userEntry.issuer, "iss": JWT_ISSUER,
"iat": int(timestamp), "iat": int(timestamp),
"exp": int(timestamp + userEntry.expiry), "exp": int(timestamp + userEntry.expiry),
"sub": str(userEntry.id) "sub": str(userEntry.id)
} }
for claim in userEntry.claims.items(): for claim in userEntry.claims.items():
# print("DEBUG: generateToken: add claim {} -> {}".format(claim[0], claim[1])) # print("DEBUG: generateToken: add claim {} -> {}".format(claim[0], claim[1]))
payload["x-{}".format(claim[0])] = claim[1] payload[claim[0]] = claim[1]
return jwt.encode(payload, userEntry.secret) return jwt.encode(payload, JWT_PRIV_KEY, algorithm='RS256')
except NoUserException: except NoUserException:
print("ERROR: generateToken: no user found, login or password wrong") print("ERROR: generateToken: no user found, login or application wrong")
raise werkzeug.exceptions.Unauthorized() raise werkzeug.exceptions.Unauthorized()
except ManyUsersException: except ManyUsersException:
print("ERROR: generateToken: too many users found") print("ERROR: generateToken: too many users found")
raise werkzeug.exceptions.Unauthorized() raise werkzeug.exceptions.Unauthorized()
except PasswordMismatchException:
print("ERROR: generateToken: wrong password")
raise werkzeug.exceptions.Unauthorized()
except KeyError: except KeyError:
print("ERROR: generateToken: login or password missing") print("ERROR: generateToken: application, login or password missing")
raise werkzeug.exceptions.Unauthorized() raise werkzeug.exceptions.Unauthorized()
except Exception as e: except Exception as e:
print("ERROR: generateToken: unspecific exception: {}".format(str(e))) print("ERROR: generateToken: unspecific exception: {}".format(str(e)))
raise werkzeug.exceptions.Unauthorized() raise werkzeug.exceptions.Unauthorized()
def getPubKey():
return JWT_PUB_KEY

View File

@ -1,45 +1,22 @@
CREATE TABLE `issuers` ( CREATE DATABASE `authservice`;
USE `authservice`;
CREATE TABLE `applications` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL, `name` varchar(128) NOT NULL,
`secret` varchar(128) NOT NULL,
`max_expiry` int(10) NOT NULL,
CONSTRAINT PRIMARY KEY (`id`), CONSTRAINT PRIMARY KEY (`id`),
CONSTRAINT UNIQUE KEY `uk_issuers_name` (`name`) CONSTRAINT UNIQUE KEY `uk_applications_name` (`name`)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
ALTER TABLE `issuers`
MODIFY COLUMN `max_expiry` int(10) unsigned NOT NULL;
CREATE TABLE `users` ( CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`login` varchar(64) NOT NULL, `login` varchar(64) NOT NULL,
`password` varchar(64) NOT NULL, `pwhash` varchar(64) NOT NULL,
CONSTRAINT PRIMARY KEY (`id`), `expiry` int(10) unsigned NOT NULL DEFAULT 600,
CONSTRAINT UNIQUE KEY `uk_users_login` (`login`) CONSTRAINT PRIMARY KEY (`id`),
CONSTRAINT UNIQUE KEY `uk_users_login` (`login`)
) ENGINE=InnoDB; ) 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` ( CREATE TABLE `claims` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(64) NOT NULL, `key` varchar(64) NOT NULL,
@ -58,6 +35,16 @@ CREATE TABLE `user_claims_mapping` (
REFERENCES `claims`(`id`) REFERENCES `claims`(`id`)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
CREATE TABLE `user_applications_mapping` (
`user` int(10) unsigned NOT NULL,
`application` int(10) unsigned NOT NULL,
CONSTRAINT UNIQUE KEY `uk_user_applications_mapping` (`user`, `application` ),
CONSTRAINT FOREIGN KEY `fk_user_applications_mapping_user` (`user`)
REFERENCES `users`(`id`),
CONSTRAINT FOREIGN KEY `fk_user_applications_mapping_application` (`application`)
REFERENCES `applications`(`id`)
) ENGINE=InnoDB;
CREATE OR REPLACE VIEW claims_for_user AS CREATE OR REPLACE VIEW claims_for_user AS
SELECT u.id AS user, SELECT u.id AS user,
c.`key` AS `key`, c.`key` AS `key`,
@ -68,14 +55,57 @@ CREATE OR REPLACE VIEW claims_for_user AS
WHERE m.user = u.id AND WHERE m.user = u.id AND
m.claim = c.id; m.claim = c.id;
CREATE OR REPLACE VIEW user_and_issuer AS CREATE OR REPLACE VIEW user_application AS
SELECT u.login AS login, SELECT u.login AS login,
u.password AS password, u.pwhash AS pwhash,
u.id AS id, u.id AS id,
i.name AS issuer, u.expiry AS expiry,
i.secret AS secret, a.name as application
least(u.expiry, i.max_expiry) AS expiry
FROM users u, FROM users u,
issuers i applications a,
WHERE u.issuer = i.id; user_applications_mapping m
WHERE u.id = m.user AND
a.id = m.application;
CREATE USER 'authservice-ui'@'%' IDENTIFIED BY 'test123';
GRANT SELECT ON `user_application` TO 'authservice-ui'@'%';
GRANT SELECT ON `claims_for_user` TO 'authservice-ui'@'%';
CREATE USER 'authservice-cli'@'%' IDENTIFIED BY 'test123';
GRANT INSERT ON `users` TO 'authservice-cli'@'%';
GRANT INSERT ON `user_applications_mapping` TO 'authservice-cli'@'%';
FLUSH PRIVILEGES;
INSERT INTO `applications` (`name`) VALUES ('hv');
INSERT INTO `claims` (`key`, `value`) VALUES ('accesslevel', 'r');
INSERT INTO `claims` (`key`, `value`) VALUES ('accesslevel', 'rw');
-- password is 'test123'
INSERT INTO `users` (`login`, `pwhash`) VALUES ('wn', '$p5k2$186a0$dJXL0AjF$0HualDF92nyilDXPgSbaUn/UpFzSrpPx');
INSERT INTO `user_applications_mapping` (`user`, `application`)
VALUES(
(SELECT `id` FROM `users` WHERE `login` = 'wn'),
(SELECT `id` FROM `applications` WHERE `name` = 'hv')
);
INSERT INTO `user_claims_mapping` (`user`, `claim`)
VALUES(
(SELECT `id` FROM `users` WHERE `login` = 'wn'),
(SELECT `id` FROM `claims` WHERE `key` = 'accesslevel' AND `value` = 'rw')
);
-- password is 'geheim'
INSERT INTO `users` (`login`, `pwhash`) VALUES ('gregor', '$p5k2$186a0$Tcwps8Ar$TsypGB.y1dCB9pWOPz2X2SsxYqrTn3Fv');
INSERT INTO `user_applications_mapping` (`user`, `application`)
VALUES(
(SELECT `id` FROM `users` WHERE `login` = 'gregor'),
(SELECT `id` FROM `applications` WHERE `name` = 'hv')
);
INSERT INTO `user_claims_mapping` (`user`, `claim`)
VALUES(
(SELECT `id` FROM `users` WHERE `login` = 'gregor'),
(SELECT `id` FROM `claims` WHERE `key` = 'accesslevel' AND `value` = 'rw')
);

View File

@ -35,6 +35,19 @@ paths:
type: string type: string
security: security:
- jwt: ['secret'] - jwt: ['secret']
/pubkey:
get:
tags: [ "JWT" ]
summary: Get the public key of this issuer
operationId: auth.getPubKey
responses:
'200':
description: public key
content:
'text/plain':
schema:
type: string
components: components:
securitySchemes: securitySchemes:
@ -45,9 +58,11 @@ components:
x-bearerInfoFunc: test.decodeToken x-bearerInfoFunc: test.decodeToken
schemas: schemas:
User: User:
description: Login/Password tuple description: Application/Login/Password tuple
type: object type: object
properties: properties:
application:
type: string
login: login:
type: string type: string
password: password:

13
readme.md Normal file
View File

@ -0,0 +1,13 @@
Generate the RSA key pair using:
Private key (keep it secret!):
openssl genrsa -out authservice.key 2048
Extract the public key (publish it):
openssl rsa -in authservice.pem -outform PEM -pubout -out authservice.pub

View File

@ -3,7 +3,7 @@ from flask_cors import CORS
# instantiate the webservice # instantiate the webservice
app = connexion.App(__name__) app = connexion.App(__name__)
app.add_api('openapi.yaml') app.add_api('openapi.yaml', options = {"swagger_ui": True})
# CORSify it - otherwise Angular won't accept it # CORSify it - otherwise Angular won't accept it
CORS(app.app) CORS(app.app)