19 Commits
0.0.8 ... 0.3.0

8 changed files with 231 additions and 179 deletions

View File

@ -16,8 +16,9 @@ ENV JWT_SECRET='streng_geheim'
RUN \
apt update && \
apt install -y libmariadbclient-dev && \
pip3 install mariadb && \
apt install -y postgresql-client-common && \
pip3 install psycopg2 && \
pip3 install loguru && \
pip3 install dateparser && \
pip3 install connexion && \
pip3 install connexion[swagger-ui] && \

147
auth.py
View File

@ -1,11 +1,13 @@
import time
import connexion
from jose import JWTError, jwt
from jose import JWTError, jwt, jwe
import json
import werkzeug
import os
import mariadb
import psycopg2
from collections import namedtuple
from pbkdf2 import crypt
from loguru import logger
DB_USER = os.environ["DB_USER"]
DB_PASS = os.environ["DB_PASS"]
@ -13,7 +15,9 @@ DB_HOST = os.environ["DB_HOST"]
DB_NAME = os.environ["DB_NAME"]
JWT_ISSUER = os.environ["JWT_ISSUER"]
JWT_SECRET = os.environ["JWT_SECRET"]
class NoUserException(Exception):
@ -26,69 +30,97 @@ class PasswordMismatchException(Exception):
pass
UserEntry = namedtuple('UserEntry', ['id', 'login', 'expiry', 'claims'])
UserEntry = namedtuple('UserEntry', ['id', 'login', 'pwhash', '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:
conn = mariadb.connect(user = DB_USER, password = DB_PASS,
host = DB_HOST, database = DB_NAME)
conn = psycopg2.connect(user = DB_USER, password = DB_PASS,
host = DB_HOST, database = DB_NAME)
conn.autocommit = False
cur = conn.cursor(dictionary=True)
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:
raise NoUserException()
invObj = cur.next()
if invObj:
raise ManyUsersException()
userObj = None
with conn.cursor() as cur:
cur.execute("SELECT id, pwhash, expiry FROM user_application_v" +
" WHERE application = %s AND login = %s",
(application, login))
userObj = cur.fetchone()
logger.debug("userObj: {}".format(userObj))
if not userObj:
raise NoUserException()
invObj = cur.fetchone()
if invObj:
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"])
with conn.cursor() as cur:
cur.execute('SELECT key, value FROM claims_for_user_v where "user" = %s and application = %s',
(userObj[0], application))
for claimObj in cur:
logger.debug("add claim {} -> {}".format(claimObj[0], claimObj[1]))
if claimObj[0] in claims:
if isinstance(claims[claimObj[0]], list):
claims[claimObj[0]].append(claimObj[1])
else:
claims[claimObj[0]] = [ claims[claimObj[0]] ]
claims[claimObj[0]].append(claimObj[1])
else:
claims[claimObj["key"]] = [ claims[claimObj["key"]] ]
claims[claimObj["key"]].append(claimObj["value"])
else:
claims[claimObj["key"]] = claimObj["value"]
claims[claimObj[0]] = claimObj[1]
userEntry = UserEntry(id=userId, login=login, expiry=resObj["expiry"], claims=claims)
userEntry = UserEntry(id=userObj[0], login=login, pwhash=userObj[1], expiry=userObj[2], claims=claims)
return userEntry, resObj["pwhash"]
except mariadb.Error as err:
return userEntry
except psycopg2.Error as err:
raise Exception("Error when connecting to database: {}".format(err))
finally:
if cur:
cur.close()
if conn:
conn.rollback()
conn.close()
def getUserEntry(application, login, password):
userEntry, pwhash = getUserEntryFromDB(application, login)
if pwhash != crypt(password, pwhash):
userEntry = getUserEntryFromDB(application, login)
if userEntry.pwhash != crypt(password, userEntry.pwhash):
raise PasswordMismatchException()
return userEntry
def generateToken(**args):
try:
body = args["body"]
application = body["application"]
login = body["login"]
password = body["password"]
application = ""
login = ""
password = ""
if (("application" in body) and
("login" in body) and
("password" in body)):
application = body["application"]
login = body["login"]
password = body["password"]
elif ("encAleTuple" in body):
clearContent = jwe.decrypt(body["encAleTuple"], JWT_PRIV_KEY)
clearObj = json.loads(clearContent)
application = clearObj["application"]
login = clearObj["login"]
password = clearObj["password"]
else:
raise KeyError("Neither application, login and password nor encAleTuple given")
logger.debug(f"Tuple: {application} {login} {password}")
userEntry = getUserEntry(application, login, password)
@ -97,25 +129,44 @@ def generateToken(**args):
"iss": JWT_ISSUER,
"iat": int(timestamp),
"exp": int(timestamp + userEntry.expiry),
"sub": str(userEntry.id)
"sub": str(userEntry.id),
"aud": application
}
logger.debug("claims: {}".format(userEntry.claims))
for claim in userEntry.claims.items():
# print("DEBUG: generateToken: add claim {} -> {}".format(claim[0], claim[1]))
logger.debug("add claim {}".format(claim))
payload[claim[0]] = claim[1]
return jwt.encode(payload, JWT_SECRET)
return jwt.encode(payload, JWT_PRIV_KEY, algorithm='RS256')
except NoUserException:
print("ERROR: generateToken: no user found, login or application wrong")
logger.error("no user found, login or application wrong")
raise werkzeug.exceptions.Unauthorized()
except ManyUsersException:
print("ERROR: generateToken: too many users found")
logger.error("too many users found")
raise werkzeug.exceptions.Unauthorized()
except PasswordMismatchException:
print("ERROR: generateToken: wrong password")
logger.error("wrong password")
raise werkzeug.exceptions.Unauthorized()
except KeyError:
print("ERROR: generateToken: application, login or password missing")
logger.error("application, login or password missing")
raise werkzeug.exceptions.Unauthorized()
except Exception as e:
print("ERROR: generateToken: unspecific exception: {}".format(str(e)))
logger.error("unspecific exception: {}".format(str(e)))
raise werkzeug.exceptions.Unauthorized()
def getPubKey():
return JWT_PUB_KEY
def decodeToken(token):
try:
return jwt.decode(token, JWT_PUB_KEY, audience="test")
except JWTError as e:
logger.error("{}".format(e))
raise werkzeug.exceptions.Unauthorized()
def testToken(user, token_info):
return '''
You are user_id {user} and the provided token has been signed by this issuers. Fine.'.
Decoded token claims: {token_info}.
'''.format(user=user, token_info=token_info)

View File

@ -4,5 +4,5 @@ IMAGE_NAME="registry.hottis.de/wolutator/authservice"
VERSION=0.0.1
docker build -t ${IMAGE_NAME}:${VERSION} .
docker push ${IMAGE_NAME}:${VERSION}
# docker push ${IMAGE_NAME}:${VERSION}

View File

@ -1,111 +1,61 @@
CREATE DATABASE `authservice`;
USE `authservice`;
create sequence application_s start with 1 increment by 1;
create table application_t (
id integer primary key not null default nextval('application_s'),
name varchar(128) not null unique
);
CREATE TABLE `applications` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL,
CONSTRAINT PRIMARY KEY (`id`),
CONSTRAINT UNIQUE KEY `uk_applications_name` (`name`)
) ENGINE=InnoDB;
create sequence user_s start with 1 increment by 1;
create table user_t (
id integer primary key not null default nextval('user_s'),
login varchar(64) not null unique,
pwhash varchar(64) not null,
expiry integer not null default 600
);
CREATE TABLE `users` (
`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;
create sequence claim_s start with 1 increment by 1;
create table claim_t (
id integer primary key not null default nextval('claim_s'),
key varchar(64) not null,
value varchar(64) not null,
application integer not null references application(id),
unique (key, value)
);
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_claim_mapping_t (
"user" integer not null references user_t(id),
claim integer not null references claim_t(id),
unique ("user", claim)
);
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 table user_application_mapping_t (
"user" integer not null references user_t(id),
application integer not null references application_t(id),
unique ("user", application)
);
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`,
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_application AS
SELECT u.login AS login,
u.pwhash AS pwhash,
u.id AS id,
u.expiry AS expiry,
create or replace view claims_for_user_v as
select u.id as "user",
a.name as application,
c.key as key,
c.value as value
from user_t u,
claim_t c,
user_claim_mapping_t m,
application_t a
where m.user = u.id and
m.claim = c.id and
a.id = c.application;
create or replace view user_application_v as
select u.login as login,
u.pwhash as pwhash,
u.id as id,
u.expiry as expiry,
a.name as application
FROM users u,
applications a,
user_applications_mapping m
WHERE u.id = m.user AND
a.id = m.application;
from user_t u,
application_t a,
user_application_mapping_t 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

@ -4,16 +4,18 @@ info:
version: "0.1"
paths:
/auth:
/token:
post:
tags: [ "JWT" ]
summary: Return JWT token
summary: Accept encrypted or clear set of credentials, return JWT token
operationId: auth.generateToken
requestBody:
content:
'application/json':
schema:
$ref: '#/components/schemas/User'
anyOf:
- $ref: '#/components/schemas/User'
- $ref: '#/components/schemas/EncUser'
responses:
'200':
description: JWT token
@ -21,11 +23,11 @@ paths:
'text/plain':
schema:
type: string
/secret:
/test:
get:
tags: [ "JWT" ]
tags: [ "Test" ]
summary: Return secret string
operationId: test.getSecret
operationId: auth.testToken
responses:
'200':
description: secret response
@ -35,6 +37,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:
@ -42,7 +57,7 @@ components:
type: http
scheme: bearer
bearerFormat: JWT
x-bearerInfoFunc: test.decodeToken
x-bearerInfoFunc: auth.decodeToken
schemas:
User:
description: Application/Login/Password tuple
@ -54,3 +69,9 @@ components:
type: string
password:
type: string
EncUser:
description: Encrypted Application/Login/Password tuple
type: object
properties:
encAleTuple:
type: string

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

24
test.py
View File

@ -1,20 +1,8 @@
from jose import JWTError, jwt
import os
import werkzeug
import connexion
import logging
logging.basicConfig(level=logging.DEBUG)
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)
app = connexion.App('authservice')
app.add_api('./openapi.yaml')
app.run(port=8080)

28
testjwe.py Normal file
View File

@ -0,0 +1,28 @@
import unittest
from jose import jwe
import os
import json
JWT_PUB_KEY = os.environ["JWT_PUB_KEY"]
JWT_PRIV_KEY = os.environ["JWT_PRIV_KEY"]
class JweTestMethods(unittest.TestCase):
def test_encryptDecrypt(self):
inObj = {"application":"test", "login":"wn", "password":"joshua"}
plainText = json.dumps(inObj)
cryptText = jwe.encrypt(plainText, JWT_PUB_KEY, "A256GCM", "RSA-OAEP")
print(cryptText)
clearText = jwe.decrypt(cryptText, JWT_PRIV_KEY)
print(clearText)
outObj = json.loads(clearText)
print(outObj)
self.assertEqual(outObj, inObj)
if __name__ == '__main__':
unittest.main()