Build reusable bot framework
This commit is contained in:
180
core/auth.py
Normal file
180
core/auth.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import datetime
|
||||
import os
|
||||
|
||||
import bcrypt
|
||||
import jwt
|
||||
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
|
||||
|
||||
import core.api_keys as apiKeys
|
||||
import core.postgres as postgres
|
||||
import core.users as users
|
||||
|
||||
|
||||
DEFAULT_TOKEN_LIFETIME = datetime.timedelta(hours=1)
|
||||
|
||||
|
||||
def decodeLoginToken(loginToken):
|
||||
secret = os.getenv("JWT_SECRET")
|
||||
if not secret or not isinstance(loginToken, str):
|
||||
return None
|
||||
try:
|
||||
payload = jwt.decode(loginToken, secret, algorithms=["HS256"])
|
||||
except (ExpiredSignatureError, InvalidTokenError):
|
||||
return None
|
||||
|
||||
userUUID = payload.get("sub")
|
||||
if not isinstance(userUUID, str) or not users.doesUserUUIDExist(userUUID):
|
||||
return None
|
||||
return payload
|
||||
|
||||
|
||||
def createLoginToken(userUUID, name=None, expiresIn=None, extraClaims=None):
|
||||
secret = os.getenv("JWT_SECRET")
|
||||
if not secret:
|
||||
raise RuntimeError("JWT_SECRET is required")
|
||||
if not userUUID or not users.doesUserUUIDExist(userUUID):
|
||||
raise ValueError("user does not exist")
|
||||
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
if expiresIn is None:
|
||||
expiresIn = DEFAULT_TOKEN_LIFETIME
|
||||
elif isinstance(expiresIn, (int, float)):
|
||||
expiresIn = datetime.timedelta(seconds=expiresIn)
|
||||
if not isinstance(expiresIn, datetime.timedelta) or expiresIn.total_seconds() <= 0:
|
||||
raise ValueError("token lifetime must be positive")
|
||||
|
||||
payload = {
|
||||
"sub": str(userUUID),
|
||||
"name": name if name is not None else users.getUserFirstName(userUUID),
|
||||
"iat": now,
|
||||
"exp": now + expiresIn,
|
||||
}
|
||||
if extraClaims is not None:
|
||||
if not isinstance(extraClaims, dict):
|
||||
raise ValueError("extra claims must be an object")
|
||||
protectedClaims = {"sub", "iat", "exp"}
|
||||
payload.update(
|
||||
{
|
||||
key: value
|
||||
for key, value in extraClaims.items()
|
||||
if key not in protectedClaims
|
||||
}
|
||||
)
|
||||
return jwt.encode(payload, secret, algorithm="HS256")
|
||||
|
||||
|
||||
def verifyLoginToken(login_token, username=False, userUUID=False):
|
||||
if username:
|
||||
userUUID = users.getUserUUID(username)
|
||||
|
||||
if userUUID:
|
||||
decodedToken = decodeLoginToken(login_token)
|
||||
if decodedToken and decodedToken.get("sub") == str(userUUID):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def getUserpasswordHash(userUUID):
|
||||
user = postgres.select_one("users", {"id": userUUID})
|
||||
if user:
|
||||
pw_hash = user.get("password_hashed")
|
||||
if isinstance(pw_hash, memoryview):
|
||||
return bytes(pw_hash)
|
||||
return pw_hash
|
||||
return None
|
||||
|
||||
|
||||
def getLoginToken(username, password):
|
||||
if not isinstance(username, str) or not isinstance(password, str):
|
||||
return False
|
||||
userUUID = users.getUserUUID(username)
|
||||
if userUUID:
|
||||
formattedPass = password.encode("utf-8")
|
||||
if not formattedPass or len(formattedPass) > users.MAX_PASSWORD_BYTES:
|
||||
return False
|
||||
usersHashedPassword = getUserpasswordHash(userUUID)
|
||||
if not usersHashedPassword:
|
||||
return False
|
||||
try:
|
||||
if bcrypt.checkpw(formattedPass, usersHashedPassword):
|
||||
return createLoginToken(userUUID)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def unregisterUser(userUUID, password):
|
||||
pw_hash = getUserpasswordHash(userUUID)
|
||||
if not pw_hash or not isinstance(password, str):
|
||||
return False
|
||||
formattedPassword = password.encode("utf-8")
|
||||
if not formattedPassword or len(formattedPassword) > users.MAX_PASSWORD_BYTES:
|
||||
return False
|
||||
try:
|
||||
if bcrypt.checkpw(formattedPassword, pw_hash):
|
||||
return users.deleteUser(userUUID)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def getBearerToken(authorization):
|
||||
if not isinstance(authorization, str):
|
||||
return None
|
||||
scheme, separator, token = authorization.strip().partition(" ")
|
||||
if not separator or scheme.lower() != "bearer" or not token.strip():
|
||||
return None
|
||||
return token.strip()
|
||||
|
||||
|
||||
def authenticateBearerToken(
|
||||
authorization,
|
||||
requiredScopes=None,
|
||||
allowUser=True,
|
||||
allowService=True,
|
||||
):
|
||||
token = getBearerToken(authorization)
|
||||
if not token:
|
||||
return None
|
||||
|
||||
if token.count(".") == 2:
|
||||
payload = decodeLoginToken(token)
|
||||
if payload:
|
||||
if not allowUser or requiredScopes:
|
||||
return None
|
||||
return {
|
||||
"type": "user",
|
||||
"authentication": "jwt",
|
||||
"user_uuid": payload["sub"],
|
||||
"service_name": None,
|
||||
"api_key_id": None,
|
||||
"scopes": [],
|
||||
"can_manage_api_keys": True,
|
||||
"claims": payload,
|
||||
}
|
||||
|
||||
principal = apiKeys.authenticateApiKey(token, requiredScopes=requiredScopes)
|
||||
if not principal:
|
||||
return None
|
||||
if principal["type"] == apiKeys.USER_KEY_TYPE and not allowUser:
|
||||
return None
|
||||
if principal["type"] == apiKeys.SERVICE_KEY_TYPE and not allowService:
|
||||
return None
|
||||
return principal
|
||||
|
||||
|
||||
def isUserPrincipal(principal, userUUID=None, requireLogin=False):
|
||||
if not principal or principal.get("type") != "user":
|
||||
return False
|
||||
if userUUID is not None and str(principal.get("user_uuid")) != str(userUUID):
|
||||
return False
|
||||
if requireLogin and principal.get("authentication") != "jwt":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def hasServiceScope(principal, scope):
|
||||
if not principal or principal.get("type") != "service":
|
||||
return False
|
||||
scopes = principal.get("scopes") or []
|
||||
return "*" in scopes or scope in scopes
|
||||
Reference in New Issue
Block a user