refreshable tokens
This commit is contained in:
parent
5b1209679b
commit
b2e1ecab2b
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,2 +1,6 @@
|
||||
__pycache__/
|
||||
ENV
|
||||
ENV
|
||||
config/
|
||||
*~
|
||||
.*~
|
||||
|
||||
|
245
auth.py
245
auth.py
@ -2,48 +2,76 @@ import time
|
||||
import connexion
|
||||
from jose import JWTError, jwt, jwe
|
||||
import json
|
||||
from jose.exceptions import ExpiredSignatureError
|
||||
import werkzeug
|
||||
import os
|
||||
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"]
|
||||
DB_HOST = os.environ["DB_HOST"]
|
||||
DB_NAME = os.environ["DB_NAME"]
|
||||
|
||||
JWT_ISSUER = os.environ["JWT_ISSUER"]
|
||||
|
||||
import configparser
|
||||
import random
|
||||
import string
|
||||
|
||||
|
||||
DB_USER = ""
|
||||
DB_PASS = ""
|
||||
DB_HOST = ""
|
||||
DB_NAME = ""
|
||||
JWT_ISSUER = ""
|
||||
try:
|
||||
DB_USER = os.environ["DB_USER"]
|
||||
DB_PASS = os.environ["DB_PASS"]
|
||||
DB_HOST = os.environ["DB_HOST"]
|
||||
DB_NAME = os.environ["DB_NAME"]
|
||||
JWT_ISSUER = os.environ["JWT_ISSUER"]
|
||||
except KeyError:
|
||||
config = configparser.ConfigParser()
|
||||
config.read('./config/config.ini')
|
||||
DB_USER = config["database"]["user"]
|
||||
DB_PASS = config["database"]["pass"]
|
||||
DB_HOST = config["database"]["host"]
|
||||
DB_NAME = config["database"]["name"]
|
||||
JWT_ISSUER = config["jwt"]["issuer"]
|
||||
|
||||
|
||||
class NoUserException(Exception):
|
||||
pass
|
||||
|
||||
class NoTokenException(Exception):
|
||||
pass
|
||||
|
||||
class NoValidTokenException(Exception):
|
||||
pass
|
||||
|
||||
class ManyUsersException(Exception):
|
||||
pass
|
||||
|
||||
class ManyTokensException(Exception):
|
||||
pass
|
||||
|
||||
class PasswordMismatchException(Exception):
|
||||
pass
|
||||
|
||||
class RefreshTokenExpiredException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
UserEntry = namedtuple('UserEntry', ['id', 'login', 'pwhash', 'expiry', 'claims'])
|
||||
RefreshTokenEntry = namedtuple('RefreshTokenEntry', ['id', 'salt', 'login', 'app', 'expiry'])
|
||||
|
||||
JWT_PRIV_KEY = ""
|
||||
try:
|
||||
JWT_PRIV_KEY = os.environ["JWT_PRIV_KEY"]
|
||||
except KeyError:
|
||||
with open('/opt/app/config/authservice.key', 'r') as f:
|
||||
with open('./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:
|
||||
with open('./config/authservice.pub', 'r') as f:
|
||||
JWT_PUB_KEY = f.read()
|
||||
|
||||
|
||||
@ -92,13 +120,57 @@ def getUserEntryFromDB(application: str, login: str):
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
def getRefreshTokenFromDB(application, login):
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = psycopg2.connect(user = DB_USER, password = DB_PASS,
|
||||
host = DB_HOST, database = DB_NAME)
|
||||
conn.autocommit = False
|
||||
|
||||
with conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT u.id, u.expiry, a.name FROM user_t u, application_t a, user_application_mapping_t m' +
|
||||
' WHERE u.login = %s AND ' +
|
||||
' a.name = %s AND ' +
|
||||
' a.id = m.application AND u.id = m."user"',
|
||||
(login, application))
|
||||
userObj = cur.fetchone()
|
||||
logger.debug("userObj: {}".format(userObj))
|
||||
if not userObj:
|
||||
raise NoUserException()
|
||||
invObj = cur.fetchone()
|
||||
if invObj:
|
||||
raise ManyUsersException()
|
||||
|
||||
with conn.cursor() as cur:
|
||||
salt = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(64))
|
||||
cur.execute('INSERT INTO token_t ("user", salt) VALUES (%s, %s) RETURNING id',
|
||||
(userObj[0], salt))
|
||||
tokenObj = cur.fetchone()
|
||||
logger.debug("tokenObj: {}".format(tokenObj))
|
||||
if not tokenObj:
|
||||
raise NoTokenException()
|
||||
invObj = cur.fetchone()
|
||||
if invObj:
|
||||
raise ManyTokensException()
|
||||
|
||||
refreshTokenEntry = RefreshTokenEntry(id=tokenObj[0], salt=salt, login=login, app=userObj[2], expiry=userObj[1])
|
||||
|
||||
return refreshTokenEntry
|
||||
except psycopg2.Error as err:
|
||||
raise Exception("Error when connecting to database: {}".format(err))
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
def getUserEntry(application, login, password):
|
||||
userEntry = getUserEntryFromDB(application, login)
|
||||
if userEntry.pwhash != crypt(password, userEntry.pwhash):
|
||||
raise PasswordMismatchException()
|
||||
return userEntry
|
||||
|
||||
def generateToken(**args):
|
||||
def generateToken(func, **args):
|
||||
try:
|
||||
body = args["body"]
|
||||
|
||||
@ -121,23 +193,14 @@ def generateToken(**args):
|
||||
raise KeyError("Neither application, login and password nor encAleTuple given")
|
||||
|
||||
logger.debug(f"Tuple: {application} {login} {password}")
|
||||
|
||||
userEntry = getUserEntry(application, login, password)
|
||||
|
||||
timestamp = int(time.time())
|
||||
payload = {
|
||||
"iss": JWT_ISSUER,
|
||||
"iat": int(timestamp),
|
||||
"exp": int(timestamp + userEntry.expiry),
|
||||
"sub": str(userEntry.id),
|
||||
"aud": application
|
||||
}
|
||||
logger.debug("claims: {}".format(userEntry.claims))
|
||||
for claim in userEntry.claims.items():
|
||||
logger.debug("add claim {}".format(claim))
|
||||
payload[claim[0]] = claim[1]
|
||||
|
||||
return jwt.encode(payload, JWT_PRIV_KEY, algorithm='RS256')
|
||||
return func(application, login, password)
|
||||
except NoTokenException:
|
||||
logger.error("no token created")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except ManyTokensException:
|
||||
logger.error("too many tokens created")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except NoUserException:
|
||||
logger.error("no user found, login or application wrong")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
@ -154,6 +217,55 @@ def generateToken(**args):
|
||||
logger.error("unspecific exception: {}".format(str(e)))
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
|
||||
|
||||
def _makeSimpleToken(application, login, password, refresh=False):
|
||||
userEntry = getUserEntry(application, login, password) if not refresh else getUserEntryFromDB(application, login)
|
||||
|
||||
timestamp = int(time.time())
|
||||
payload = {
|
||||
"iss": JWT_ISSUER,
|
||||
"iat": int(timestamp),
|
||||
"exp": int(timestamp + userEntry.expiry),
|
||||
"sub": str(userEntry.id),
|
||||
"aud": application
|
||||
}
|
||||
logger.debug("claims: {}".format(userEntry.claims))
|
||||
for claim in userEntry.claims.items():
|
||||
logger.debug("add claim {}".format(claim))
|
||||
payload[claim[0]] = claim[1]
|
||||
|
||||
return jwt.encode(payload, JWT_PRIV_KEY, algorithm='RS256')
|
||||
|
||||
def _makeRefreshToken(application, login, password):
|
||||
refreshTokenEntry = getRefreshTokenFromDB(application, login)
|
||||
|
||||
timestamp = int(time.time())
|
||||
payload = {
|
||||
"iss": JWT_ISSUER,
|
||||
"iat": int(timestamp),
|
||||
"exp": int(timestamp + refreshTokenEntry.expiry),
|
||||
"sub": str(refreshTokenEntry.login),
|
||||
"xap": str(refreshTokenEntry.app),
|
||||
"xid": str(refreshTokenEntry.id),
|
||||
"xal": str(refreshTokenEntry.salt)
|
||||
}
|
||||
|
||||
return jwt.encode(payload, JWT_PRIV_KEY, algorithm='RS256')
|
||||
|
||||
def _makeRefreshableTokens(application, login, password):
|
||||
authToken = _makeSimpleToken(application, login, password)
|
||||
refreshToken = _makeRefreshToken(application, login, password)
|
||||
return {
|
||||
"authToken": authToken,
|
||||
"refreshToken": refreshToken
|
||||
}
|
||||
|
||||
def generateSimpleToken(**args):
|
||||
return generateToken(_makeSimpleToken, **args)
|
||||
|
||||
def generateRefreshableTokens(**args):
|
||||
return generateToken(_makeRefreshableTokens, **args)
|
||||
|
||||
def getPubKey():
|
||||
return JWT_PUB_KEY
|
||||
|
||||
@ -170,3 +282,82 @@ def testToken(user, token_info):
|
||||
"details": token_info
|
||||
}
|
||||
|
||||
|
||||
def checkAndInvalidateRefreshToken(login, xid, xal):
|
||||
conn = None
|
||||
cur = None
|
||||
try:
|
||||
conn = psycopg2.connect(user = DB_USER, password = DB_PASS,
|
||||
host = DB_HOST, database = DB_NAME)
|
||||
conn.autocommit = False
|
||||
|
||||
with conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('SELECT t.id FROM token_t t, user_t u' +
|
||||
' WHERE t.id = %s AND ' +
|
||||
' t.salt = %s AND ' +
|
||||
' t."user" = u.id AND ' +
|
||||
' u.login = %s AND ' +
|
||||
' t.valid = true',
|
||||
(xid, xal, login))
|
||||
tokenObj = cur.fetchone()
|
||||
logger.debug("tokenObj: {}".format(tokenObj))
|
||||
if not tokenObj:
|
||||
raise NoValidTokenException()
|
||||
invObj = cur.fetchone()
|
||||
if invObj:
|
||||
raise ManyTokensException()
|
||||
|
||||
with conn.cursor() as cur:
|
||||
cur.execute('UPDATE token_t SET valid = false WHERE id = %s',
|
||||
[ xid ])
|
||||
except psycopg2.Error as err:
|
||||
raise Exception("Error when connecting to database: {}".format(err))
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def refreshTokens(**args):
|
||||
try:
|
||||
refreshToken = args["body"]
|
||||
refreshTokenObj = jwt.decode(refreshToken, JWT_PUB_KEY)
|
||||
logger.info(str(refreshTokenObj))
|
||||
|
||||
checkAndInvalidateRefreshToken(refreshTokenObj["sub"], refreshTokenObj["xid"], refreshTokenObj["xal"])
|
||||
|
||||
authToken = _makeSimpleToken(refreshTokenObj["xap"], refreshTokenObj["sub"], "", refresh=True)
|
||||
refreshToken = _makeRefreshToken(refreshTokenObj["xap"], refreshTokenObj["sub"], "")
|
||||
return {
|
||||
"authToken": authToken,
|
||||
"refreshToken": refreshToken
|
||||
}
|
||||
except JWTError as e:
|
||||
logger.error("jwt.decode failed: {}".format(e))
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except NoTokenException:
|
||||
logger.error("no token created")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except NoValidTokenException:
|
||||
logger.error("no valid token found")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except ManyTokensException:
|
||||
logger.error("too many tokens created/found")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except NoUserException:
|
||||
logger.error("no user found, login or application wrong")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except ManyUsersException:
|
||||
logger.error("too many users found")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except PasswordMismatchException:
|
||||
logger.error("wrong password")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
except KeyError:
|
||||
logger.error("application, login or password missing")
|
||||
raise werkzeug.exceptions.Unauthorized()
|
||||
#except Exception as e:
|
||||
# logger.error("unspecific exception: {}".format(str(e)))
|
||||
# raise werkzeug.exceptions.Unauthorized()
|
||||
|
||||
|
||||
|
@ -12,6 +12,14 @@ create table user_t (
|
||||
expiry integer not null default 600
|
||||
);
|
||||
|
||||
create sequence token_s start with 1 increment by 1;
|
||||
create table token_t (
|
||||
id integer primary key not null default nextval('token_s'),
|
||||
"user" integer not null references user_t (id),
|
||||
salt varchar(64) not null,
|
||||
valid boolean not null default true
|
||||
);
|
||||
|
||||
create sequence claim_s start with 1 increment by 1;
|
||||
create table claim_t (
|
||||
id integer primary key not null default nextval('claim_s'),
|
||||
|
55
openapi.yaml
55
openapi.yaml
@ -7,8 +7,8 @@ paths:
|
||||
/token:
|
||||
post:
|
||||
tags: [ "JWT" ]
|
||||
summary: Accept encrypted or clear set of credentials, return JWT token
|
||||
operationId: auth.generateToken
|
||||
summary: Accepts encrypted or clear set of credentials, returns JWT token
|
||||
operationId: auth.generateSimpleToken
|
||||
requestBody:
|
||||
content:
|
||||
'application/json':
|
||||
@ -23,6 +23,42 @@ paths:
|
||||
'text/plain':
|
||||
schema:
|
||||
type: string
|
||||
/refreshable:
|
||||
post:
|
||||
tags: [ "JWT" ]
|
||||
summary: Accepts encrypted or clear set of credentials, returns tuple of AuthToken and RefreshToken
|
||||
operationId: auth.generateRefreshableTokens
|
||||
requestBody:
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
anyOf:
|
||||
- $ref: '#/components/schemas/User'
|
||||
- $ref: '#/components/schemas/EncUser'
|
||||
responses:
|
||||
'200':
|
||||
description: Token tuple
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenTuple'
|
||||
/refresh:
|
||||
post:
|
||||
tags: [ "JWT" ]
|
||||
summary: Accepts refresh token, returns tuple of AuthToken and RefreshToken
|
||||
operationId: auth.refreshTokens
|
||||
requestBody:
|
||||
content:
|
||||
'text/plain':
|
||||
schema:
|
||||
$ref: '#/components/schemas/RefreshToken'
|
||||
responses:
|
||||
'200':
|
||||
description: Token tuple
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenTuple'
|
||||
/test:
|
||||
get:
|
||||
tags: [ "Test" ]
|
||||
@ -83,3 +119,18 @@ components:
|
||||
type: string
|
||||
details:
|
||||
type: object
|
||||
AuthToken:
|
||||
description: Token for authentication purposes, just a string
|
||||
type: string
|
||||
RefreshToken:
|
||||
description: Token for refresh purposes, just a string
|
||||
type: string
|
||||
TokenTuple:
|
||||
description: Test Output
|
||||
type: object
|
||||
properties:
|
||||
authToken:
|
||||
$ref: '#/components/schemas/AuthToken'
|
||||
refreshToken:
|
||||
$ref: '#/components/schemas/RefreshToken'
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user