181 lines
5.5 KiB
Python
181 lines
5.5 KiB
Python
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
|