22 Commits
0.0.1 ... 0.1.6

Author SHA1 Message Date
0911a73085 fix 2021-05-07 14:06:18 +02:00
1de73e99e3 message 2021-05-07 14:05:57 +02:00
b44af0658a jwe 2021-05-07 13:28:12 +02:00
309b4c6ba8 authe 2021-05-07 12:24:59 +02:00
a921fb6a0f changes 2021-05-07 12:15:30 +02:00
f56db65012 pubkey stuff, remove debug 2021-05-06 16:55:39 +02:00
ef0793be4e pubkey stuff 2021-05-06 16:52:16 +02:00
3f2442e259 pubkey stuff 2021-05-06 16:50:17 +02:00
78439a7ed8 pubkey stuff 2021-05-06 16:46:19 +02:00
0377278ea0 pubkey stuff 2021-05-06 16:37:32 +02:00
49e8aa43b4 use rs256 2021-05-06 15:42:46 +02:00
35a997774f fix in claims handling 2021-05-06 15:22:43 +02:00
08734cb82c remove x from private claims 2021-01-27 13:31:34 +01:00
875301b437 fix 2021-01-27 12:40:27 +01:00
da06065959 enable ui 2021-01-27 12:06:21 +01:00
fe007cbfe7 forgotten fix 2021-01-27 11:02:19 +01:00
e2d5ed21ad schema fixes 2021-01-27 10:57:54 +01:00
003c83da92 ui user 2021-01-26 22:56:07 +01:00
ca17c556d6 disable ui 2021-01-26 22:36:11 +01:00
ef1b8ddf30 add module to dockerfile 2021-01-26 22:11:08 +01:00
e1b9597fdb crypt and adduser tool 2021-01-26 22:06:39 +01:00
ca9e0b81d3 application and pwhash 2021-01-26 21:27:17 +01:00
9 changed files with 255 additions and 72 deletions

View File

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

View File

@ -1,9 +1,10 @@
# copy to ENV and adjust values
export DB_HOST="172.16.10.18"
export DB_USER="hausverwaltung-ui"
export DB_USER="authservice-ui"
export DB_PASS="test123"
export DB_NAME="authservice"
# only required for decoding, on client side
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()

90
auth.py
View File

@ -5,22 +5,48 @@ import werkzeug
import os
import mariadb
from collections import namedtuple
from pbkdf2 import crypt
DB_USER = os.environ["DB_USER"]
DB_PASS = os.environ["DB_PASS"]
DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"]
class NoUserException(Exception): pass
class ManyUsersException(Exception): pass
UserEntry = namedtuple('UserEntry', ['id', 'login', 'issuer', 'secret', 'expiry', 'claims'])
JWT_ISSUER = os.environ["JWT_ISSUER"]
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 = ""
try:
JWT_PRIV_KEY = os.environ["JWT_PRIV_KEY"]
except KeyError:
with open('/opt/app/config/authservice.key', 'r') as f:
JWT_PRIV_KEY = f.read()
JWT_PUB_KEY = ""
try:
JWT_PUB_KEY = os.environ["JWT_PUB_KEY"]
except KeyError:
with open('/opt/app/config/authservice.pub', 'r') as f:
JWT_PUB_KEY = f.read()
def getUserEntryFromDB(application: str, login: str):
conn = None
cur = None
try:
@ -29,9 +55,9 @@ def getUserEntryFromDB(login: str, password: str) -> UserEntry:
conn.autocommit = False
cur = conn.cursor(dictionary=True)
# 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])
cur.execute("SELECT id, pwhash, expiry FROM user_application" +
" WHERE application = ? AND login = ?",
[application, login])
resObj = cur.next()
print("DEBUG: getUserEntryFromDB: resObj: {}".format(resObj))
if not resObj:
@ -47,7 +73,7 @@ def getUserEntryFromDB(login: str, password: str) -> UserEntry:
for claimObj in cur:
print("DEBUG: getUserEntryFromDB: add claim {} -> {}".format(claimObj["key"], claimObj["value"]))
if claimObj["key"] in claims:
if isinstance(claimObj["key"], list):
if isinstance(claims[claimObj["key"]], list):
claims[claimObj["key"]].append(claimObj["value"])
else:
claims[claimObj["key"]] = [ claims[claimObj["key"]] ]
@ -55,11 +81,9 @@ def getUserEntryFromDB(login: str, password: str) -> UserEntry:
else:
claims[claimObj["key"]] = claimObj["value"]
userEntry = UserEntry(id=userId, login=login, secret=resObj["secret"],
issuer=resObj["issuer"], expiry=resObj["expiry"],
claims=claims)
userEntry = UserEntry(id=userId, login=login, expiry=resObj["expiry"], claims=claims)
return userEntry
return userEntry, resObj["pwhash"]
except mariadb.Error as err:
raise Exception("Error when connecting to database: {}".format(err))
finally:
@ -69,39 +93,53 @@ def getUserEntryFromDB(login: str, password: str) -> UserEntry:
conn.rollback()
conn.close()
def getUserEntry(login: str, password: str) -> UserEntry:
return getUserEntryFromDB(login, password)
def getUserEntry(application, login, password):
userEntry, pwhash = getUserEntryFromDB(application, login)
if pwhash != crypt(password, pwhash):
raise PasswordMismatchException()
return userEntry
def generateToken(**args):
try:
body = args["body"]
application = body["application"]
login = body["login"]
password = body["password"]
userEntry = getUserEntryFromDB(login, password)
userEntry = getUserEntry(application, login, password)
timestamp = int(time.time())
payload = {
"iss": userEntry.issuer,
"iss": JWT_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]
payload[claim[0]] = claim[1]
return jwt.encode(payload, userEntry.secret)
return jwt.encode(payload, JWT_PRIV_KEY, algorithm='RS256')
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()
except ManyUsersException:
print("ERROR: generateToken: too many users found")
raise werkzeug.exceptions.Unauthorized()
except PasswordMismatchException:
print("ERROR: generateToken: wrong password")
raise werkzeug.exceptions.Unauthorized()
except KeyError:
print("ERROR: generateToken: login or password missing")
print("ERROR: generateToken: application, login or password missing")
raise werkzeug.exceptions.Unauthorized()
except Exception as e:
print("ERROR: generateToken: unspecific exception: {}".format(str(e)))
raise werkzeug.exceptions.Unauthorized()
def generateTokenFromEnc(**args):
cryptContent = args["body"]
raise werkzeug.exceptions.NotImplemented("Stay tuned, will be added soon")
return str(cryptContent)
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,
`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`)
CONSTRAINT UNIQUE KEY `uk_applications_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`)
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`login` varchar(64) NOT NULL,
`pwhash` varchar(64) NOT NULL,
`expiry` int(10) unsigned NOT NULL DEFAULT 600,
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,
@ -58,6 +35,16 @@ CREATE TABLE `user_claims_mapping` (
REFERENCES `claims`(`id`)
) 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
SELECT u.id AS user,
c.`key` AS `key`,
@ -68,14 +55,57 @@ CREATE OR REPLACE VIEW claims_for_user AS
WHERE m.user = u.id AND
m.claim = c.id;
CREATE OR REPLACE VIEW user_and_issuer AS
CREATE OR REPLACE VIEW user_application AS
SELECT u.login AS login,
u.password AS password,
u.pwhash AS pwhash,
u.id AS id,
i.name AS issuer,
i.secret AS secret,
least(u.expiry, i.max_expiry) AS expiry
u.expiry AS expiry,
a.name as application
FROM users u,
issuers i
WHERE u.issuer = i.id;
applications a,
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

@ -7,7 +7,7 @@ paths:
/auth:
post:
tags: [ "JWT" ]
summary: Return JWT token
summary: Accept login and password, return JWT token
operationId: auth.generateToken
requestBody:
content:
@ -21,6 +21,23 @@ paths:
'text/plain':
schema:
type: string
/authe:
post:
tags: [ "JWT" ]
summary: Accept encrypted set of credentials, return JWT token
operationId: auth.generateTokenFromEnc
requestBody:
content:
'text/plain':
schema:
type: string
responses:
'200':
description: JWT token
content:
'text/plain':
schema:
type: string
/secret:
get:
tags: [ "JWT" ]
@ -35,6 +52,19 @@ paths:
type: string
security:
- 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:
securitySchemes:
@ -45,9 +75,11 @@ components:
x-bearerInfoFunc: test.decodeToken
schemas:
User:
description: Login/Password tuple
description: Application/Login/Password tuple
type: object
properties:
application:
type: string
login:
type: string
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
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
CORS(app.app)

9
testjwe.py Normal file
View File

@ -0,0 +1,9 @@
from jose import jwe
JWT_PUB_KEY = os.environ["JWT_PUB_KEY"]
plainText = "BlaBlaBla123"
cryptText = jwe.encrypt(plainText, JWT_PUB_KEY, "A256GCM", "RSA-OAEP")
print(cryptText)