quart-security 1.1.0__tar.gz → 1.2.1__tar.gz
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.
- {quart_security-1.1.0 → quart_security-1.2.1}/PKG-INFO +8 -1
- {quart_security-1.1.0 → quart_security-1.2.1}/README.md +7 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/pyproject.toml +1 -1
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/core.py +10 -5
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/forms.py +8 -2
- quart_security-1.2.1/quart_security/utils.py +25 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/views.py +59 -24
- quart_security-1.1.0/quart_security/utils.py +0 -8
- {quart_security-1.1.0 → quart_security-1.2.1}/LICENSE +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/__init__.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/datastore.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/decorators.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/email.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/models.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/password.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/proxies.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/signals.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/_macros.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/change_password.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/login_user.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/mf_recovery.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/mf_recovery_codes.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/register_user.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/two_factor_select.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/two_factor_setup.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/two_factor_verify_code.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/wan_register.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/wan_signin.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/wan_verify.html +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/totp.py +0 -0
- {quart_security-1.1.0 → quart_security-1.2.1}/quart_security/webauthn.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: quart-security
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Native async auth for Quart
|
|
5
5
|
Author: quart-security maintainers
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -55,6 +55,7 @@ It is designed as a practical replacement path for Flask-Security style session
|
|
|
55
55
|
- Flask-Security-style endpoint naming through `url_for_security()`
|
|
56
56
|
- Signals for auth lifecycle events
|
|
57
57
|
- Overridable templates under `templates/security/`
|
|
58
|
+
- Datastore hooks can be implemented as async methods or simple sync methods
|
|
58
59
|
|
|
59
60
|
## Non-Goals (Current Scope)
|
|
60
61
|
|
|
@@ -130,6 +131,10 @@ For tracking / MFA / WebAuthn features:
|
|
|
130
131
|
- `fs_webauthn_user_handle`
|
|
131
132
|
- relationship/association for stored WebAuthn credentials
|
|
132
133
|
|
|
134
|
+
Optional for lockout support:
|
|
135
|
+
- `failed_login_count`
|
|
136
|
+
- `locked_until`
|
|
137
|
+
|
|
133
138
|
## Key Configuration
|
|
134
139
|
|
|
135
140
|
The extension uses `SECURITY_*` keys for migration-friendly configuration.
|
|
@@ -138,6 +143,8 @@ Core:
|
|
|
138
143
|
- `SECURITY_PASSWORD_HASH` (default: `pbkdf2_sha512`)
|
|
139
144
|
- `SECURITY_PASSWORD_SALT` (recommended)
|
|
140
145
|
- `SECURITY_PASSWORD_LENGTH_MIN` (default: `12`)
|
|
146
|
+
- `SECURITY_LOGIN_MAX_ATTEMPTS` (default: `5`)
|
|
147
|
+
- `SECURITY_LOCKOUT_MINUTES` (default: `15`)
|
|
141
148
|
- `SECURITY_REGISTERABLE`
|
|
142
149
|
- `SECURITY_CHANGEABLE`
|
|
143
150
|
- `SECURITY_TRACKABLE`
|
|
@@ -28,6 +28,7 @@ It is designed as a practical replacement path for Flask-Security style session
|
|
|
28
28
|
- Flask-Security-style endpoint naming through `url_for_security()`
|
|
29
29
|
- Signals for auth lifecycle events
|
|
30
30
|
- Overridable templates under `templates/security/`
|
|
31
|
+
- Datastore hooks can be implemented as async methods or simple sync methods
|
|
31
32
|
|
|
32
33
|
## Non-Goals (Current Scope)
|
|
33
34
|
|
|
@@ -103,6 +104,10 @@ For tracking / MFA / WebAuthn features:
|
|
|
103
104
|
- `fs_webauthn_user_handle`
|
|
104
105
|
- relationship/association for stored WebAuthn credentials
|
|
105
106
|
|
|
107
|
+
Optional for lockout support:
|
|
108
|
+
- `failed_login_count`
|
|
109
|
+
- `locked_until`
|
|
110
|
+
|
|
106
111
|
## Key Configuration
|
|
107
112
|
|
|
108
113
|
The extension uses `SECURITY_*` keys for migration-friendly configuration.
|
|
@@ -111,6 +116,8 @@ Core:
|
|
|
111
116
|
- `SECURITY_PASSWORD_HASH` (default: `pbkdf2_sha512`)
|
|
112
117
|
- `SECURITY_PASSWORD_SALT` (recommended)
|
|
113
118
|
- `SECURITY_PASSWORD_LENGTH_MIN` (default: `12`)
|
|
119
|
+
- `SECURITY_LOGIN_MAX_ATTEMPTS` (default: `5`)
|
|
120
|
+
- `SECURITY_LOCKOUT_MINUTES` (default: `15`)
|
|
114
121
|
- `SECURITY_REGISTERABLE`
|
|
115
122
|
- `SECURITY_CHANGEABLE`
|
|
116
123
|
- `SECURITY_TRACKABLE`
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import datetime
|
|
6
5
|
import secrets
|
|
7
6
|
from datetime import timedelta
|
|
8
7
|
|
|
@@ -12,7 +11,7 @@ from .forms import ChangePasswordForm, LoginForm, RegisterForm
|
|
|
12
11
|
from .password import init_password_context
|
|
13
12
|
from .proxies import AnonymousUser, current_user
|
|
14
13
|
from .signals import user_authenticated, user_logged_out
|
|
15
|
-
from .utils import url_for_security
|
|
14
|
+
from .utils import maybe_await, naive_utcnow, url_for_security
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class _SecurityConfig:
|
|
@@ -88,6 +87,10 @@ class Security:
|
|
|
88
87
|
async def _security_load_user():
|
|
89
88
|
await self.load_user()
|
|
90
89
|
|
|
90
|
+
@app.before_websocket
|
|
91
|
+
async def _security_load_user_ws():
|
|
92
|
+
await self.load_user()
|
|
93
|
+
|
|
91
94
|
app.extensions["quart_security_load_user_registered"] = True
|
|
92
95
|
|
|
93
96
|
app.jinja_env.globals.setdefault("url_for_security", url_for_security)
|
|
@@ -127,6 +130,8 @@ class Security:
|
|
|
127
130
|
"SECURITY_WAN_REQUIRE_USER_VERIFICATION": True,
|
|
128
131
|
"SECURITY_FRESHNESS": timedelta(minutes=60),
|
|
129
132
|
"SECURITY_FRESHNESS_GRACE_PERIOD": timedelta(minutes=60),
|
|
133
|
+
"SECURITY_LOGIN_MAX_ATTEMPTS": 5,
|
|
134
|
+
"SECURITY_LOCKOUT_MINUTES": 15,
|
|
130
135
|
"SECURITY_POST_LOGIN_VIEW": "/",
|
|
131
136
|
"SECURITY_POST_REGISTER_VIEW": "/login",
|
|
132
137
|
"SECURITY_EMAIL_SENDER": "noreply@example.com",
|
|
@@ -144,7 +149,7 @@ class Security:
|
|
|
144
149
|
g._current_user = AnonymousUser()
|
|
145
150
|
return
|
|
146
151
|
|
|
147
|
-
user = await self.datastore.find_user(fs_uniquifier=user_id)
|
|
152
|
+
user = await maybe_await(self.datastore.find_user(fs_uniquifier=user_id))
|
|
148
153
|
g._current_user = user or AnonymousUser()
|
|
149
154
|
|
|
150
155
|
async def login_user(self, user, fresh=True):
|
|
@@ -164,11 +169,11 @@ class Security:
|
|
|
164
169
|
if app.config.get("SECURITY_TRACKABLE", True):
|
|
165
170
|
user.last_login_at = getattr(user, "current_login_at", None)
|
|
166
171
|
user.last_login_ip = getattr(user, "current_login_ip", None)
|
|
167
|
-
user.current_login_at =
|
|
172
|
+
user.current_login_at = naive_utcnow()
|
|
168
173
|
user.current_login_ip = request.remote_addr
|
|
169
174
|
user.login_count = (getattr(user, "login_count", None) or 0) + 1
|
|
170
175
|
|
|
171
|
-
await self.datastore.commit()
|
|
176
|
+
await maybe_await(self.datastore.commit())
|
|
172
177
|
await user_authenticated.send_async(app, user=user, authn_via="session")
|
|
173
178
|
|
|
174
179
|
async def logout_user(self, user=None):
|
|
@@ -63,7 +63,10 @@ class RegisterForm(QuartForm):
|
|
|
63
63
|
password = PasswordField("Password", validators=[DataRequired()])
|
|
64
64
|
password_confirm = PasswordField(
|
|
65
65
|
"Confirm Password",
|
|
66
|
-
validators=[
|
|
66
|
+
validators=[
|
|
67
|
+
DataRequired(),
|
|
68
|
+
EqualTo("password", message="Passwords must match"),
|
|
69
|
+
],
|
|
67
70
|
)
|
|
68
71
|
|
|
69
72
|
|
|
@@ -95,7 +98,10 @@ class WebAuthnRegisterForm(QuartForm):
|
|
|
95
98
|
name = StringField("Credential Name", validators=[DataRequired()])
|
|
96
99
|
usage = RadioField(
|
|
97
100
|
"Usage",
|
|
98
|
-
choices=[
|
|
101
|
+
choices=[
|
|
102
|
+
("secondary", "Multi-factor only"),
|
|
103
|
+
("primary", "Passwordless sign-in"),
|
|
104
|
+
],
|
|
99
105
|
default="secondary",
|
|
100
106
|
validators=[DataRequired()],
|
|
101
107
|
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Utility helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import inspect
|
|
7
|
+
|
|
8
|
+
from quart import url_for
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def url_for_security(endpoint, **kwargs):
|
|
12
|
+
"""Flask-Security-compatible helper for endpoint URLs."""
|
|
13
|
+
return url_for(f"security.{endpoint}", **kwargs)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def maybe_await(value):
|
|
17
|
+
"""Await values when needed while supporting sync extension hooks."""
|
|
18
|
+
if inspect.isawaitable(value):
|
|
19
|
+
return await value
|
|
20
|
+
return value
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def naive_utcnow() -> datetime.datetime:
|
|
24
|
+
"""Return a naive UTC datetime while avoiding utcnow() deprecations."""
|
|
25
|
+
return datetime.datetime.now(datetime.UTC).replace(tzinfo=None)
|
|
@@ -32,7 +32,7 @@ from .forms import (
|
|
|
32
32
|
from .password import hash_password, validate_password, verify_password
|
|
33
33
|
from .proxies import _security, current_user
|
|
34
34
|
from .signals import password_changed, tf_profile_changed, user_registered
|
|
35
|
-
from .utils import url_for_security
|
|
35
|
+
from .utils import maybe_await, naive_utcnow, url_for_security
|
|
36
36
|
|
|
37
37
|
security_bp = Blueprint("security", __name__, template_folder="templates")
|
|
38
38
|
|
|
@@ -58,15 +58,15 @@ def _resolve_redirect(config_key: str, fallback_endpoint: str) -> str:
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
async def _find_user(**kwargs):
|
|
61
|
-
return await _security.datastore.find_user(**kwargs)
|
|
61
|
+
return await maybe_await(_security.datastore.find_user(**kwargs))
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
async def _create_user(**kwargs):
|
|
65
|
-
return await _security.datastore.create_user(**kwargs)
|
|
65
|
+
return await maybe_await(_security.datastore.create_user(**kwargs))
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
async def _commit():
|
|
69
|
-
await _security.datastore.commit()
|
|
69
|
+
await maybe_await(_security.datastore.commit())
|
|
70
70
|
|
|
71
71
|
|
|
72
72
|
async def _enforce_csrf(submitted_token: str | None = None):
|
|
@@ -125,7 +125,7 @@ def _webauthn_rp_name() -> str:
|
|
|
125
125
|
def _set_wan_state(key: str, **payload):
|
|
126
126
|
session[key] = {
|
|
127
127
|
"payload": payload,
|
|
128
|
-
"issued_at": int(
|
|
128
|
+
"issued_at": int(naive_utcnow().timestamp()),
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
|
|
@@ -137,7 +137,7 @@ def _pop_wan_state(key: str, max_age_seconds: int = 300) -> dict | None:
|
|
|
137
137
|
payload = state.get("payload")
|
|
138
138
|
if not isinstance(issued_at, int) or not isinstance(payload, dict):
|
|
139
139
|
return None
|
|
140
|
-
age = int(
|
|
140
|
+
age = int(naive_utcnow().timestamp()) - issued_at
|
|
141
141
|
if age < 0 or age > max_age_seconds:
|
|
142
142
|
return None
|
|
143
143
|
return payload
|
|
@@ -146,7 +146,7 @@ def _pop_wan_state(key: str, max_age_seconds: int = 300) -> dict | None:
|
|
|
146
146
|
async def _list_webauthn_credentials(user, usage: str | None = None):
|
|
147
147
|
getter = getattr(_security.datastore, "get_webauthn_credentials", None)
|
|
148
148
|
if getter:
|
|
149
|
-
return await getter(user, usage=usage)
|
|
149
|
+
return await maybe_await(getter(user, usage=usage))
|
|
150
150
|
credentials = list(getattr(user, "webauthn", None) or [])
|
|
151
151
|
if usage:
|
|
152
152
|
credentials = [
|
|
@@ -160,7 +160,7 @@ async def _list_webauthn_credentials(user, usage: str | None = None):
|
|
|
160
160
|
async def _find_webauthn_credential(credential_id: bytes, user=None):
|
|
161
161
|
finder = getattr(_security.datastore, "find_webauthn_credential", None)
|
|
162
162
|
if finder:
|
|
163
|
-
return await finder(credential_id, user=user)
|
|
163
|
+
return await maybe_await(finder(credential_id, user=user))
|
|
164
164
|
|
|
165
165
|
candidates = list(getattr(user, "webauthn", None) or []) if user else []
|
|
166
166
|
for credential in candidates:
|
|
@@ -172,14 +172,14 @@ async def _find_webauthn_credential(credential_id: bytes, user=None):
|
|
|
172
172
|
async def _create_webauthn_credential(user, **kwargs):
|
|
173
173
|
creator = getattr(_security.datastore, "create_webauthn_credential", None)
|
|
174
174
|
if creator:
|
|
175
|
-
return await creator(user, **kwargs)
|
|
175
|
+
return await maybe_await(creator(user, **kwargs))
|
|
176
176
|
raise RuntimeError("Datastore does not implement create_webauthn_credential")
|
|
177
177
|
|
|
178
178
|
|
|
179
179
|
async def _delete_webauthn_credential(user, credential):
|
|
180
180
|
deleter = getattr(_security.datastore, "delete_webauthn_credential", None)
|
|
181
181
|
if deleter:
|
|
182
|
-
return await deleter(user, credential)
|
|
182
|
+
return await maybe_await(deleter(user, credential))
|
|
183
183
|
|
|
184
184
|
user_credentials = getattr(user, "webauthn", None)
|
|
185
185
|
if user_credentials is not None and credential in user_credentials:
|
|
@@ -253,11 +253,33 @@ async def login():
|
|
|
253
253
|
|
|
254
254
|
if _is_post() and form.validate():
|
|
255
255
|
user = await _find_user(email=_normalize_email(form.email.data))
|
|
256
|
+
|
|
257
|
+
# Account lockout check
|
|
258
|
+
if user and hasattr(user, "locked_until") and user.locked_until:
|
|
259
|
+
if datetime.datetime.now() < user.locked_until:
|
|
260
|
+
await flash("Account temporarily locked. Try again later.", "error")
|
|
261
|
+
return await render_template(
|
|
262
|
+
"security/login_user.html", login_user_form=form
|
|
263
|
+
)
|
|
264
|
+
# Lock expired, reset
|
|
265
|
+
user.failed_login_count = 0
|
|
266
|
+
user.locked_until = None
|
|
267
|
+
await _commit()
|
|
268
|
+
|
|
256
269
|
if (
|
|
257
270
|
user
|
|
258
271
|
and getattr(user, "active", True)
|
|
259
272
|
and verify_password(form.password.data, user.password)
|
|
260
273
|
):
|
|
274
|
+
# Reset failed attempts on success
|
|
275
|
+
if (
|
|
276
|
+
hasattr(user, "failed_login_count")
|
|
277
|
+
and (user.failed_login_count or 0) > 0
|
|
278
|
+
):
|
|
279
|
+
user.failed_login_count = 0
|
|
280
|
+
user.locked_until = None
|
|
281
|
+
await _commit()
|
|
282
|
+
|
|
261
283
|
if current_app.config.get("SECURITY_TWO_FACTOR") and getattr(
|
|
262
284
|
user, "tf_primary_method", None
|
|
263
285
|
):
|
|
@@ -267,6 +289,17 @@ async def login():
|
|
|
267
289
|
await _security.login_user(user)
|
|
268
290
|
return redirect(_resolve_redirect("SECURITY_POST_LOGIN_VIEW", "login"))
|
|
269
291
|
|
|
292
|
+
# Track failed login
|
|
293
|
+
if user and hasattr(user, "failed_login_count"):
|
|
294
|
+
user.failed_login_count = (user.failed_login_count or 0) + 1
|
|
295
|
+
max_attempts = current_app.config.get("SECURITY_LOGIN_MAX_ATTEMPTS", 5)
|
|
296
|
+
lockout_minutes = current_app.config.get("SECURITY_LOCKOUT_MINUTES", 15)
|
|
297
|
+
if user.failed_login_count >= max_attempts:
|
|
298
|
+
user.locked_until = datetime.datetime.now() + datetime.timedelta(
|
|
299
|
+
minutes=lockout_minutes
|
|
300
|
+
)
|
|
301
|
+
await _commit()
|
|
302
|
+
|
|
270
303
|
await flash("Invalid email or password", "error")
|
|
271
304
|
|
|
272
305
|
return await render_template("security/login_user.html", login_user_form=form)
|
|
@@ -309,7 +342,7 @@ async def register():
|
|
|
309
342
|
|
|
310
343
|
user = await _create_user(**user_kwargs)
|
|
311
344
|
if hasattr(user, "confirmed_at"):
|
|
312
|
-
user.confirmed_at =
|
|
345
|
+
user.confirmed_at = naive_utcnow()
|
|
313
346
|
|
|
314
347
|
await _commit()
|
|
315
348
|
await user_registered.send_async(current_app._get_current_object(), user=user)
|
|
@@ -429,16 +462,12 @@ async def two_factor_setup():
|
|
|
429
462
|
current_user.tf_primary_method = "authenticator"
|
|
430
463
|
session.pop("tf_pending_secret", None)
|
|
431
464
|
|
|
432
|
-
if current_app.config.get(
|
|
433
|
-
"SECURITY_MULTI_FACTOR_RECOVERY_CODES", True
|
|
434
|
-
):
|
|
465
|
+
if current_app.config.get("SECURITY_MULTI_FACTOR_RECOVERY_CODES", True):
|
|
435
466
|
if not getattr(current_user, "mf_recovery_codes", None):
|
|
436
467
|
count = current_app.config.get(
|
|
437
468
|
"SECURITY_MULTI_FACTOR_RECOVERY_CODES_N", 3
|
|
438
469
|
)
|
|
439
|
-
current_user.mf_recovery_codes = generate_recovery_codes(
|
|
440
|
-
count
|
|
441
|
-
)
|
|
470
|
+
current_user.mf_recovery_codes = generate_recovery_codes(count)
|
|
442
471
|
|
|
443
472
|
await _commit()
|
|
444
473
|
await tf_profile_changed.send_async(
|
|
@@ -505,8 +534,10 @@ async def two_factor_token_validation():
|
|
|
505
534
|
if token and getattr(user, "tf_totp_secret", None):
|
|
506
535
|
valid = verify_totp(user.tf_totp_secret, token)
|
|
507
536
|
|
|
508
|
-
if
|
|
509
|
-
|
|
537
|
+
if (
|
|
538
|
+
not valid
|
|
539
|
+
and token
|
|
540
|
+
and current_app.config.get("SECURITY_MULTI_FACTOR_RECOVERY_CODES", True)
|
|
510
541
|
):
|
|
511
542
|
codes = list(getattr(user, "mf_recovery_codes", None) or [])
|
|
512
543
|
ok, remaining = verify_recovery_code(token, codes)
|
|
@@ -553,7 +584,9 @@ async def mf_recovery_codes():
|
|
|
553
584
|
codes = generate_recovery_codes(count)
|
|
554
585
|
current_user.mf_recovery_codes = codes
|
|
555
586
|
await _commit()
|
|
556
|
-
await tf_profile_changed.send_async(
|
|
587
|
+
await tf_profile_changed.send_async(
|
|
588
|
+
current_app._get_current_object(), user=current_user
|
|
589
|
+
)
|
|
557
590
|
await flash("Recovery codes regenerated.", "success")
|
|
558
591
|
elif request.args.get("show_codes"):
|
|
559
592
|
codes = list(getattr(current_user, "mf_recovery_codes", None) or [])
|
|
@@ -724,7 +757,7 @@ async def wan_register_response():
|
|
|
724
757
|
usage=state.get("usage", "secondary"),
|
|
725
758
|
backup_state=verification.get("backup_state", False),
|
|
726
759
|
device_type=verification.get("device_type") or "single_device",
|
|
727
|
-
lastuse_datetime=
|
|
760
|
+
lastuse_datetime=naive_utcnow(),
|
|
728
761
|
)
|
|
729
762
|
await _commit()
|
|
730
763
|
|
|
@@ -848,7 +881,7 @@ async def wan_signin_response():
|
|
|
848
881
|
|
|
849
882
|
stored_credential.sign_count = int(new_sign_count)
|
|
850
883
|
if hasattr(stored_credential, "lastuse_datetime"):
|
|
851
|
-
stored_credential.lastuse_datetime =
|
|
884
|
+
stored_credential.lastuse_datetime = naive_utcnow()
|
|
852
885
|
|
|
853
886
|
session.permanent = bool(state.get("remember"))
|
|
854
887
|
await _security.login_user(user)
|
|
@@ -930,7 +963,9 @@ async def wan_verify_response():
|
|
|
930
963
|
await flash("Invalid passkey response payload.", "error")
|
|
931
964
|
return redirect(url_for_security("wan_verify"))
|
|
932
965
|
|
|
933
|
-
stored_credential = await _find_webauthn_credential(
|
|
966
|
+
stored_credential = await _find_webauthn_credential(
|
|
967
|
+
credential_id, user=current_user
|
|
968
|
+
)
|
|
934
969
|
if stored_credential is None:
|
|
935
970
|
await flash("Passkey not recognized.", "error")
|
|
936
971
|
return redirect(url_for_security("wan_verify"))
|
|
@@ -952,7 +987,7 @@ async def wan_verify_response():
|
|
|
952
987
|
|
|
953
988
|
stored_credential.sign_count = int(new_sign_count)
|
|
954
989
|
if hasattr(stored_credential, "lastuse_datetime"):
|
|
955
|
-
stored_credential.lastuse_datetime =
|
|
990
|
+
stored_credential.lastuse_datetime = naive_utcnow()
|
|
956
991
|
|
|
957
992
|
session["_fresh"] = True
|
|
958
993
|
await _commit()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/_macros.html
RENAMED
|
File without changes
|
{quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/change_password.html
RENAMED
|
File without changes
|
{quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/login_user.html
RENAMED
|
File without changes
|
{quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/mf_recovery.html
RENAMED
|
File without changes
|
|
File without changes
|
{quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/register_user.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/wan_register.html
RENAMED
|
File without changes
|
{quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/wan_signin.html
RENAMED
|
File without changes
|
{quart_security-1.1.0 → quart_security-1.2.1}/quart_security/templates/security/wan_verify.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|