feat: implement WebAuthn credential registration, login verification, and management

Complete the WebAuthn/Passkey flow that was previously stubbed out:
- Add ChallengeStore (Redis + TTLCache fallback) for challenge lifecycle
- Add RP config helper to extract rp_id/origin from site settings
- Fix registration start (exclude_credentials, user_id, challenge storage)
- Implement registration finish (verify + create UserAuthn & AuthIdentity)
- Add authentication options endpoint for Discoverable Credentials login
- Fix passkey login to use challenge_token and base64url encoding
- Add credential management endpoints (list/rename/delete)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 12:56:46 +08:00
parent 729773cae3
commit 800c85bf8d
8 changed files with 451 additions and 59 deletions

41
service/webauthn.py Normal file
View File

@@ -0,0 +1,41 @@
"""
WebAuthn RPRelying Party配置辅助模块
从数据库 Setting 中读取 siteURL / siteTitle
解析出 rp_id、rp_name、origin供注册/登录流程复用。
"""
from urllib.parse import urlparse
from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodels.setting import Setting, SettingsType
async def get_rp_config(session: AsyncSession) -> tuple[str, str, str]:
"""
获取 WebAuthn RP 配置。
:param session: 数据库会话
:return: ``(rp_id, rp_name, origin)`` 元组
- ``rp_id``: 站点域名(从 siteURL 解析,如 ``example.com``
- ``rp_name``: 站点标题
- ``origin``: 完整 origin如 ``https://example.com``
"""
site_url_setting: Setting | None = await Setting.get(
session,
(Setting.type == SettingsType.BASIC) & (Setting.name == "siteURL"),
)
site_title_setting: Setting | None = await Setting.get(
session,
(Setting.type == SettingsType.BASIC) & (Setting.name == "siteTitle"),
)
site_url: str = site_url_setting.value if site_url_setting and site_url_setting.value else "https://localhost"
rp_name: str = site_title_setting.value if site_title_setting and site_title_setting.value else "DiskNext"
parsed = urlparse(site_url)
rp_id: str = parsed.hostname or "localhost"
origin: str = f"{parsed.scheme}://{parsed.netloc}" if parsed.netloc else site_url
return rp_id, rp_name, origin