# Kit 启动 token 验证示例

原生 WebView Kit 的后端模块：验证平台签名身份，并在返回 claims 前，以原子方式消费服务端签发、绑定预会话的 nonce。它们不是完整 HTTP 应用，你须完成下方 HTTP／CSRF／Cookie 和会话接入。未签名 `user.getProfile` 仅用于展示，不是凭据。示例不实现独立账号登录协议。

## 运行离线测试

Node 22.13 或更高（使用 `node:sqlite`，Node 22 中为实验功能）：

```sh
npm ci
npm test
```

Python 3.10 或更高，SQLite 3.35 或更高：

```sh
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python -m unittest -v test_verification.py
```

测试生成临时 EC 密钥和 SQLite 数据库，不使用生产秘密、网络或账号。先安装依赖。

## 接入自己的后端

在后端固定配置产品 issuer（`https://ims.buko.app` 或 `https://ims.koee.app`）、Kit ID 和精确 HTTPS Origin。不从请求或 JWT 接受这些值、密钥或 JWKS URL。每进程每 Kit 创建一个验证器以复用缓存。配置 URL 为 `<issuer>/kit-keys/<kit_id>/jwks.json`，HTTPS 重定向会拒绝认证。

Node：

```js
import { NonceStore, createKitVerifier } from './verification.mjs';
const nonces = new NonceStore('/private-data/kit-login.sqlite');
const verifier = createKitVerifier({
  issuer: 'https://ims.koee.app',
  kitId: 'com.example.shop',
  allowedOrigins: ['https://shop.example.com'],
  nonceStore: nonces,
});
// In the nonce route, after checking origin/CSRF and creating/reading your secure cookie:
const nonce = nonces.issue(preSessionIdFromServerCookie);
// In the exchange route, after checking origin/CSRF and reading the same cookie:
const identity = await verifier.verify(tokenFromRequestBody, preSessionIdFromServerCookie);
// Now create your own new authenticated session keyed by [identity.iss, identity.sub].
```

Python：

```python
from verification import NonceStore, KitVerifier
nonces = NonceStore('/private-data/kit-login.sqlite')
verifier = KitVerifier('https://ims.koee.app', 'com.example.shop',
                       ['https://shop.example.com'], nonces)
nonce = nonces.issue(pre_session_id_from_server_cookie)
identity = verifier.verify(token_from_request_body, pre_session_id_from_server_cookie)
# Only now create a new authenticated session keyed by (identity['iss'], identity['sub']).
```

其他产品使用其配置的 issuer。不得使用浏览器单独提交的名称、handle 或资料 `sub` 选择登录账号。

路由需负责：

1. 网站与两个后端路由位于配置的 HTTPS Origin。使用 POST，检查精确 HTTP `Origin` 和框架 CSRF 防护，拒绝跨 Origin，限流并返回 `Cache-Control: no-store`。限制请求体／token 大小，日志及遥测移除 token／nonce。
2. 新生成密码学随机预会话 ID（至少 32 随机字节），设为 HttpOnly、Secure、SameSite Cookie，签发和兑换都绑定该 Cookie。不得从请求体、查询或攻击者选择的已有标识读取预会话 ID。共享／全局 nonce 会允许 login CSRF，应避免。认证后轮换预会话，并生成新认证会话 ID。
3. nonce 路由返回 `nonces.issue(cookieValue)`。网页传给 `KitBridge.call('auth.getLaunchToken', { nonce })`，再在同一预会话将返回 `token` POST 到兑换路由。不将凭据放 URL。尽管桥接接受其他 nonce 格式，示例要求自身的 43 字符随机 nonce。
4. 调用 `verify`，仅成功后创建本站会话。不得捕获验证、数据库或网络错误后继续认证。返回通用未授权响应，不向客户端返回异常诊断或 token 内容。签名展示字段以文字而非 HTML 渲染。
5. SQLite 文件位于私有持久目录。示例用单条 SQL `DELETE ... RETURNING` 原子校验预会话绑定、截止并消费 nonce。多个连接／进程访问**同一文件**安全；多主机／serverless 用共享事务 SQL 存储或原子 Redis 脚本替代，独立本地文件或先读后删不安全。安全清理过期行并保护文件，不作为网页资源暴露。

验证器执行 ES256、专属 `typ`、非空 `kid`、精确 issuer 和单一 audience、允许 Origin、必需 profile scope／claims、JWT 整数秒、正且最多 300 秒的期限及严格到期（`now_ms >= exp * 1000` 拒绝）。仅 `iat` 允许最多超前 60 秒，无全局时钟宽限。获取密钥后及 nonce 存储竞争后再次检查期限。后端时钟应同步。桥接 `expiresAt` 为毫秒，精确等于 `exp * 1000`，不能按 JWT 秒解析。

JWKS 整组缓存最多 300 秒。未知 `kid` 每次查找最多刷新一次（Node 还设 30 秒刷新冷却）。仅获取配置地址；Python 的永久单 key 缓存明确关闭。密钥撤销通知后，在所有验证进程调用 `refreshKeys()`（Node）或 `refresh_keys()`（Python）。刷新／网络失败拒绝认证，不在缓存到期后回退旧密钥。缓存的撤销 key 可能仍可用最多五分钟；正常轮换保留旧公钥 24 小时，不延长 token 期限。

示例采用 nonce 绑定。其他协议可在共享重放存储中原子消费 `(iss, aud, jti)`，保留至 token `exp` 或之后并拒绝重复。不仅移除示例 nonce 检查；替代协议需同时设计请求绑定／login CSRF 和重放规则。

平台同意／访问撤销停止**新的**资料与启动 token 读取，已有签名 token 仍可在 300 秒期限剩余时间验证。离线验证无法检查当前平台同意。平台撤销或 `auth.changed` 不自动终止本站会话；网页需处理事件、清除浏览器状态、调用本站退出路由，并在敏感操作前要求新平台 token。事件是尽力通知，不是后端撤销 webhook 或安全保证。

库参考：[jose remote JWKS](https://github.com/panva/jose/blob/main/docs/jwks/remote/functions/createRemoteJWKSet.md) 与 [PyJWT 用法](https://pyjwt.readthedocs.io/en/2.10.1/usage.html)。
