Passkey bug fix in gunicorn

This commit is contained in:
喆OvO
2026-08-13 12:50:18 +08:00
parent 15b54f204e
commit 530b8a8269
4 changed files with 131 additions and 16 deletions
+23 -2
View File
@@ -544,7 +544,7 @@ def api_webauthn_register_complete():
uid = _cur()
try:
result = complete_registration(request, credential)
result = complete_registration(request, credential, user_id=uid)
if result.get("user_id") != uid:
return jsonify({"error": "注册上下文不匹配,请重试"}), 400
update_auth_config(
@@ -569,6 +569,27 @@ def api_webauthn_register_complete():
return jsonify({"error": f"Passkey 注册失败: {e}"}), 500
@api.route("/api/auth/passkey", methods=["DELETE"])
@login_required
def api_remove_passkey():
"""Remove the current user's Passkey (re-auth required).
If this leaves the account with no MFA method at all, the account is
flagged must_configure_mfa so the next login forces MFA setup again
(same guard as a fresh temp account).
"""
if not require_reauth():
return jsonify({"error": "需要重新验证身份", "reauth_required": True}), 403
uid = _cur()
cfg = get_auth_config(user_id=uid)
if not cfg.get("passkey_credential_id"):
return jsonify({"error": "未配置 Passkey"}), 400
update_auth_config(uid, passkey_credential_id="", passkey_public_key="")
if not has_mfa_configured(user_id=uid):
update_auth_config(uid, must_configure_mfa=1)
return jsonify({"ok": True})
# ─── TOTP Setup ──────────────────────────────
@api.route("/api/auth/totp/setup", methods=["POST"])
@@ -974,7 +995,7 @@ def admin_provision_passkey_complete(user_id):
return jsonify({"error": "用户不存在"}), 404
try:
result = complete_registration(request, credential)
result = complete_registration(request, credential, user_id=user_id)
if result.get("user_id") != user_id:
return jsonify({"error": "注册上下文不匹配,请重试"}), 400
update_auth_config(
+23 -12
View File
@@ -153,32 +153,43 @@ def begin_registration(request, user_id: int, display_name: str = "",
return _serialize_options(options)
def complete_registration(request, credential: dict) -> dict:
def complete_registration(request, credential: dict, user_id: int = None) -> dict:
"""Complete WebAuthn registration.
Returns {'ok': True, 'user_id': int, 'credential_id': str, 'public_key': str}
or raises an exception on failure.
user_id: explicit target user. Always pass it from the route when known —
admin provisioning runs inside the ADMIN's session (its uid differs from
the target user, so deriving the challenge key from the session would never
match). When omitted, it is derived from credential.user_name ("user_5" → 5),
then from the authenticated request context.
"""
rp_id = _get_rp_id(request)
origin = _get_origin(request)
user_name = credential.get("user_name", "")
# Derive the target user id from the registration challenge key.
expected_challenge = pop_webauthn_challenge(f"reg_{user_name}", db_path=_challenge_db())
if not expected_challenge:
# Self-registration fallback: the browser may not echo the user handle,
# so resolve the target from the authenticated request context.
uid = user_id
if uid is None:
uname = credential.get("user_name", "")
if isinstance(uname, str) and uname.startswith("user_"):
try:
uid = int(uname.split("_")[-1])
except (ValueError, IndexError):
uid = None
if uid is None:
try:
from backend.auth import current_user_id
uid = current_user_id()
if uid is not None:
expected_challenge = pop_webauthn_challenge(f"reg_{uid}", db_path=_challenge_db())
user_name = f"user_{uid}"
except Exception:
pass
uid = None
if uid is None:
raise ValueError("No registration challenge found. Please restart registration.")
# Challenge key must match begin_registration's f"reg_{user_id}" exactly.
expected_challenge = pop_webauthn_challenge(f"reg_{uid}", db_path=_challenge_db())
if not expected_challenge:
raise ValueError("No registration challenge found. Please restart registration.")
user_id = int(user_name.split("_")[-1])
user_id = uid
try:
# Library expects rawId field (browser-native camelCase, base64url string)