cherrypy-foundation 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cherrypy_foundation/__init__.py +0 -0
- cherrypy_foundation/components/ColorModes.jinja +70 -0
- cherrypy_foundation/components/Datatable.css +47 -0
- cherrypy_foundation/components/Datatable.jinja +63 -0
- cherrypy_foundation/components/Datatable.js +358 -0
- cherrypy_foundation/components/Field.css +10 -0
- cherrypy_foundation/components/Field.jinja +66 -0
- cherrypy_foundation/components/Field.js +56 -0
- cherrypy_foundation/components/Fields.jinja +4 -0
- cherrypy_foundation/components/Flash.jinja +13 -0
- cherrypy_foundation/components/Icon.jinja +3 -0
- cherrypy_foundation/components/LocaleSelection.jinja +13 -0
- cherrypy_foundation/components/LocaleSelection.js +26 -0
- cherrypy_foundation/components/SideBySideMultiSelect.css +25 -0
- cherrypy_foundation/components/SideBySideMultiSelect.jinja +9 -0
- cherrypy_foundation/components/SideBySideMultiSelect.js +9 -0
- cherrypy_foundation/components/Typeahead.css +55 -0
- cherrypy_foundation/components/Typeahead.jinja +106 -0
- cherrypy_foundation/components/Typeahead.js +8 -0
- cherrypy_foundation/components/__init__.py +51 -0
- cherrypy_foundation/components/tests/__init__.py +0 -0
- cherrypy_foundation/components/tests/test_static.py +90 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.css +2106 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/bootstrap-icons.min.css +5 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- cherrypy_foundation/components/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css +9262 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.css.map +95 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css +6 -0
- cherrypy_foundation/components/vendor/bootstrap5/css/bootstrap.min.css.map +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js +4846 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.js.map +1 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/bootstrap.min.js.map +7 -0
- cherrypy_foundation/components/vendor/bootstrap5/js/color-modes.js +80 -0
- cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.css +849 -0
- cherrypy_foundation/components/vendor/datatables/css/dataTables.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_asc.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_asc_disabled.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_both.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_desc.png +0 -0
- cherrypy_foundation/components/vendor/datatables/images/sort_desc_disabled.png +0 -0
- cherrypy_foundation/components/vendor/datatables/js/dataTables.js +14073 -0
- cherrypy_foundation/components/vendor/datatables/js/dataTables.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.css +556 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/css/buttons.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.js +1700 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/buttons.html5.min.js +8 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.js +2944 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Buttons/js/dataTables.buttons.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.css +13 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/css/fixedHeader.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.js +1202 -0
- cherrypy_foundation/components/vendor/datatables-extensions/FixedHeader/js/dataTables.fixedHeader.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.js +11577 -0
- cherrypy_foundation/components/vendor/datatables-extensions/JSZip/jszip.min.js +13 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.css +194 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/css/responsive.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.js +1861 -0
- cherrypy_foundation/components/vendor/datatables-extensions/Responsive/js/dataTables.responsive.min.js +4 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.js +75023 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/pdfmake.min.js +3 -0
- cherrypy_foundation/components/vendor/datatables-extensions/pdfmake/build/vfs_fonts.js +6 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.css +53 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/css/rowGroup.dataTables.min.css +1 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.js +485 -0
- cherrypy_foundation/components/vendor/datatables-extensions/rowgroup/js/dataTables.rowGroup.min.js +4 -0
- cherrypy_foundation/components/vendor/jquery/jquery.min.js +2 -0
- cherrypy_foundation/components/vendor/multi/LICENSE +7 -0
- cherrypy_foundation/components/vendor/multi/README.md +109 -0
- cherrypy_foundation/components/vendor/multi/multi.css +95 -0
- cherrypy_foundation/components/vendor/multi/multi.js +328 -0
- cherrypy_foundation/components/vendor/popper/popper.js +1825 -0
- cherrypy_foundation/components/vendor/popper/popper.min.js +6 -0
- cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.css +1 -0
- cherrypy_foundation/components/vendor/typeahead/jquery.typeahead.min.js +10 -0
- cherrypy_foundation/error_page.py +94 -0
- cherrypy_foundation/flash.py +50 -0
- cherrypy_foundation/form.py +119 -0
- cherrypy_foundation/logging.py +103 -0
- cherrypy_foundation/passwd.py +65 -0
- cherrypy_foundation/plugins/__init__.py +0 -0
- cherrypy_foundation/plugins/db.py +286 -0
- cherrypy_foundation/plugins/ldap.py +257 -0
- cherrypy_foundation/plugins/restapi.py +74 -0
- cherrypy_foundation/plugins/scheduler.py +287 -0
- cherrypy_foundation/plugins/smtp.py +223 -0
- cherrypy_foundation/plugins/tests/__init__.py +0 -0
- cherrypy_foundation/plugins/tests/test_db.py +118 -0
- cherrypy_foundation/plugins/tests/test_ldap.py +451 -0
- cherrypy_foundation/plugins/tests/test_scheduler.py +100 -0
- cherrypy_foundation/plugins/tests/test_scheduler_db.py +107 -0
- cherrypy_foundation/plugins/tests/test_smtp.py +140 -0
- cherrypy_foundation/sessions.py +93 -0
- cherrypy_foundation/tests/__init__.py +72 -0
- cherrypy_foundation/tests/templates/test_flash.html +9 -0
- cherrypy_foundation/tests/templates/test_form.html +16 -0
- cherrypy_foundation/tests/templates/test_url.html +15 -0
- cherrypy_foundation/tests/test_error_page.py +78 -0
- cherrypy_foundation/tests/test_flash.py +61 -0
- cherrypy_foundation/tests/test_form.py +148 -0
- cherrypy_foundation/tests/test_logging.py +78 -0
- cherrypy_foundation/tests/test_passwd.py +51 -0
- cherrypy_foundation/tests/test_sessions.py +89 -0
- cherrypy_foundation/tests/test_url.py +161 -0
- cherrypy_foundation/tools/__init__.py +0 -0
- cherrypy_foundation/tools/auth.py +263 -0
- cherrypy_foundation/tools/auth_mfa.py +249 -0
- cherrypy_foundation/tools/i18n.py +529 -0
- cherrypy_foundation/tools/jinja2.py +158 -0
- cherrypy_foundation/tools/ratelimit.py +265 -0
- cherrypy_foundation/tools/secure_headers.py +119 -0
- cherrypy_foundation/tools/sessions_timeout.py +167 -0
- cherrypy_foundation/tools/tests/__init__.py +0 -0
- cherrypy_foundation/tools/tests/components/Button.jinja +2 -0
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.po +15 -0
- cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/fr/LC_MESSAGES/messages.po +15 -0
- cherrypy_foundation/tools/tests/locales/messages.pot +2 -0
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +11 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +9 -0
- cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +22 -0
- cherrypy_foundation/tools/tests/test_auth.py +110 -0
- cherrypy_foundation/tools/tests/test_auth_mfa.py +369 -0
- cherrypy_foundation/tools/tests/test_i18n.py +247 -0
- cherrypy_foundation/tools/tests/test_jinja2.py +153 -0
- cherrypy_foundation/tools/tests/test_ratelimit.py +109 -0
- cherrypy_foundation/tools/tests/test_secure_headers.py +200 -0
- cherrypy_foundation/url.py +66 -0
- cherrypy_foundation/widgets.py +48 -0
- cherrypy_foundation-1.0.0.dist-info/METADATA +71 -0
- cherrypy_foundation-1.0.0.dist-info/RECORD +136 -0
- cherrypy_foundation-1.0.0.dist-info/WHEEL +5 -0
- cherrypy_foundation-1.0.0.dist-info/licenses/LICENSE.md +674 -0
- cherrypy_foundation-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# MFA tool for cherrypy
|
|
2
|
+
# Copyright (C) 2022-2026 IKUS Software
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
import datetime
|
|
17
|
+
import logging
|
|
18
|
+
import secrets
|
|
19
|
+
import string
|
|
20
|
+
from typing import Optional
|
|
21
|
+
|
|
22
|
+
import cherrypy
|
|
23
|
+
|
|
24
|
+
from cherrypy_foundation.sessions import session_lock
|
|
25
|
+
|
|
26
|
+
from ..passwd import check_password, hash_password
|
|
27
|
+
|
|
28
|
+
MFA_CODE = '_auth_mfa_code'
|
|
29
|
+
MFA_CODE_TIME = '_auth_mfa_code_time'
|
|
30
|
+
MFA_CODE_ATTEMPT = '_auth_mfa_code_attempt'
|
|
31
|
+
MFA_REDIRECT_URL = '_auth_mfa_redirect_url'
|
|
32
|
+
MFA_TRUSTED_IP_LIST = '_auth_mfa_trusted_ip_list'
|
|
33
|
+
MFA_USER_KEY = '_auth_mfa_user_key'
|
|
34
|
+
MFA_VERIFICATION_TIME = '_auth_mfa_time'
|
|
35
|
+
|
|
36
|
+
MFA_DEFAULT_CODE_TIMEOUT = 10 # minutes
|
|
37
|
+
MFA_DEFAULT_LENGTH = 8
|
|
38
|
+
MFA_DEFAULT_MAX_ATTEMPT = 3
|
|
39
|
+
MFA_DEFAULT_MAX_TRUSTED_IPS = 5
|
|
40
|
+
MFA_DEFAULT_TRUST_DURATION = 43200 # 30 days
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CheckAuthMfa(cherrypy.Tool):
|
|
44
|
+
def __init__(self, priority: int = 75):
|
|
45
|
+
super().__init__(point='before_handler', callable=self.run, priority=priority)
|
|
46
|
+
|
|
47
|
+
# ---- Config helpers ----
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _code_length() -> int:
|
|
51
|
+
"""Return the configured code length."""
|
|
52
|
+
length = cherrypy.request.config.get('tools.auth_mfa.code_length', MFA_DEFAULT_LENGTH)
|
|
53
|
+
return max(1, int(length))
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _code_timeout_minutes() -> int:
|
|
57
|
+
# Lifetime for a one-time MFA code
|
|
58
|
+
return int(cherrypy.request.config.get('tools.auth_mfa.code_timeout', MFA_DEFAULT_CODE_TIMEOUT))
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _max_attempts() -> int:
|
|
62
|
+
return int(cherrypy.request.config.get('tools.auth_mfa.max_attempts', MFA_DEFAULT_MAX_ATTEMPT))
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _max_trusted_ips() -> int:
|
|
66
|
+
return int(cherrypy.request.config.get('tools.auth_mfa.max_trusted_ips', MFA_DEFAULT_MAX_TRUSTED_IPS))
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _trust_duration_minutes() -> int:
|
|
70
|
+
# How long a device/IP remains “MFA-verified”
|
|
71
|
+
return int(cherrypy.request.config.get('tools.auth_mfa.trust_duration', MFA_DEFAULT_TRUST_DURATION))
|
|
72
|
+
|
|
73
|
+
# ---- Core operations ----
|
|
74
|
+
|
|
75
|
+
def generate_code(self) -> str:
|
|
76
|
+
"""
|
|
77
|
+
Generate a random numeric code, store its hash and metadata in the session,
|
|
78
|
+
and return the cleartext so the caller can deliver it out-of-band.
|
|
79
|
+
"""
|
|
80
|
+
if not hasattr(cherrypy.serving, 'session'):
|
|
81
|
+
raise cherrypy.HTTPError(500, 'MFA requires sessions')
|
|
82
|
+
|
|
83
|
+
if not cherrypy.request.login:
|
|
84
|
+
raise cherrypy.HTTPError(401, 'MFA requires an authenticated username')
|
|
85
|
+
|
|
86
|
+
length = self._code_length()
|
|
87
|
+
code = ''.join(secrets.choice(string.digits) for _ in range(length))
|
|
88
|
+
with session_lock() as session:
|
|
89
|
+
session[MFA_USER_KEY] = cherrypy.request.login
|
|
90
|
+
session[MFA_CODE] = hash_password(code)
|
|
91
|
+
session[MFA_CODE_TIME] = session.now()
|
|
92
|
+
session[MFA_CODE_ATTEMPT] = 0
|
|
93
|
+
return code
|
|
94
|
+
|
|
95
|
+
def _is_verified(self) -> bool:
|
|
96
|
+
"""Return True if current user/IP is within the MFA trust window."""
|
|
97
|
+
if not cherrypy.request.login or not hasattr(cherrypy.serving, 'session'):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
# Check if our username match current login user.
|
|
101
|
+
with session_lock() as session:
|
|
102
|
+
if session.get(MFA_USER_KEY) != cherrypy.request.login:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
# Check if the current IP is trusted
|
|
106
|
+
ip_list = session.get(MFA_TRUSTED_IP_LIST) or []
|
|
107
|
+
if cherrypy.request.remote.ip not in ip_list:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
# Check if user ever pass the verification.
|
|
111
|
+
verified_at: Optional[datetime.datetime] = session.get(MFA_VERIFICATION_TIME)
|
|
112
|
+
if not verified_at:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Check trusted duration time
|
|
116
|
+
trust_minutes = self._trust_duration_minutes()
|
|
117
|
+
if (verified_at + datetime.timedelta(minutes=trust_minutes)) <= session.now():
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
def is_code_expired(self) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Return True if the current user's MFA code is absent, expired, or attempts exceeded.
|
|
125
|
+
"""
|
|
126
|
+
# Check session existence first to avoid AttributeError
|
|
127
|
+
if not hasattr(cherrypy.serving, 'session'):
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
with session_lock() as session:
|
|
131
|
+
if session.get(MFA_USER_KEY) != cherrypy.request.login:
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
# Check if code is defined.
|
|
135
|
+
hash_ = session.get(MFA_CODE)
|
|
136
|
+
if not hash_:
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
# Check issued time.
|
|
140
|
+
issued_at: Optional[datetime.datetime] = session.get(MFA_CODE_TIME)
|
|
141
|
+
if not issued_at or (
|
|
142
|
+
(issued_at + datetime.timedelta(minutes=self._code_timeout_minutes())) < session.now()
|
|
143
|
+
):
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
# Check number of attempt.
|
|
147
|
+
attempts = int(session.get(MFA_CODE_ATTEMPT, 0))
|
|
148
|
+
if attempts >= self._max_attempts():
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
# ---- Tool entrypoint ----
|
|
154
|
+
|
|
155
|
+
def run(self, mfa_url: str = '/mfa/', mfa_enabled=True, **_):
|
|
156
|
+
"""
|
|
157
|
+
Gate requests for users who have MFA enabled but are not yet verified.
|
|
158
|
+
mfa_enabled can be a bool or a callable returning bool.
|
|
159
|
+
"""
|
|
160
|
+
enabled = mfa_enabled() if callable(mfa_enabled) else bool(mfa_enabled)
|
|
161
|
+
|
|
162
|
+
# Normalize request path
|
|
163
|
+
request = cherrypy.serving.request
|
|
164
|
+
req_path = request.path_info or '/'
|
|
165
|
+
mfa_path = mfa_url if mfa_url.startswith('/') else '/' + mfa_url
|
|
166
|
+
|
|
167
|
+
# If the current request is the MFA page itself:
|
|
168
|
+
if req_path.rstrip('/') == mfa_path.rstrip('/'):
|
|
169
|
+
# If disabled or already verified, send user back to the original URL (or home)
|
|
170
|
+
if not enabled or self._is_verified():
|
|
171
|
+
raise cherrypy.tools.auth.redirect_to_original_url()
|
|
172
|
+
return # Allow the MFA page handler to render
|
|
173
|
+
|
|
174
|
+
# If MFA is globally disabled for this user/realm, allow through
|
|
175
|
+
if not enabled:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Final gate: redirect to MFA if not verified
|
|
179
|
+
if not self._is_verified():
|
|
180
|
+
cherrypy.tools.auth.save_original_url()
|
|
181
|
+
# Use a relative, safe redirect
|
|
182
|
+
raise cherrypy.HTTPRedirect(mfa_path)
|
|
183
|
+
|
|
184
|
+
# ---- Verification API for the MFA page handler ----
|
|
185
|
+
|
|
186
|
+
def verify_code(self, code: str, persistent: bool = False) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Verify the supplied one-time code. On success:
|
|
189
|
+
- mark session persistent if requested
|
|
190
|
+
- stamp verification time
|
|
191
|
+
- add client IP to trusted list (dedup, cap)
|
|
192
|
+
- clear the code, rotate session id
|
|
193
|
+
"""
|
|
194
|
+
if not hasattr(cherrypy.serving, 'session'):
|
|
195
|
+
return False
|
|
196
|
+
|
|
197
|
+
with session_lock() as session:
|
|
198
|
+
stored_hash = session.get(MFA_CODE)
|
|
199
|
+
is_expired = self.is_code_expired()
|
|
200
|
+
if self.is_code_expired():
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
# Always perform the hash check regardless of expiration
|
|
204
|
+
# to prevent timing attacks
|
|
205
|
+
code_valid = False
|
|
206
|
+
if stored_hash:
|
|
207
|
+
code_valid = check_password(code, stored_hash)
|
|
208
|
+
|
|
209
|
+
# Check all conditions after hash verification
|
|
210
|
+
if is_expired or not code_valid:
|
|
211
|
+
if not is_expired: # Only increment if not expired
|
|
212
|
+
session[MFA_CODE_ATTEMPT] = attempts = int(session.get(MFA_CODE_ATTEMPT, 0)) + 1
|
|
213
|
+
cherrypy.log(
|
|
214
|
+
f'verification failed user={cherrypy.request.login} ip={cherrypy.request.remote.ip} attempts={attempts}',
|
|
215
|
+
context='MFA',
|
|
216
|
+
severity=logging.WARNING,
|
|
217
|
+
)
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
# Success: trust this device/IP for the configured duration
|
|
221
|
+
session[MFA_VERIFICATION_TIME] = session.now()
|
|
222
|
+
ip_list = list({*(session.get(MFA_TRUSTED_IP_LIST) or []), cherrypy.request.remote.ip})
|
|
223
|
+
# Cap the list to avoid unbounded growth (keep most recent N)
|
|
224
|
+
max_ips = self._max_trusted_ips()
|
|
225
|
+
if len(ip_list) > max_ips:
|
|
226
|
+
ip_list = ip_list[-max_ips:]
|
|
227
|
+
session[MFA_TRUSTED_IP_LIST] = ip_list
|
|
228
|
+
|
|
229
|
+
# Clear the one-time code
|
|
230
|
+
session[MFA_CODE] = None
|
|
231
|
+
session[MFA_CODE_TIME] = None
|
|
232
|
+
session[MFA_CODE_ATTEMPT] = 0
|
|
233
|
+
|
|
234
|
+
# Honor “remember this device” option by making the session persistent
|
|
235
|
+
if hasattr(cherrypy.tools, 'sessions_timeout'):
|
|
236
|
+
try:
|
|
237
|
+
cherrypy.tools.sessions_timeout.set_persistent(bool(persistent))
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
# Rotate session id to prevent fixation
|
|
242
|
+
session.regenerate()
|
|
243
|
+
cherrypy.log(
|
|
244
|
+
f'verification successful user={cherrypy.request.login} ip={cherrypy.request.remote.ip}', context='MFA'
|
|
245
|
+
)
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
cherrypy.tools.auth_mfa = CheckAuthMfa()
|