# 统一认证运行时配置与标准应用接入

## 状态

Accepted，2026-07-17。

## 决策

Gateway 是标准应用接入协议的首个消费方。认证中心保持消费方无关；“统一认证”页面、Gateway 本地租户映射、Legacy JWT 兼容和 Session 策略均属于 Gateway 自己的管理边界。

OIDC、JIT、Introspection、BFF 和 SSF 的业务配置以 Gateway 管理 API/数据库为唯一来源。数据库、SecretStore、Bootstrap 管理凭据、TLS 与 RBAC 仍由部署环境提供；Machine Credential 和 Session Encryption Key 由服务端生成或领取，只存入 SecretStore，Revision 仅保存引用。开发阶段不读取或迁移旧 `OIDC_*`、`VITE_OIDC_*` 业务变量，升级后由管理员重新配对。

## Revision 状态机

```text
draft -> validated -> active
draft | validated -> failed
active -> active (重新验证)
active -> superseded (禁用)
superseded (只读历史，不可重新验证或激活)
```

同一时刻最多一个 Active Revision。Revision 保存非敏感配置与 Secret 引用，不保存机器 Secret 或 Session 加密根。Auth Center 当前会复用并修改远端 OAuth/SSF 资源，因此旧 Revision 无法证明 Redirect URI、Scope、Audience、Client Secret 和 Stream 仍与历史快照一致。直接回滚以及 `superseded -> validated -> active` 均被服务端拒绝；恢复旧配置也必须先禁用，再使用新接入码完成一次新的远端交接。

## 管理员标准流程

1. 先确认至少一个本地 `source=gateway`、已启用且具有 Manager/Admin 权限的 Break-glass 密码凭据可登录。
2. 若已有 Active，先点击“禁用统一认证”。启用 SSF 时，Gateway 会先删除远端 Stream；只有连接进入 `retiring` 后才封存 Active 和删除 BFF Session。
3. 在 Auth Center 通用 Application 的“应用接入”向导选择能力、确认预览并生成一张新的接入码。
4. 在 Gateway 填写 Auth Center、接入码、Gateway API/Web 地址和本地租户映射。
5. 等待 Pairing `completed`，保存本地策略，执行“验证配置”，再执行“激活配置”。

当前流程是受控停机切换，不宣称跨配置零停机。要支持 Active 直接换绑或历史 Revision 一键回滚，必须先在 Auth Center 实现远端 OAuth/SSF 资源版本化与可证明的双版本交接。

## 激活顺序

1. 从 Draft 和 SecretStore 构建不可变候选 Runtime。
2. 验证 Discovery、Issuer、JWKS、Audience、Tenant、Client、本地租户、Introspection 和可选 SSF Discovery。
3. 确认存在可用的本地 Break-glass Manager。
4. 在数据库事务中确认没有其他 Active 或配对启动预留，再把 Revision 原子设为 Active。
5. 原子替换进程内 Runtime 引用；在途请求继续使用旧实例。
6. 数据库提交结果不确定时，运行时先 fail closed，再由后台协调器按数据库中的唯一 Active 自动重建；不得部分启用。

验证失败不会修改当前 Active Runtime；首次配置失败时统一认证保持关闭，本地管理登录继续可用。

## 配对与 Secret 边界

管理员只填写认证中心地址、一次性接入码、Gateway API/Web 公网地址、本地租户映射和 Legacy JWT 开关。Gateway 后端领取接入码、提交精确 URI、轮询资源状态、获取 Manifest 与一次性机器凭据，并先写入 SecretStore 再确认 Exchange 完成。

接入码、Exchange Token、机器 Secret 不进入 URL、数据库、日志、审计、指标或浏览器持久存储。SecretStore 写入失败时请求认证中心轮换，旧 Secret 立即失效。

配对主状态为 `metadata_pending -> preparing -> ready -> credentials_saved -> completed`。`failed`、`expired` 和 `cancelled` 是终态；可重试步骤保留当前主状态，并只记录白名单内的脱敏错误分类。这样页面既能说明当前停在哪一步，也不会把 Discovery URL、Token、Secret 或底层响应写入数据库和浏览器。

`cancelled` 只表示管理员放弃 Gateway 本地的未激活配对，不伪装成已撤销认证中心中已经领取的 Exchange 或历史凭据。Gateway 使用独立的 `cleanup_status`（`none`、`pending`、`completed`）跟踪本地补偿：

1. `POST /api/admin/system/identity/pairings/{pairingID}/cancel` 要求 Manager、`Idempotency-Key` 和 Pairing `If-Match`。
2. 数据库事务先把未激活 Revision 封存为 `failed/pairing_cancelled`，再把 Pairing 标记为 `cancelled/pending`。
3. 后台任务停止原配对 Worker，只销毁该 Revision 的 Exchange Token、机器凭据、Session Key 和以精确 owner key 创建的 SSF 连接；不得删除共享或 Active 连接。
4. 绑定了 Stream 的 SSF 连接遵循既有安全退役窗口；服务重启后自动恢复 `pending` 清理。
5. 只有清理进入 `completed` 后才允许提交下一张接入码。下一次凭据交付会轮换机器 Secret，因此不依赖回放旧 Secret。

管理员页面在 `credentials_saved` 等处理中状态展示稳定错误分类并继续自动重试，同时始终提供“放弃并清理本次配对”。`failed`、`expired` 或验证失败的未激活草稿也必须提供相同恢复入口；清理完成后重新显示标准配对表单。

`credentials_saved` 只表示 Manifest 与机器凭据已经安全保存，不代表 SSF Stream 已建立或 Revision 已可激活：

- Discovery、远端暂时不可用和 `security_event_retirement_pending` 由后台重试。
- `security_event_connection_conflict` 只能通过当前 Pairing 作用域的“安全退役现有连接”处理。
- `security_event_credential_handoff_unsafe` 表示旧连接与新 Issuer/Client 不匹配；系统不会尝试旧凭据，管理员应在原配置下断开，或放弃本次 Pairing 并重新生成接入码。
- 放弃后必须等 `cleanup_status=completed`，再提交新接入码。

当 `credentials_saved` 因已有 SSF owner 冲突而等待时，页面只能调用 Pairing 作用域的退役操作。服务端同时校验 Pairing ID、版本、错误分类和 Revision owner：只退役不属于当前 Revision 的旧连接；一旦当前 Revision 已拥有连接，陈旧操作自动成为 no-op。退役期间持久化 `security_event_retirement_pending`，页面隐藏重复退役按钮并显示自动继续。服务重启时除恢复处理中 Pairing 和 Cleanup 外，还会为“已完成、待验证/激活”的 Revision 从 SecretStore 重建 prepared Receiver。

旧 SSF 连接需要换用新机器凭据时，Gateway 先用新凭据向新 OIDC Issuer 取得管理 Token，再对旧 Transmitter 执行经过认证的 Stream 读取；存在 Stream 时必须精确核验 Stream ID、Issuer、Receiver 和 Audience。只有该端到端证明成功后才替换本地凭据引用并进入旧连接退役；证明失败不会发送旧 Secret、不会覆盖引用，也不会自动删除连接。

后台协调器每 5 秒恢复持久状态：优先继续 `cleanup_status=pending`，再恢复唯一的处理中 Pairing，然后恢复 `completed` 但尚未激活的 prepared Receiver，并校准提交结果不确定的 Active Runtime。SSF `retiring` 的最终清理由服务生命周期 Worker 托管，不随旧 Runtime 的 30 秒关闭而取消；到达安全窗口后，数据库事务先把全部 Secret 引用移交给持久清理队列，再删除精确的连接 generation。

SSF 凭据写入采用持久化 Secret 清理队列：新引用先以延迟清理记录暂存，数据库事务在引用生效时撤销暂存记录，并把旧引用原子加入到期清理队列。SecretStore 删除完成后才删除队列记录；进程在写入、切换或删除任一步退出，重启任务都能继续收敛。队列只保存非敏感引用和时间，不保存 Secret 值。

## 威胁模型摘要

- 伪造/越权：所有管理写操作要求 Manager、`Idempotency-Key`、`If-Match` 和写前审计；审计存储不可用时拒绝变更。管理 API 不接受 API Key，OIDC/JWT 不接受 `?key=` URL 传递。
- 篡改：Manifest 使用严格 schema_version 和字段白名单；Issuer 与 Discovery issuer 必须精确相等。
- 信息泄露：公开运行时接口只返回启用状态、登录/退出入口和脱敏健康状态。
- SSRF：认证中心地址必须通过协议与地址策略校验，禁止 userinfo、重定向和非开发 HTTP；Discovery/Token/Introspection 请求使用超时与响应大小上限。
- 锁死：激活和禁用前确认本地 Break-glass Manager 可用；Break-glass Token 使用独立 `local_break_glass_manager` purpose。即使 Active Runtime 构造失败，持久化 Active 的精确 Web Origin 仍可用于恢复管理，Cookie 身份校验继续 fail closed。
- 降级：OIDC 校验 fail closed；Introspection/SSF 不可用时遵循已有降级窗口，不绕过身份校验。

## 前端运行时配置

Web 前端启动后读取公开只读统一认证状态，不使用 `VITE_OIDC_*` 构建变量。安全事件作为统一认证能力状态显示，原独立页面只保留运行运维动作。

Active Revision 的 `WebBaseURL` 会以精确 scheme + host 动态加入 CORS，禁止 `*`；Cookie CSRF 只信任当前健康 Active 的 Web Origin。第一次接入或确认禁用后没有 Active 时，跨 Origin 管理 UI 仍需部署级精确 `CORS_ALLOWED_ORIGIN`，或由反向代理以同源 `/gateway-api` 提供 API。该 bootstrap Origin 是管理平面的网络边界，不是 OIDC 业务配置。

## 部署级配置

允许继续由部署环境提供的参数仅限 SecretStore、管理面 bootstrap Origin 与基础设施健康窗口：

```dotenv
IDENTITY_SECRET_STORE=file
IDENTITY_SECRET_DIR=.local-secrets/identity
IDENTITY_SECURITY_EVENTS_HEARTBEAT_INTERVAL_SECONDS=60
IDENTITY_SECURITY_EVENTS_STALE_AFTER_SECONDS=180
IDENTITY_SECURITY_EVENTS_CLOCK_SKEW_SECONDS=60
CORS_ALLOWED_ORIGIN=https://gateway-admin.example.com
```

Kubernetes 部署改用 `IDENTITY_KUBERNETES_NAMESPACE`、`IDENTITY_KUBERNETES_SECRET_NAME`、`IDENTITY_KUBERNETES_API_SERVER`、`IDENTITY_KUBERNETES_TOKEN_FILE` 和 `IDENTITY_KUBERNETES_CA_FILE`。这些参数不包含 Issuer、Audience、Scope、Client ID、Machine Secret 或业务开关。
