syntaxwebsite/app/util/auth.py

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