Skip to content

Commit 4bab2d3

Browse files
authored
Merge pull request #417 from ImMin5/master
Add logic to block token issue after repeated attempts
2 parents 7aad2e1 + c2d58af commit 4bab2d3

File tree

3 files changed

+80
-13
lines changed

3 files changed

+80
-13
lines changed

src/spaceone/identity/conf/global_conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
"admin_refresh_max_timeout": 2419200, # 28 days
5858
},
5959
"mfa": {"verify_code_timeout": 300},
60+
"max_issue_attempts": 10,
61+
"issue_block_time": 300,
6062
}
6163

6264
# Handler Settings

src/spaceone/identity/error/error_authentication.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ class ERROR_INVALID_GRANT_TYPE(ERROR_INVALID_ARGUMENT):
2323

2424
class ERROR_UPDATE_PASSWORD_REQUIRED(ERROR_INVALID_ARGUMENT):
2525
_message = "Password reset is required.(user_id = {user_id})"
26+
27+
28+
class ERROR_LOGIN_BLOCKED(ERROR_AUTHENTICATE_FAILURE):
29+
_message = "Login is blocked. Please try again later."

src/spaceone/identity/service/token_service.py

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
22
from typing import List, Tuple
3+
from collections import OrderedDict
34

4-
from spaceone.core import cache
5+
from spaceone.core import cache, config, utils
56
from spaceone.core.auth.jwt import JWTAuthenticator, JWTUtil
67
from spaceone.core.service import *
78
from spaceone.core.service.utils import *
89

10+
911
from spaceone.identity.error.error_authentication import *
1012
from spaceone.identity.error.error_domain import ERROR_DOMAIN_STATE
1113
from spaceone.identity.error.error_mfa import *
@@ -48,6 +50,7 @@ def __init__(self, *args, **kwargs):
4850
self.project_mgr = ProjectManager()
4951
self.project_group_mgr = ProjectGroupManager()
5052
self.workspace_mgr = WorkspaceManager()
53+
self._load_conf()
5154

5255
@transaction()
5356
@convert_model
@@ -78,34 +81,51 @@ def issue(self, params: TokenIssueRequest) -> Union[TokenResponse, dict]:
7881
# Check Domain state is ENABLED
7982
self._check_domain_state(domain_id)
8083

81-
token_mgr = TokenManager.get_token_manager_by_auth_type(params.auth_type)
82-
token_mgr.authenticate(
83-
domain_id, verify_code=verify_code, credentials=credentials
84-
)
84+
try:
85+
token_mgr = TokenManager.get_token_manager_by_auth_type(params.auth_type)
86+
token_mgr.authenticate(
87+
domain_id, verify_code=verify_code, credentials=credentials
88+
)
89+
except Exception as e:
90+
self._increment_issue_attempts(domain_id, credentials)
91+
raise e
8592

8693
user_vo = token_mgr.user
8794
user_mfa = user_vo.mfa.to_dict() if user_vo.mfa else {}
88-
mfa_type = user_mfa.get('mfa_type')
95+
mfa_type = user_mfa.get("mfa_type")
8996
permissions = self._get_permissions_from_required_actions(user_vo)
9097

9198
mfa_user_id = user_vo.user_id
9299

93100
if self._check_login_protocol_with_user_auth_type(params.auth_type, domain_id):
94-
if user_mfa.get("state", "DISABLED") == "ENABLED" and params.auth_type != "MFA":
101+
if (
102+
user_mfa.get("state", "DISABLED") == "ENABLED"
103+
and params.auth_type != "MFA"
104+
):
95105
mfa_manager = MFAManager.get_manager_by_mfa_type(mfa_type)
96106
if mfa_type == "EMAIL":
97107
mfa_email = user_mfa["options"].get("email")
98108
mfa_manager.send_mfa_authentication_email(
99-
user_vo.user_id, domain_id, mfa_email, user_vo.language, credentials
109+
user_vo.user_id,
110+
domain_id,
111+
mfa_email,
112+
user_vo.language,
113+
credentials,
100114
)
101115
mfa_user_id = mfa_email
102116

103117
elif mfa_type == "OTP":
104-
secret_manager: SecretManager = self.locator.get_manager(SecretManager)
118+
secret_manager: SecretManager = self.locator.get_manager(
119+
SecretManager
120+
)
105121
user_secret_id = user_mfa["options"].get("user_secret_id")
106-
otp_secret_key = secret_manager.get_user_otp_secret_key(user_secret_id, domain_id)
122+
otp_secret_key = secret_manager.get_user_otp_secret_key(
123+
user_secret_id, domain_id
124+
)
107125

108-
mfa_manager.set_cache_otp_mfa_secret_key(otp_secret_key, user_vo.user_id, domain_id, credentials)
126+
mfa_manager.set_cache_otp_mfa_secret_key(
127+
otp_secret_key, user_vo.user_id, domain_id, credentials
128+
)
109129

110130
raise ERROR_MFA_REQUIRED(user_id=mfa_user_id, mfa_type=mfa_type)
111131

@@ -117,6 +137,8 @@ def issue(self, params: TokenIssueRequest) -> Union[TokenResponse, dict]:
117137
permissions=permissions,
118138
)
119139

140+
self._clear_issue_attempts(domain_id, credentials)
141+
120142
return TokenResponse(**token_info)
121143

122144
@transaction()
@@ -396,11 +418,17 @@ def _get_user_projects(
396418

397419
return user_projects
398420

399-
def _check_login_protocol_with_user_auth_type(self, user_auth_type: str, domain_id: str) -> bool:
421+
def _check_login_protocol_with_user_auth_type(
422+
self, user_auth_type: str, domain_id: str
423+
) -> bool:
400424
if user_auth_type == "EXTERNAL":
401425
domain: Domain = self.domain_mgr.get_domain(domain_id)
402426
external_auth_mgr = ExternalAuthManager()
403-
external_metadata_protocol = external_auth_mgr.get_auth_info(domain).get('metadata', {}).get('protocol')
427+
external_metadata_protocol = (
428+
external_auth_mgr.get_auth_info(domain)
429+
.get("metadata", {})
430+
.get("protocol")
431+
)
404432

405433
if external_metadata_protocol == "saml":
406434
return False
@@ -413,3 +441,36 @@ def _check_user_required_actions(required_actions: list, user_id: str) -> None:
413441
for required_action in required_actions:
414442
if required_action == "UPDATE_PASSWORD":
415443
raise ERROR_UPDATE_PASSWORD_REQUIRED(user_id=user_id)
444+
445+
def _increment_issue_attempts(self, domain_id: str, credentials: dict) -> None:
446+
if cache.is_set():
447+
ordered_credentials = OrderedDict(sorted(credentials.items()))
448+
hashed_credentials = utils.dict_to_hash(ordered_credentials)
449+
cache_key = f"identity:token:issue-attempt:{domain_id}:{hashed_credentials}"
450+
451+
issue_attempts: int = cache.get(cache_key) or 0
452+
453+
if issue_attempts == 0:
454+
cache.set(cache_key, value=0, expire=self.ISSUE_BLOCK_TIME)
455+
elif issue_attempts == self.MAX_ISSUE_ATTEMPTS:
456+
cache.set(cache_key, value=issue_attempts, expire=self.ISSUE_BLOCK_TIME)
457+
_LOGGER.debug(f"[_increment_login_attempts] {issue_attempts} attempts")
458+
elif issue_attempts > self.MAX_ISSUE_ATTEMPTS:
459+
raise ERROR_LOGIN_BLOCKED()
460+
461+
cache.increment(cache_key)
462+
463+
@staticmethod
464+
def _clear_issue_attempts(domain_id: str, credentials: dict) -> None:
465+
if cache.is_set():
466+
ordered_credentials = OrderedDict(sorted(credentials.items()))
467+
hashed_credentials = utils.dict_to_hash(ordered_credentials)
468+
cache_key = f"identity:token:issue-attempt:{domain_id}:{hashed_credentials}"
469+
cache.delete(cache_key)
470+
471+
def _load_conf(self):
472+
identity_conf = config.get_global("IDENTITY") or {}
473+
token_conf = identity_conf.get("token", {})
474+
475+
self.ISSUE_BLOCK_TIME = token_conf.get("issue_block_time", 300)
476+
self.MAX_ISSUE_ATTEMPTS = token_conf.get("max_issue_attempts", 10)

0 commit comments

Comments
 (0)