24 Commits
0.0.2 ... 0.2.1

Author SHA1 Message Date
19b8aab8c2 test 2021-05-11 16:52:41 +02:00
e0bc8371ac adjust for postgres 2021-05-11 16:48:02 +02:00
7346da8419 move to psycopg2 2021-05-11 15:16:13 +02:00
fd9b673df9 change to postgres 2021-05-11 14:31:22 +02:00
c7dbaeabbb add application as aud in token 2021-05-07 14:40:40 +02:00
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
11 changed files with 212 additions and 184 deletions

View File

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

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'

View File

@ -16,15 +16,11 @@ parser.add_argument('--password', '-p',
parser.add_argument('--application', '-a', parser.add_argument('--application', '-a',
help='Application', help='Application',
required=True) required=True)
parser.add_argument('--issuer', '-i',
help='Issuer',
required=True)
args = parser.parse_args() args = parser.parse_args()
user = args.user user = args.user
password = args.password password = args.password
application = args.application application = args.application
issuer = args.issuer
DB_USER = os.environ["DB_USER"] DB_USER = os.environ["DB_USER"]
@ -43,13 +39,9 @@ try:
cur = conn.cursor() cur = conn.cursor()
cur.execute(""" cur.execute("""
INSERT INTO users (issuer, login, password) INSERT INTO users (login, pwhash)
VALUES( VALUES(?, ?)
(SELECT id FROM issuers WHERE name = ?), """, [user, pwhash])
?,
?
)
""", [issuer, user, pwhash])
cur.execute(""" cur.execute("""
INSERT INTO user_applications_mapping (application, user) INSERT INTO user_applications_mapping (application, user)
VALUES( VALUES(

133
auth.py
View File

@ -3,15 +3,21 @@ import connexion
from jose import JWTError, jwt from jose import JWTError, jwt
import werkzeug import werkzeug
import os import os
import mariadb import psycopg2
from collections import namedtuple from collections import namedtuple
from pbkdf2 import crypt from pbkdf2 import crypt
from loguru import logger
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): class NoUserException(Exception):
pass pass
@ -23,62 +29,71 @@ class PasswordMismatchException(Exception):
pass pass
UserEntry = namedtuple('UserEntry', ['id', 'login', 'issuer', 'secret', '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): def getUserEntryFromDB(application: str, login: str):
conn = None conn = None
cur = None cur = None
try: try:
conn = mariadb.connect(user = DB_USER, password = DB_PASS, conn = psycopg2.connect(user = DB_USER, password = DB_PASS,
host = DB_HOST, database = DB_NAME) host = DB_HOST, database = DB_NAME)
conn.autocommit = False conn.autocommit = False
cur = conn.cursor(dictionary=True) userObj = None
cur.execute("SELECT id, password, issuer, secret, expiry FROM user_application_and_issuer " + with conn.cursor() as cur:
" WHERE application = ? AND login = ?", cur.execute("SELECT id, pwhash, expiry FROM user_application_v" +
[application, login]) " WHERE application = %s AND login = %s",
resObj = cur.next() (application, login))
print("DEBUG: getUserEntryFromDB: resObj: {}".format(resObj)) userObj = cur.fetchone()
if not resObj: logger.debug("getUserEntryFromDB: userObj: {}".format(userObj))
raise NoUserException() if not userObj:
invObj = cur.next() raise NoUserException()
if invObj: invObj = cur.fetchone()
raise ManyUsersException() if invObj:
raise ManyUsersException()
userId = resObj["id"]
cur.execute("SELECT user, `key`, `value` FROM claims_for_user where user = ?",
[userId])
claims = {} claims = {}
for claimObj in cur: with conn.cursor() as cur:
print("DEBUG: getUserEntryFromDB: add claim {} -> {}".format(claimObj["key"], claimObj["value"])) cur.execute('SELECT key, value FROM claims_for_user_v where "user" = %s and application = %s',
if claimObj["key"] in claims: (userObj[0], application))
if isinstance(claimObj["key"], list): for claimObj in cur:
claims[claimObj["key"]].append(claimObj["value"]) logger.debug("getUserEntryFromDB: 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: else:
claims[claimObj["key"]] = [ claims[claimObj["key"]] ] claims[claimObj[0]] = claimObj[1]
claims[claimObj["key"]].append(claimObj["value"])
else:
claims[claimObj["key"]] = claimObj["value"]
userEntry = UserEntry(id=userId, login=login, userEntry = UserEntry(id=userObj[0], login=login, pwhash=userObj[1], expiry=userObj[2], claims=claims)
secret=resObj["secret"], issuer=resObj["issuer"],
expiry=resObj["expiry"], claims=claims)
return userEntry, resObj["password"] return userEntry
except mariadb.Error as err: except psycopg2.Error as err:
raise Exception("Error when connecting to database: {}".format(err)) raise Exception("Error when connecting to database: {}".format(err))
finally: finally:
if cur:
cur.close()
if conn: if conn:
conn.rollback()
conn.close() conn.close()
def getUserEntry(application, login, password): def getUserEntry(application, login, password):
userEntry, pwhash = getUserEntryFromDB(application, login) userEntry = getUserEntryFromDB(application, login)
if pwhash != crypt(password, pwhash): if userEntry.pwhash != crypt(password, userEntry.pwhash):
raise PasswordMismatchException() raise PasswordMismatchException()
return userEntry return userEntry
@ -93,28 +108,52 @@ def generateToken(**args):
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),
"aud": application
} }
logger.debug("claims: {}".format(userEntry.claims))
for claim in userEntry.claims.items(): for claim in userEntry.claims.items():
# print("DEBUG: generateToken: add claim {} -> {}".format(claim[0], claim[1])) logger.debug("generateToken: add claim {}".format(claim))
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 application wrong") logger.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") logger.error("generateToken: too many users found")
raise werkzeug.exceptions.Unauthorized() raise werkzeug.exceptions.Unauthorized()
except PasswordMismatchException: except PasswordMismatchException:
print("ERROR: generateToken: wrong password") logger.error("generateToken: wrong password")
raise werkzeug.exceptions.Unauthorized() raise werkzeug.exceptions.Unauthorized()
except KeyError: except KeyError:
print("ERROR: generateToken: application, login or password missing") logger.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))) logger.error("generateToken: unspecific exception: {}".format(str(e)))
raise werkzeug.exceptions.Unauthorized() 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
def decodeToken(token):
try:
return jwt.decode(token, JWT_PUB_KEY, audience="test")
except JWTError as e:
logger.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)

View File

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

View File

@ -1,106 +1,61 @@
CREATE TABLE `issuers` ( create sequence application_s start with 1 increment by 1;
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, create table application_t (
`name` varchar(128) NOT NULL, id integer primary key not null default nextval('application_s'),
`secret` varchar(128) NOT NULL, name varchar(128) not null unique
`max_expiry` int(10) NOT NULL, );
CONSTRAINT PRIMARY KEY (`id`),
CONSTRAINT UNIQUE KEY `uk_issuers_name` (`name`)
) ENGINE=InnoDB;
ALTER TABLE `issuers` create sequence user_s start with 1 increment by 1;
MODIFY COLUMN `max_expiry` int(10) unsigned NOT NULL; 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 `applications` ( create sequence claim_s start with 1 increment by 1;
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, create table claim_t (
`name` varchar(128) NOT NULL, id integer primary key not null default nextval('claim_s'),
CONSTRAINT PRIMARY KEY (`id`), key varchar(64) not null,
CONSTRAINT UNIQUE KEY `uk_applications_name` (`name`) value varchar(64) not null,
) ENGINE=InnoDB; application integer not null references application(id),
unique (key, value)
);
CREATE TABLE `users` ( create table user_claim_mapping_t (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, "user" integer not null references user_t(id),
`login` varchar(64) NOT NULL, claim integer not null references claim_t(id),
`password` varchar(64) NOT NULL, unique ("user", claim)
CONSTRAINT PRIMARY KEY (`id`), );
CONSTRAINT UNIQUE KEY `uk_users_login` (`login`)
) ENGINE=InnoDB;
ALTER TABLE `users` create table user_application_mapping_t (
ADD COLUMN issuer int(10) unsigned; "user" integer not null references user_t(id),
application integer not null references application_t(id),
unique ("user", application)
);
ALTER TABLE `users` create or replace view claims_for_user_v as
MODIFY COLUMN issuer int(10) unsigned NOT NULL; select u.id as "user",
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;
ALTER TABLE `users`
MODIFY COLUMN expiry int(10) unsigned NOT NULL DEFAULT 600;
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_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_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_and_issuer AS
SELECT u.login AS login,
u.password AS password,
u.id AS id,
a.name as application, a.name as application,
i.name AS issuer, c.key as key,
i.secret AS secret, c.value as value
least(u.expiry, i.max_expiry) AS expiry from user_t u,
FROM users u, claim_t c,
issuers i, user_claim_mapping_t m,
applications a, application_t a
user_applications_mapping m where m.user = u.id and
WHERE u.issuer = i.id AND m.claim = c.id and
u.id = m.user AND a.id = c.application;
a.id = m.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 user_t u,
application_t a,
user_application_mapping_t m
where u.id = m.user and
a.id = m.application;

View File

@ -7,7 +7,7 @@ paths:
/auth: /auth:
post: post:
tags: [ "JWT" ] tags: [ "JWT" ]
summary: Return JWT token summary: Accept login and password, return JWT token
operationId: auth.generateToken operationId: auth.generateToken
requestBody: requestBody:
content: content:
@ -21,11 +21,28 @@ paths:
'text/plain': 'text/plain':
schema: schema:
type: string 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: /secret:
get: get:
tags: [ "JWT" ] tags: [ "Test" ]
summary: Return secret string summary: Return secret string
operationId: test.getSecret operationId: auth.getSecret
responses: responses:
'200': '200':
description: secret response description: secret response
@ -35,6 +52,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:
@ -42,7 +72,7 @@ components:
type: http type: http
scheme: bearer scheme: bearer
bearerFormat: JWT bearerFormat: JWT
x-bearerInfoFunc: test.decodeToken x-bearerInfoFunc: auth.decodeToken
schemas: schemas:
User: User:
description: Application/Login/Password tuple description: Application/Login/Password tuple

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)

24
test.py
View File

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

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)