From 0d698568d4fe6e9b8345587d0968358d98f8d53b Mon Sep 17 00:00:00 2001 From: Wolfgang Hottgenroth Date: Sun, 12 Sep 2021 13:58:14 +0200 Subject: [PATCH] consider client ip --- auth.py | 62 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/auth.py b/auth.py index 66c8f8b..f457c45 100755 --- a/auth.py +++ b/auth.py @@ -12,6 +12,7 @@ from loguru import logger import configparser import random import string +from flask import request DB_USER = "" @@ -123,7 +124,7 @@ def getUserEntryFromDB(application: str, login: str): if conn: conn.close() -def getRefreshTokenFromDB(application, login): +def getRefreshTokenFromDB(application, login, httpClientIp): conn = None cur = None try: @@ -148,8 +149,8 @@ def getRefreshTokenFromDB(application, login): 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, expiry) VALUES (%s, %s, %s) RETURNING id', - (userObj[0], salt, userObj[1])) + cur.execute('INSERT INTO token_t ("user", salt, expiry, client_ip) VALUES (%s, %s, %s, %s) RETURNING id', + (userObj[0], salt, userObj[1], httpClientIp)) tokenObj = cur.fetchone() logger.debug("tokenObj: {}".format(tokenObj)) if not tokenObj: @@ -195,9 +196,11 @@ def generateToken(func, **args): else: raise KeyError("Neither application, login and password nor encAleTuple given") - logger.debug(f"Tuple: {application} {login} {password}") + httpClientIp = request.remote_addr - return func(application, login, password) + logger.debug(f"Tuple: {application} {login} {password} {httpClientIp}") + + return func(application, login, password, httpClientIp) except NoTokenException: logger.error("no token created") raise werkzeug.exceptions.Unauthorized() @@ -221,7 +224,7 @@ def generateToken(func, **args): raise werkzeug.exceptions.Unauthorized() -def _makeSimpleToken(application, login, password, refresh=False): +def _makeSimpleToken(application, login, password, httpClientIp, refresh=False): userEntry = getUserEntry(application, login, password) if not refresh else getUserEntryFromDB(application, login) timestamp = int(time.time()) @@ -239,8 +242,8 @@ def _makeSimpleToken(application, login, password, refresh=False): return jwt.encode(payload, JWT_PRIV_KEY, algorithm='RS256') -def _makeRefreshToken(application, login, password): - refreshTokenEntry = getRefreshTokenFromDB(application, login) +def _makeRefreshToken(application, login, password, httpClientIp): + refreshTokenEntry = getRefreshTokenFromDB(application, login, httpClientIp) timestamp = int(time.time()) payload = { @@ -255,17 +258,17 @@ def _makeRefreshToken(application, login, password): return jwt.encode(payload, JWT_PRIV_KEY, algorithm='RS256') -def _makeRefreshableTokens(application, login, password): - authToken = _makeSimpleToken(application, login, password) - refreshToken = _makeRefreshToken(application, login, password) +def _makeRefreshableTokens(application, login, password, httpClientIp): + authToken = _makeSimpleToken(application, login, password, httpClientIp) + refreshToken = _makeRefreshToken(application, login, password, httpClientIp) return { "authToken": authToken, "refreshToken": refreshToken } def generateSimpleToken(**args): - return generateToken(_makeSimpleToken, **args) + return generateToken(_makeSimpleToken, **args) def generateRefreshableTokens(**args): return generateToken(_makeRefreshableTokens, **args) @@ -286,18 +289,19 @@ def testToken(user, token_info): } -def checkAndInvalidateRefreshToken(login, xid, xal): - conn = None - cur = None +def checkAndInvalidateRefreshToken(login, xid, xal, httpClientIp): try: + validTokenFound = False + 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 ' + + cur.execute('SELECT t.id, t.client_ip FROM token_t t, user_t u' + + ' WHERE t.valid = true AND ' + + ' t.id = %s AND ' + ' t.salt = %s AND ' + ' t."user" = u.id AND ' + ' u.login = %s', @@ -310,9 +314,19 @@ def checkAndInvalidateRefreshToken(login, xid, xal): if invObj: raise ManyTokensException() - with conn.cursor() as cur: - cur.execute('UPDATE token_t SET used = used + 1 WHERE id = %s', - [ xid ]) + if (tokenObj[1] == httpClientIp): + with conn.cursor() as cur: + cur.execute('UPDATE token_t SET used = used + 1 WHERE id = %s', + [ xid ]) + validTokenFound = True + else: + logger.warning(f"Client IP in token {tokenObj[1]} and current one {httpClientIp} does not match") + with conn.cursor() as cur: + cur.execute('UPDATE token_t SET valid = false WHERE id = %s', + [ xid ]) + if (not validTokenFound): + raise NoValidTokenException() + except psycopg2.Error as err: raise Exception("Error when connecting to database: {}".format(err)) finally: @@ -326,13 +340,15 @@ def refreshTokens(**args): refreshTokenObj = jwt.decode(refreshToken, JWT_PUB_KEY) logger.info(str(refreshTokenObj)) + httpClientIp = request.remote_addr + if refreshTokenObj["exp"] < int(time.time()): raise RefreshTokenExpiredException() - checkAndInvalidateRefreshToken(refreshTokenObj["sub"], refreshTokenObj["xid"], refreshTokenObj["xal"]) + checkAndInvalidateRefreshToken(refreshTokenObj["sub"], refreshTokenObj["xid"], refreshTokenObj["xal"], httpClientIp) - authToken = _makeSimpleToken(refreshTokenObj["xap"], refreshTokenObj["sub"], "", refresh=True) - refreshToken = _makeRefreshToken(refreshTokenObj["xap"], refreshTokenObj["sub"], "") + authToken = _makeSimpleToken(refreshTokenObj["xap"], refreshTokenObj["sub"], "", httpClientIp, refresh=True) + refreshToken = _makeRefreshToken(refreshTokenObj["xap"], refreshTokenObj["sub"], "", httpClientIp) return { "authToken": authToken, "refreshToken": refreshToken