此项目更新比较快~
一个使用 Python、Flask 和 py_webauthn(包名 webauthn)实现的 passkey 注册、登录、OAuth / SSO 认证服务。
- 用户可自定义用户名
- 支持注册、用户名登录和无用户名 passkey 登录
- 浏览器侧接口只返回最小登录状态,不把用户名放进 URL 或普通 UI 响应
- 提供服务端验证 API,方便作为自建 OAuth / SSO 的身份校验入口
- 内置 OAuth authorization code flow
- 内置第三方网页跳转和 OAuth 回跳示例,复用标准 OAuth client 管道
- 内置 link challenge flow 示例:模拟
login.xxxxx跳转到auth.xxxxx/{challenge} - 使用标准 WebAuthn 浏览器接口:
navigator.credentials.create()/navigator.credentials.get() - 后端 passkey 逻辑位于可导入库函数中
- SQLite 本地存储用户和 credential public key
- 可选浏览器遥测:全局性能短路、按用户能力下发、独立统计库和 Management 可视化
- 支持 Chrome、Safari、Firefox、Edge 等现代浏览器
本项目由仓库所有者与 OpenAI Codex 协作开发。该声明由仓库所有者主动保留,用于透明记录 AI-assisted development;项目授权、免责声明和责任限制以 Apache License 2.0 为准。贡献范围见 AI_ATTRIBUTION.md。
想了解完整 OAuth / SSO 接入、安全模型、生产部署、扩展方式或 Agent 友好的项目地图,请从 项目 Wiki 开始。
推荐阅读路径:
- 开发者接入:先读 Quick Start,再读 Authentication Flows
- 生产部署:读 Deployment 和 Security
- AI coding agents / vibe coding:优先读取 Development,再根据任务进入认证、管理或部署页面
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m jstu_passkey.app仓库提供 .github/workflows/build-desktop-apps.yml,可生成包含 Python
运行时、依赖和前端资源的自包含程序:
- Windows x64:
Passkey-Auth-windows-x64.zip - Linux x64:
Passkey-Auth-linux-x64.tar.gz - macOS Intel:
Passkey-Auth-macos-x64.zip - macOS Apple Silicon:
Passkey-Auth-macos-arm64.zip
在 GitHub Actions 中手动运行 Build desktop apps 后,可从该次运行的
Artifacts 下载。推送 V* 标签(例如 V3.0.0)时,产物还会自动附加到对应
GitHub Release。
桌面版本启动后会自动打开浏览器。数据库与固定的 Flask Session Secret 保存在 当前用户的应用数据目录,而不是临时打包目录:
- Windows:
%APPDATA%\Passkey-Auth - macOS:
~/Library/Application Support/Passkey-Auth - Linux:
${XDG_DATA_HOME:-~/.local/share}/Passkey-Auth
认证主库、可选遥测库和固定 Secret 都保存在该目录;遥测库仍只会在启用遥测或 管理员读取统计时创建。
构建产物未进行 Apple Developer ID 或 Windows Authenticode 签名,因此首次启动时 可能出现 Gatekeeper 或 SmartScreen 提示。正式公开分发时建议增加平台代码签名。
当前版本使用全新的 v2 数据结构,默认数据库为
instance/passkeys-v2.sqlite3。旧版 passkeys.sqlite3 不会自动迁移;如显式把
PASSKEY_DATABASE 指向旧库,应用会拒绝启动并提示使用新数据库。
使用一次性 URL 创建首个或额外的全权限管理员:
PORT=5003 \
PASSKEY_ORIGIN=http://localhost:5003 \
.venv/bin/python -m jstu_passkey.app --reregister-admin qpwoeiruty然后访问:
http://localhost:5003/qpwoeiruty
通过反向代理部署时使用真实 HTTPS 地址访问;如需让开发服务器直接监听局域网,
另外设置 HOST=0.0.0.0,并确保 PASSKEY_ORIGIN 与浏览器实际 URL 完全一致。
恢复 token 只能使用 A-Z、a-z、0-9、_、-,长度为 8–128
字符,并且不能与 api、demo、oauth、static、management、
_error 等一级路由冲突。格式或路由冲突会在服务监听端口前输出醒目的启动错误并退出。
管理员创建成功后,一次性 URL 立即失效。登录后打开:
http://localhost:5003/management
管理端支持:
- 用户、Passkey、
admin/login/demo权限及平台白黑名单 - OAuth Client 平台、回调地址、启停和 secret 轮换
- 注册永久开启、关闭或自定义期限临时开启
- 登录历史、管理审计、CSV 导出和日志清理
- 遥测总开关、逐用户采集策略、设备分布统计、CSV 导出和独立数据清理
- 撤销用户会话、停用和删除用户
敏感管理写入除管理员 Session、CSRF 和最近一次 Passkey 验证外,还要求当前
X-Action-Token。该 token 的明文仅在登录或二次 Passkey 验证成功后返回给当前
浏览器;服务端只保存与用户及当前 Session 绑定的 hash。每次成功写入都会在 JSON
中返回 next_action_token,旧 token 立即失效;缺失或重放旧 token 会要求重新完成
Passkey 验证。只读 GET 接口不受影响。
Management UI 还会在支持 WebCrypto 和 Server-Sent Events 的浏览器里建立一个轻量
管理通道:当前页面生成临时 P-256 签名密钥,服务端通过 SSE 推送 nonce 和推荐
ACK 间隔,浏览器回传签名 ACK、页面前后台状态和网络提示。通道启动后,同一
Session 的管理写入还必须带 X-Management-Channel-* 签名 proof;服务端用单调
counter 拒绝重放,并按前后台、低速网络或省流量模式自动放宽心跳间隔。该通道只
证明当前页面仍持有临时私钥和最近 nonce,不替代 Passkey、CSRF、Session 或
X-Action-Token。
CSV 使用 UTF-8 BOM 和稳定英文列名,不包含 credential 公钥、Client Secret hash、session 或 token。登录历史包含原始 IP 和完整 User-Agent,应按部署地的隐私要求使用。
遥测默认关闭。总开关关闭时,请求热路径只进行一次内存布尔检查,不打开遥测 数据库、不修改 HTML、不加载浏览器采集脚本,也不创建网络任务。默认使用内置 Telemetry;也可切换到 jason-telemetry 或任意 HTTP POST 接收端,并选择浏览器直连 或 Passkey-Auth 服务端异步转发。启用后可以针对 匿名访客和每个已登录用户分别选择屏幕、硬件摘要、显示偏好、网络、字体和电池 能力;浏览器在空闲时使用同源 beacon 上报,并按操作系统动态加载匹配模块。完整 设计、隐私边界和浏览器兼容说明见 遥测文档。
完整 OAuth 接入说明见:OAuth 接入开发文档。
然后打开:
http://localhost:XXXX
如果本机 XXXX 端口被占用,可以换端口。WebAuthn 的 origin 必须和浏览器地址一致:
PORT=5001 PASSKEY_ORIGIN=http://localhost:5001 .venv/bin/python -m jstu_passkey.app未设置 PASSKEY_ORIGIN 时,服务会自动使用当前请求地址,例如 http://localhost:5002。
Passkey / WebAuthn 要求安全上下文。localhost 属于浏览器允许的安全上下文;如果部署到线上,请使用 HTTPS,并把 PASSKEY_RP_ID 和 PASSKEY_ORIGIN 改成你的真实域名。
如果要用手机或其他局域网设备测试本机服务,可启动内置本地 HTTPS 反向代理:
.venv/bin/python -m jstu_passkey.local_https_proxy该命令会自动检测本机局域网 IP,生成自签名证书,并用
https://<YOUR_HOSTNAME>.local:5443 作为 PASSKEY_ORIGIN。Flask 后端仍只监听
127.0.0.1:5003,HTTPS 代理会注入可信 X-Forwarded-* 头,并在启动输出里显示检测到的
局域网 IP 方便排查网络。浏览器首次访问会提示证书不受信任,这是自签名证书的预期行为;
证书和私钥保存在系统用户数据目录,不会写入仓库。
如需创建管理员,也可以直接把一次性 token 传给 HTTPS 反代启动器:
.venv/bin/python -m jstu_passkey.local_https_proxy --reregister-admin qpwoeiruty如果自动检测到的 .local 主机名不适合当前网络,可显式指定 origin。WebAuthn 不接受裸
IP 作为 Passkey RP ID,因此这里必须使用本地域名、mDNS 主机名或 hosts / DNS 记录:
.venv/bin/python -m jstu_passkey.local_https_proxy \
--origin https://passkey.local:5443默认配置集中在 jstu_passkey/config.py,运行时仍可通过环境变量覆盖:
PASSKEY_RP_ID=localhost
PASSKEY_ORIGIN=http://localhost:5000
PASSKEY_RP_NAME="JSTU Passkey"
PASSKEY_DATABASE=/path/to/passkeys-v2.sqlite3
PASSKEY_TELEMETRY_DATABASE=/path/to/passkeys-telemetry-v1.sqlite3
FLASK_SECRET_KEY=change-me
PASSKEY_REGISTRATION_ENABLED=false
PASSKEY_HOME_AUTH_ENABLED=true
PASSKEY_SERVER_API_TOKEN=change-this-server-token
PASSKEY_OAUTH_CLIENT_ID=jstu-passkey-client
PASSKEY_OAUTH_CLIENT_SECRET=jstu-passkey-secret
PASSKEY_OAUTH_CLIENT_NAME="Passkey OAuth Client"
PASSKEY_OAUTH_REDIRECT_URIS=http://localhost:8765/api/auth/callback
PASSKEY_OAUTH_CHALLENGE_TTL_SECONDS=300
PASSKEY_TRUST_PROXY_HEADERS=false
PASSKEY_HTTP3_ALT_SVC=
PASSKEY_SERVER_TIMING_ENABLED=truePASSKEY_REGISTRATION_ENABLED 默认关闭,仅作为新数据库尚未保存管理设置时的初始值。
之后可在 /management 中永久开启、关闭或按自定义到期时间临时开启。
PASSKEY_HOME_AUTH_ENABLED 默认开启,主页会加载 main.js,启用 Logo
隐藏注册/登录交互和对应快捷键。设为 false 后,主页仅展示品牌页面。
HTTP/3/QUIC 通常由 Caddy、NGINX、Cloudflare 等 HTTPS 反向代理终止,Flask 开发服务器本身不提供 HTTP/3。线上部署时设置 PASSKEY_ORIGIN=https://auth.xxxxx;如果代理会传递可信 X-Forwarded-* 头,再开启 PASSKEY_TRUST_PROXY_HEADERS=true。确认代理已经支持 HTTP/3 后,可设置 PASSKEY_HTTP3_ALT_SVC='h3=":443"; ma=86400' 让 HTTPS 响应宣告 HTTP/3 替代服务。
PASSKEY_SERVER_TIMING_ENABLED 默认开启,会发送低敏的 Server-Timing: app;dur=...,方便在 Chrome DevTools 的 Network 面板里查看 Flask 应用处理总耗时;不包含 WebAuthn、OAuth、用户或 token 内部细节。
PASSKEY_TELEMETRY_DATABASE 默认使用
instance/passkeys-telemetry-v1.sqlite3。该库与认证主库分离,只有管理员打开
遥测面板读取历史统计,或内置模式收到样本时才会打开。外部模式不会为了本地图表
重复落盘,且只有被选中的适配器会按需加载。jason-telemetry v13 可通过一次性
配对码与 Passkey-Auth 自动协商并保存专用 API Key,无需在浏览器或 Management
响应中显示最终密钥。旧的
PASSKEY_TELEMETRY_TOKEN_URL / PASSKEY_TELEMETRY_API_KEY 仍保留兼容端点,并在
尚未保存 Management 设置时作为旧部署的初始启用信号;新功能不需要把 API key
发送给浏览器。
浏览器中的 Passkey 登录和二次验证统一跳转到 /auth/passkey Logo 页面完成,
验证成功后再返回发起页面。旧的通用 /api/login/options 和
/api/login/verify 已删除,普通业务页面不能直接调用 WebAuthn 验证接口。
浏览器登录或二次验证成功后,前端接口返回登录结果和本次 Session 的 action token:
{"ok": true, "mode": "login", "action_token": "..."}action token 只用于同源浏览器发起敏感状态变更,不是身份信息或服务端 API token。
如需让你的业务后端确认用户身份,由业务后端调用:
POST /api/server/session/verify
Authorization: Bearer $PASSKEY_SERVER_API_TOKEN
Content-Type: application/json请求体可以省略,此时接口会验证当前请求携带的 Flask session cookie;也可以显式传入 cookie,便于服务端转发验证:
{"sessionCookie": "session=..."}验证成功时返回服务端可用的身份信息:
{
"ok": true,
"authenticated": true,
"user": {
"sub": "stable-user-handle",
"id": 1,
"username": "alice",
"createdAt": 1780000000
}
}sub 来自 WebAuthn user handle,适合作为 OAuth / SSO 场景里的稳定用户标识。这个接口默认只有设置了 PASSKEY_SERVER_API_TOKEN 且 Bearer token 匹配时才会返回身份信息。
启动后打开:
http://localhost:5002/demo/oauth
流程:
- OAuth Client 示例页面跳转到
/oauth/authorize - Passkey-Auth 展示极简 Logo 页面,并自动呼出 passkey 验证
- 登录成功后回调到
/demo/oauth/callback?code=...&state=... - 示例回调后端使用
client_id/client_secret/code/redirect_uri换取 token 和用户信息 - 页面显示登录成功或失败
OAuth 相关端点:
GET /oauth/authorize
POST /oauth/authorize/complete
POST /oauth/token
GET /oauth/userinfo
默认 OAuth client:
client_id=jstu-passkey-client
client_secret=jstu-passkey-secret
redirect_uri=http://localhost:5002/demo/oauth/callback
如果你部署到其他域名或端口,设置 PASSKEY_ORIGIN,并用 PASSKEY_OAUTH_CLIENT_ID、PASSKEY_OAUTH_CLIENT_SECRET 和 PASSKEY_OAUTH_REDIRECT_URIS 配置生产 client。PASSKEY_OAUTH_REDIRECT_URIS 支持逗号或换行分隔多个精确 callback 地址。OAuth callback URL 里只携带必要的 code/state,不会携带 username。
本地开发时,默认 OAuth client 也允许 Hyping Web UI 的 callback:
http://localhost:8765/api/auth/callback
如果 Hyping 使用了其他端口、域名或 HTTPS 地址,请把那个精确 callback URL 加到 PASSKEY_OAUTH_REDIRECT_URIS。
OAuth Client 首次从 PASSKEY_OAUTH_CLIENT_* 导入全新数据库,后续在
/management 中管理。旧的 PASSKEY_OAUTH_DEMO_* 配置不再读取。
启动后打开:
http://localhost:5002/demo/third-party
流程:
- 模拟第三方网页生成
state,跳转到/oauth/authorize - Passkey-Auth 展示和根目录一致的极简 Logo 页面,并自动呼出 passkey 验证
- 登录成功后跳回
/demo/third-party/callback?code=...&state=... - 第三方 callback 校验
state,用code调/oauth/token - 第三方 callback 再用
access_token调/oauth/userinfo - 页面展示 callback 参数、token 响应和 userinfo 响应
这个页面会展示 access_token,只用于本地调试和理解 OAuth 回跳流程。生产环境不要把 access token 直接暴露在浏览器页面上。
启动后打开:
http://localhost:5002/demo/link-login
这个示例模拟你描述的域名形态:
https://login.xxxxx/ 原网站登录页
https://auth.xxxxx/oauth/challenge/{challenge}
https://login.xxxxx/callback 原网站回调页
本地流程:
- 原网站页面输入用户名并提交到
/demo/link-login/start - 原网站后端创建一次性
challenge,保存username/state/return_uri/client_id - 浏览器跳转到
/oauth/challenge/{challenge} - Auth WebUI 用该用户名发起 passkey 验证
- 验证成功后,Auth 后端把 challenge 标记为完成,并签发
challenge_result - 浏览器跳回
/demo/link-login/callback?challenge=...&challenge_result=...&state=...&status=success - 原网站 callback 校验自己的
state,再服务端验证challenge_result签名和一次性 challenge 状态,校验成功后登录
注意:status=success 只是便于页面展示,不能作为登录依据。真正可信的是服务端校验后的 challenge_result,并且同一个 challenge 只能消费一次。
如果部署到两个真实子域名,WebAuthn 配置通常类似:
PASSKEY_RP_ID=xxxxx
PASSKEY_ORIGIN=https://auth.xxxxx
PASSKEY_OAUTH_REDIRECT_URIS=https://login.xxxxx/callback
PASSKEY_TRUST_PROXY_HEADERS=true
PASSKEY_HTTP3_ALT_SVC='h3=":443"; ma=86400'PASSKEY_RP_ID=xxxxx 允许 auth.xxxxx 作为 passkey 的 RP 子域使用;浏览器实际打开 Auth WebUI 的 origin 必须和 PASSKEY_ORIGIN 一致。
核心函数在 jstu_passkey.webauthn_service:
build_registration_options(...)verify_registration(...)build_authentication_options(...)verify_authentication(...)
Flask 服务只是其中一种使用方式。