273 lines
9.6 KiB
Python
273 lines
9.6 KiB
Python
import hashlib
|
|
import logging
|
|
import time
|
|
import string
|
|
import random
|
|
import uuid
|
|
import pyotp
|
|
|
|
from argon2 import PasswordHasher
|
|
from config import Config
|
|
from flask import request, redirect, jsonify, make_response, abort, g
|
|
from functools import wraps
|
|
|
|
from app.models.user import User
|
|
from app.models.placeservers import PlaceServer
|
|
from app.models.gameservers import GameServer
|
|
from app.extensions import db, redis_controller, get_remote_address
|
|
|
|
config = Config()
|
|
|
|
def ValidateToken( token : str ) -> bool:
|
|
TokenInfo = redis_controller.get("authtoken_" + token)
|
|
if TokenInfo is None:
|
|
return False
|
|
|
|
# Token Info Format
|
|
# userid|created|expiry|ip
|
|
TokenInfo = TokenInfo.split("|")
|
|
if len(TokenInfo) != 4:
|
|
redis_controller.delete("authtoken_" + token)
|
|
return False
|
|
|
|
if int(TokenInfo[2]) < int(time.time()):
|
|
redis_controller.delete("authtoken_" + token)
|
|
return False
|
|
|
|
return True
|
|
|
|
def GetAuthenticatedUser( token : str ) -> User:
|
|
AuthTokenInfo = GetTokenInfo(token)
|
|
if AuthTokenInfo is None:
|
|
return None
|
|
return User.query.filter_by(id=int(AuthTokenInfo[0])).first()
|
|
|
|
def GetTokenInfo( token : str ) -> list:
|
|
TokenInfo = redis_controller.get("authtoken_" + token)
|
|
if TokenInfo is None:
|
|
return None
|
|
|
|
# Token Info Format
|
|
# userid|created|expiry|ip
|
|
TokenInfo = TokenInfo.split("|")
|
|
if len(TokenInfo) != 4:
|
|
redis_controller.delete("authtoken_" + token)
|
|
return None
|
|
|
|
if int(TokenInfo[2]) < int(time.time()):
|
|
redis_controller.delete("authtoken_" + token)
|
|
return None
|
|
|
|
return TokenInfo
|
|
|
|
def CreateToken( userid : int , ip, expireIn : int = (60 * 60 * 24 * 31)) -> str:
|
|
random.seed(str(uuid.uuid4()))
|
|
Token = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(512))
|
|
|
|
# Token Info Format
|
|
# userid|created|expiry|ip
|
|
|
|
TokenInfo = str(userid) + "|" + str(int(time.time())) + "|" + str(int(time.time()) + expireIn) + "|" + ip
|
|
redis_controller.set("authtoken_" + Token, TokenInfo, ex = expireIn)
|
|
|
|
return Token
|
|
|
|
def isAuthenticated():
|
|
if ".ROBLOSECURITY" not in request.cookies:
|
|
return False
|
|
if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
|
|
return False
|
|
return True
|
|
|
|
def authenticated_required(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if ".ROBLOSECURITY" not in request.cookies:
|
|
return redirect("/login")
|
|
if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
|
|
RedirectResponse = make_response(redirect("/login"))
|
|
RedirectResponse.set_cookie(".ROBLOSECURITY", expires=0)
|
|
return RedirectResponse
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
def authenticated_required_api(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if ".ROBLOSECURITY" not in request.cookies:
|
|
return jsonify({"success": False, "message": "You are not logged in"}), 401
|
|
if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
|
|
ErrorResponse = make_response(jsonify({"success": False, "message": "You are not logged in"}), 401)
|
|
ErrorResponse.set_cookie(".ROBLOSECURITY", expires=0)
|
|
return ErrorResponse
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
def authenticated_client_endpoint(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if request.cookies.get("Syntax-Session-Id", type = str ) is not None:
|
|
DataSections = request.cookies.get("Syntax-Session-Id", type = str )
|
|
else:
|
|
return jsonify({"success": False, 'error': 'Missing Headers'}), 401
|
|
|
|
if len(DataSections) != 9:
|
|
return jsonify({"success": False, 'error': 'Invalid Headers'}), 401
|
|
|
|
AuthToken = DataSections[8]
|
|
if not ValidateToken(AuthToken):
|
|
return jsonify({"success": False, 'error': 'Invalid Token'}), 401
|
|
|
|
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=str(DataSections[1])).first()
|
|
if PlaceServerObj is None:
|
|
invalidateToken(AuthToken)
|
|
return jsonify({"success": False, 'error': 'Invalid Token'}), 401
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
def gameserver_authenticated_required(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if "Roblox/" not in request.user_agent.string:
|
|
abort(404)
|
|
RemoteAddress = get_remote_address()
|
|
GameServerObj : GameServer = GameServer.query.filter_by( serverIP = RemoteAddress ).first()
|
|
if GameServerObj is None:
|
|
abort(404)
|
|
requesterAccessKey = request.headers.get( key = "AccessKey", type = str, default = "")
|
|
if "UserRequest" in requesterAccessKey:
|
|
logging.warning(f"GameServer {RemoteAddress} - UserRequest access key used for {request.url}")
|
|
abort(404)
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
def gameserver_accesskey_required(f):
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if "AccessKey" not in request.headers:
|
|
abort(404)
|
|
RequesterAccessKey = request.headers.get( key = "AccessKey", type = str, default = "")
|
|
RemoteAddress = get_remote_address()
|
|
GameServerObj : GameServer = GameServer.query.filter_by( serverIP = RemoteAddress, accessKey = RequesterAccessKey ).first()
|
|
if GameServerObj is None:
|
|
abort(404)
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
def invalidateToken(token : str):
|
|
if token is not None:
|
|
redis_controller.delete("authtoken_" + token)
|
|
|
|
def Validate2FACode( userid : int, code : str ) -> bool:
|
|
from app.pages.settings.settings import generate_secret_key_from_string
|
|
"""
|
|
Validates a 2FA code for a user
|
|
|
|
:param userid: The user id to validate the code for
|
|
:param code: The code to validate
|
|
|
|
:returns: bool (True if correct, False if not)
|
|
"""
|
|
user : User = User.query.filter_by(id=userid).first()
|
|
if user is None:
|
|
return False
|
|
if user.TOTPEnabled == False:
|
|
return False
|
|
totp = pyotp.TOTP(generate_secret_key_from_string(str(user.id) + config.FLASK_SESSION_KEY))
|
|
return totp.verify(code)
|
|
|
|
def GetCurrentUser() -> User:
|
|
"""
|
|
Gets the current user from the request
|
|
|
|
:returns: User (User object if logged in, None if not)
|
|
"""
|
|
|
|
def _get_user() -> User:
|
|
if ".ROBLOSECURITY" not in request.cookies:
|
|
if "Syntax-Session-Id" not in request.cookies:
|
|
return None
|
|
else:
|
|
DataSections = request.cookies["Syntax-Session-Id"].split("|")
|
|
if len(DataSections) != 9:
|
|
return None
|
|
|
|
AuthToken = DataSections[8]
|
|
if not ValidateToken(AuthToken):
|
|
return None
|
|
|
|
PlaceServerObj : PlaceServer = PlaceServer.query.filter_by(serveruuid=str(DataSections[1])).first()
|
|
if PlaceServerObj is None:
|
|
invalidateToken(AuthToken)
|
|
return None
|
|
|
|
AuthTokenInfo = GetTokenInfo(AuthToken)
|
|
return User.query.filter_by(id=int(AuthTokenInfo[0])).first()
|
|
if not ValidateToken(request.cookies[".ROBLOSECURITY"]):
|
|
return None
|
|
AuthTokenInfo = GetTokenInfo(request.cookies[".ROBLOSECURITY"])
|
|
if AuthTokenInfo is None:
|
|
return None
|
|
return User.query.filter_by(id=int(AuthTokenInfo[0])).first()
|
|
|
|
if not hasattr(g, 'current_authenticated_user'):
|
|
g.current_authenticated_user = _get_user()
|
|
return g.current_authenticated_user
|
|
|
|
|
|
def _GetArgonSalt( UserObj : User ) -> bytes:
|
|
return (config.FLASK_SESSION_KEY + str(UserObj.id)).encode('utf-8')
|
|
def _GetPasswordHasher() -> PasswordHasher:
|
|
return PasswordHasher(
|
|
time_cost=16,
|
|
memory_cost=2**14,
|
|
parallelism=2,
|
|
hash_len=32,
|
|
salt_len=16
|
|
)
|
|
|
|
def VerifyPassword( UserObj : User, password : str ) -> bool:
|
|
"""
|
|
Verifies the password of the given user
|
|
|
|
:param UserObj: The user object to verify the password of
|
|
:param password: The password to verify
|
|
|
|
:returns: bool (True if correct, False if not)
|
|
"""
|
|
if config.SWITCH_TO_ARGON_PASSWORD_HASH: # Switch this when we migrate to argon
|
|
argon_ph = _GetPasswordHasher()
|
|
if not UserObj.password.startswith("$argon2id"): # Handle old passwords in sha512
|
|
isCorrect = hashlib.sha512(password.encode('utf-8')).hexdigest() == UserObj.password
|
|
if isCorrect:
|
|
UserObj.password = PasswordHasher().hash(password, salt = _GetArgonSalt(UserObj) )
|
|
logging.info(f"User {UserObj.username} [{UserObj.id}] migrated to argon2id password hash")
|
|
db.session.commit()
|
|
return isCorrect
|
|
|
|
try:
|
|
return argon_ph.verify(UserObj.password, password)
|
|
except:
|
|
return False
|
|
else:
|
|
return hashlib.sha512(password.encode('utf-8')).hexdigest() == UserObj.password
|
|
|
|
def SetPassword( UserObj : User, password : str ):
|
|
"""
|
|
Sets the password of the given user
|
|
|
|
:param UserObj: The user object to set the password of
|
|
:param password: The password to set
|
|
|
|
:returns: bool (True if successful, False if not)
|
|
"""
|
|
if config.SWITCH_TO_ARGON_PASSWORD_HASH: # Switch this when we migrate to argon
|
|
argon_ph = _GetPasswordHasher()
|
|
UserObj.password = argon_ph.hash(password, salt = _GetArgonSalt(UserObj) )
|
|
else:
|
|
UserObj.password = hashlib.sha512(password.encode('utf-8')).hexdigest()
|
|
|
|
db.session.commit()
|
|
return True |