authorization: limit failed login attempts
Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
9d6719ed31
commit
eca4c7e438
|
@ -77,6 +77,8 @@ class Authorization:
|
||||||
self.force_logins = config.getboolean('force_logins', False)
|
self.force_logins = config.getboolean('force_logins', False)
|
||||||
self.default_source = config.get('default_source', "moonraker").lower()
|
self.default_source = config.get('default_source', "moonraker").lower()
|
||||||
self.enable_api_key = config.getboolean('enable_api_key', True)
|
self.enable_api_key = config.getboolean('enable_api_key', True)
|
||||||
|
self.max_logins = config.getint("max_login_attempts", None, above=0)
|
||||||
|
self.failed_logins: Dict[IPAddr, int] = {}
|
||||||
if self.default_source not in AUTH_SOURCES:
|
if self.default_source not in AUTH_SOURCES:
|
||||||
raise config.error(
|
raise config.error(
|
||||||
"[authorization]: option 'default_source' - Invalid "
|
"[authorization]: option 'default_source' - Invalid "
|
||||||
|
@ -279,7 +281,23 @@ class Authorization:
|
||||||
return self.get_oneshot_token(ip, user_info)
|
return self.get_oneshot_token(ip, user_info)
|
||||||
|
|
||||||
async def _handle_login(self, web_request: WebRequest) -> Dict[str, Any]:
|
async def _handle_login(self, web_request: WebRequest) -> Dict[str, Any]:
|
||||||
return await self._login_jwt_user(web_request)
|
ip = web_request.get_ip_address()
|
||||||
|
if ip is not None and self.check_logins_maxed(ip):
|
||||||
|
raise HTTPError(
|
||||||
|
401, "Unauthorized, Maximum Login Attempts Reached"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
ret = await self._login_jwt_user(web_request)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
if ip is not None:
|
||||||
|
failed = self.failed_logins.get(ip, 0)
|
||||||
|
self.failed_logins[ip] = failed + 1
|
||||||
|
raise
|
||||||
|
if ip is not None:
|
||||||
|
self.failed_logins.pop(ip, None)
|
||||||
|
return ret
|
||||||
|
|
||||||
async def _handle_logout(self, web_request: WebRequest) -> Dict[str, str]:
|
async def _handle_logout(self, web_request: WebRequest) -> Dict[str, str]:
|
||||||
user_info = web_request.get_current_user()
|
user_info = web_request.get_current_user()
|
||||||
|
@ -586,6 +604,17 @@ class Authorization:
|
||||||
raise self.server.error("Unknown user", 401)
|
raise self.server.error("Unknown user", 401)
|
||||||
return user_info
|
return user_info
|
||||||
|
|
||||||
|
def validate_jwt(self, token: str) -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
user_info = self.decode_jwt(token)
|
||||||
|
except Exception as e:
|
||||||
|
if isinstance(e, self.server.error):
|
||||||
|
raise
|
||||||
|
raise self.server.error(
|
||||||
|
f"Failed to decode JWT: {e}", 401
|
||||||
|
) from e
|
||||||
|
return user_info
|
||||||
|
|
||||||
def _load_private_key(self, secret: str) -> Signer:
|
def _load_private_key(self, secret: str) -> Signer:
|
||||||
try:
|
try:
|
||||||
key = Signer(bytes.fromhex(secret))
|
key = Signer(bytes.fromhex(secret))
|
||||||
|
@ -707,6 +736,16 @@ class Authorization:
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def check_logins_maxed(self, ip_addr: Union[str, IPAddr]) -> bool:
|
||||||
|
if self.max_logins is None:
|
||||||
|
return False
|
||||||
|
if isinstance(ip_addr, str):
|
||||||
|
try:
|
||||||
|
ip_addr = ipaddress.ip_address(ip_addr) # type: ignore
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return self.failed_logins.get(ip_addr, 0) >= self.max_logins
|
||||||
|
|
||||||
def check_authorized(self,
|
def check_authorized(self,
|
||||||
request: HTTPServerRequest
|
request: HTTPServerRequest
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
@ -744,7 +783,7 @@ class Authorization:
|
||||||
# If the force_logins option is enabled and at least one
|
# If the force_logins option is enabled and at least one
|
||||||
# user is created this is an unauthorized request
|
# user is created this is an unauthorized request
|
||||||
if self.force_logins and len(self.users) > 1:
|
if self.force_logins and len(self.users) > 1:
|
||||||
raise HTTPError(401, "Unauthorized")
|
raise HTTPError(401, "Unauthorized, Force Logins Enabled")
|
||||||
|
|
||||||
# Check if IP is trusted
|
# Check if IP is trusted
|
||||||
trusted_user = self._check_trusted_connection(ip)
|
trusted_user = self._check_trusted_connection(ip)
|
||||||
|
|
|
@ -576,14 +576,7 @@ class BaseSocketClient(Subscribable):
|
||||||
if auth is None:
|
if auth is None:
|
||||||
return
|
return
|
||||||
if token is not None:
|
if token is not None:
|
||||||
try:
|
user_info = auth.validate_jwt(token)
|
||||||
user_info = auth.decode_jwt(token)
|
|
||||||
except self.server.error:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
raise self.server.error(
|
|
||||||
f"Failed to decode JWT: {e}", 401
|
|
||||||
) from e
|
|
||||||
self.user_info = user_info
|
self.user_info = user_info
|
||||||
elif not auth.is_path_permitted(path):
|
elif not auth.is_path_permitted(path):
|
||||||
raise self.server.error("Unauthorized", 401)
|
raise self.server.error("Unauthorized", 401)
|
||||||
|
|
Loading…
Reference in New Issue