1
1
import string
2
2
from http import HTTPStatus
3
+ from threading import Lock
3
4
from typing import Tuple
4
5
5
6
from flask import make_response , request
7
+ from flask_limiter import Limiter , RateLimitExceeded
8
+ from flask_limiter .util import get_remote_address
6
9
7
10
from common .common_consts .token_keys import ACCESS_TOKEN_KEY_NAME , TOKEN_TTL_KEY_NAME
8
11
from common .types import OTP , AgentID
13
16
from ..authentication_facade import AuthenticationFacade
14
17
from .utils import include_auth_token
15
18
19
+ # 100 requests per second is arbitrary, but is expected to be a good-enough limit. Remember that,
20
+ # because of the agent's relay/tunnel capability, many requests could be funneled through the same
21
+ # agent's relay, making them appear to come from the same IP.
22
+ MAX_OTP_LOGIN_REQUESTS_PER_SECOND = 100
23
+
16
24
17
25
class ArgumentParsingException (Exception ):
18
26
pass
@@ -26,8 +34,21 @@ class AgentOTPLogin(AbstractResource):
26
34
"""
27
35
28
36
urls = ["/api/agent-otp-login" ]
37
+ lock = Lock ()
38
+ limiter = None
39
+
40
+ def __init__ (self , authentication_facade : AuthenticationFacade , limiter : Limiter ):
41
+ # Since flask generates a new instance of this class for each request,
42
+ # we need to ensure that a single instance of the limiter is used. Hence
43
+ # the class variable.
44
+ with AgentOTPLogin .lock :
45
+ if AgentOTPLogin .limiter is None :
46
+ AgentOTPLogin .limiter = limiter .limit (
47
+ f"{ MAX_OTP_LOGIN_REQUESTS_PER_SECOND } /second" ,
48
+ key_func = get_remote_address ,
49
+ per_method = True ,
50
+ )
29
51
30
- def __init__ (self , authentication_facade : AuthenticationFacade ):
31
52
self ._authentication_facade = authentication_facade
32
53
33
54
# Secured via OTP, not via authentication token.
@@ -39,6 +60,16 @@ def post(self):
39
60
40
61
:return: Authentication token in the response body
41
62
"""
63
+ if AgentOTPLogin .limiter is None :
64
+ raise RuntimeError ("limiter has not been initialized" )
65
+
66
+ try :
67
+ with AgentOTPLogin .limiter :
68
+ return self ._handle_otp_login_request ()
69
+ except RateLimitExceeded :
70
+ return make_response ("Rate limit exceeded" , HTTPStatus .TOO_MANY_REQUESTS )
71
+
72
+ def _handle_otp_login_request (self ):
42
73
try :
43
74
agent_id , otp = self ._get_request_arguments (request .json )
44
75
except ArgumentParsingException as err :
0 commit comments