接入 Hydra 授权、客户端管理与统一注销

This commit is contained in:
2026-09-28 12:30:06 +00:00
parent 45994e3919
commit 62b6e9db40
37 changed files with 1700 additions and 23 deletions
+131
View File
@@ -0,0 +1,131 @@
# Hydra Login/Consent 接入
本实现使用 Hydra 的私有 Admin API,沿用 Spring Security 的 AD + WebAuthn 双因素认证。
OAuth2/OIDC 签发、客户端注册表和客户端密钥由 Hydra 持有。应用只负责身份、授权确认与
Login/Consent 接受,不自行签发 token,也不实现另一套认证状态机。
## 浏览器路径
1. 客户端发起 Hydra authorization code 请求;Hydra 将浏览器带到 `/oauth2/start?login_challenge=...`。
2. 服务端通过 Admin API 核对 client、scope、audience 与 issuer 请求地址,绑定浏览器会话,
再进入 `/oauth2/login`。该页面由 Spring Security 要求两种因素,未满足时转到密码或 MFA 页。
3. MFA 完成后显示应用和申请范围;用户通过带 CSRF 的原生表单 POST 确认。
4. 服务端重新核对 Hydra 请求,使用显式 subject 绑定接受 login,浏览器返回 Hydra。
5. Hydra 回到 `/oauth2/consent`;服务端核对会话随机值、原始 challenge 摘要、主体、client、
原始请求 URL 和 scope 后一次性接受 consent;Hydra 将授权码交给客户端。
只为管理员启用的第一方 client 接受 openid/profile/email/groups,不授予 access-token audience、
不申请 offline_access/refresh token。Claims 按请求 scope 释放,组保持直接 AD memberOf 的名称;
AD mail 没有邮箱所有权验证依据,`email_verified=false`。
单个浏览器会话只保存一个未完成请求,10 分钟失效,新请求替换旧请求。认证因素与会话
依然由 Spring Security 管理;`HydraBrowserRequests` 只保存待接受的 issuer 请求及回程关联,
不记录密码、私钥或 token。退出/重启后未完成请求需从应用重新发起。
Hydra v26 的 consent `login_challenge` 是内部标识,不能与浏览器收到的 opaque challenge
逐字比较。本服务在受信任的 login accept `context` 中写入原始 challenge 的 SHA-256 摘要
与会话随机值,在 consent 读回并核对;不解析 Hydra 内部格式。
确认页的 CSP 仅增加当前 Hydra 与已校验回调的 origin,允许原生表单返回 issuer 后跳转;
身份页允许向固定 Hydra origin 完成注销跳转。Hydra 返回的 redirect 必须是已配置公共 origin 下的
`/oauth2/auth`,Admin HTTP client 不跟随重定向,不向浏览器传播上游错误正文。
本轮仅覆盖交互式授权码流,不支持静默 `prompt=none`。
接受 login 时保留 Hydra 登录会话,供统一注销关联应用;会话寿命沿用 Hydra 配置,
不延长已有会话。Hydra 的 `skip` 仅表示 issuer 记得登录,不能绕过 Spring 的有效双因素、
原生表单确认与主体匹配。`prompt=login/consent/select_account` 或 `max_age` 存在时重新验证身份,
不把之前的 MFA 时间改写为新登录时间。不请求上述参数时可复用仍有效的本地双因素会话。
## 配置与主体连续性
先启用 [AD](ad-login.md) 与 [WebAuthn](webauthn.md),再提供以下配置:
```yaml
iam:
hydra:
enabled: true
admin-url: http://hydra-admin.hydra.svc.cluster.local:4445
public-url: https://hydra.ad.ddupan.top
clients: [gitea] # 仅供尚未写入管理标记的旧客户端过渡
subjects:
- authority: ad.example.test
directory-id: 00112233-4455-6677-8899-aabbccddeeff
subject: human:EXISTING_REVIEWED_SUBJECT
```
以上主体是格式示例,不能用于真实账号。配置中的绑定按 AD authority + objectGUID 匹配,
必须唯一;未绑定用户拒绝授权。不使用用户名、邮箱、自动创建 Gitea 账号或 AD GUID 的新哈希
来替代现有主体。现役 Go 适配器使用 `human:` + SHA-256(Authelia issuer + NUL + sub),
上线前需要取得并核对该旧主体,建立到 AD 稳定键的显式映射,确认 Gitea 的外部账号关联。
普通改名不改变绑定;删除或修改绑定属于迁移操作,需要单独审查。
Hydra 环境配置需将 login URL 指向 `/oauth2/start`,consent URL 指向 `/oauth2/consent`,
logout URL 指向 `/oauth2/logout`,默认 post-logout URL 指向本服务 `/signin`。
本仓库的实现与测试不等于这些生产设置已变更。Admin URL 可以使用现役受 NetworkPolicy
约束的集群内 HTTP,也可通过 loopback port-forward;不能公开管理端口。
公共 origin 必须 HTTPS,HTTP 只接受隔离测试的 loopback。客户端请求必须显式带 redirect_uri。
## 客户端注册与管理边界
`clients` 领域模块提供受 Spring Security 保护的 CRUD,调用私有 Hydra Admin API。
Hydra PostgreSQL 是客户端与密钥的唯一持久化来源,不读其内部表、不建第二份注册表。
使用方式和边界见 [客户端管理接口](client-management.md)。
每次 login/consent 都重新读取 client 的 `metadata.iam_login_enabled`;显式 false 拒绝新授权。
仅当标记不存在时使用旧 `iam.hydra.clients` allowlist。通过 API 新增启用的客户端不需要修改
服务配置。禁用不能撤回已签发 token 或已建立的应用会话。公共动态注册入口不在本轮范围,
不能让未信任调用者写入该管理标记。
## 统一注销
RP 使用 Hydra discovery 的 `end_session_endpoint`,携带 ID token hint 与已登记回调。
Hydra 查询其登录会话后回到 `/oauth2/logout`;用户提交有 CSRF 与浏览器请求绑定的确认表单,
服务端重新核对 challenge、主体和登记回调,接受 Hydra logout,并通过 Spring 的
`SecurityContextLogoutHandler` 清除当前本地会话。Hydra 负责通知登记的 front/back-channel
端点并返回应用;不自行遍历客户端发 HTTP 请求。身份页也提供原生表单发起统一退出。
Hydra 会话不存在或已过期时可能直接返回应用,不经过确认页;这不证明 Spring 会话也被清除。
本地退出按钮仍能清除当前 Spring 会话。下游必须实现登记的注销协议,通知失败也不能宣称
应用已退出。统一注销不等于撤销所有已签发 token,不覆盖其他浏览器设备。
## 正式域名规划
正式入口目标为 `https://auth.ddupan.top`,替换现有 Authelia 入口,Hydra 与本服务按路径同源:
- Hydra:discovery/JWKS、`/oauth2/auth`、`/oauth2/token`、`/oauth2/revoke`、
`/oauth2/sessions/logout`、`/userinfo` 等经核对的 public 端点。
- iam-login:`/signin`、`/signin/**`、`/webauthn/**`、`/login/webauthn`、
`/oauth2/start`、`/oauth2/login`、`/oauth2/consent`、`/oauth2/logout`、`/api/iam/**` 和静态资源。
- 不把整个 `/oauth2/**` 路径交给 Hydra;不发布 Hydra `/admin/**` 或管理端口。
这是部署计划,不是现网配置。上线前需核对 cookie 名称、受信任代理与 TLS 转发、issuer
变更和应用配置、旧 sub 关联,以及新 WebAuthn RP ID 的凭据注册。不能仅改 DNS 后假定现有
passkey 和 OIDC 会话仍可复用;回退路径也需在基础设施 PR 中明确。
## 隔离验证与生产切换
首轮验收以现有 AD + MFA → Hydra → Gitea 原账号及仓库权限为目标,不以完整 self-service、
目录管理或自动恢复为前置条件。AD 管理与人工 MFA 恢复边界见 [第二因素](webauthn.md)。
```sh
scripts/gradle-in-docker test bootJar
scripts/gradle-in-docker hydraBrowserFixture
# 另一终端,仅连接固定的 loopback 测试夹具:
IAM_HYDRA_FIXTURE=1 npm --prefix frontend run test:browser -- hydra.spec.ts
```
夹具包含模拟 AD、临时 PostgreSQL、固定 digest 的 Hydra v26.2.0,以及测试专用 OAuth client。
Hydra 只监听 127.0.0.1:14444/14445,应用使用测试证书监听 HTTPS localhost:18083;
客户端回调由测试专用 loopback HTTP 服务接收,不在生产 JAR 中加入测试回调或 token 查看入口。
虚拟认证器产生真实 WebAuthn 签名,随后交换授权码,检查 ID token 的 JWKS 签名、issuer、
audience、nonce、有效期、预配置旧 sub、组与邮箱语义,并拒绝授权码重放。
同时验证 remembered login 仍显示授权确认、RP 发起注销、back-channel token 签名与 sid
关联,以及 Spring 会话失效。JVM 集成测试另验证真实 PostgreSQL 上客户端 CRUD、Hydra
重启后记录仍在、更新保留旧密钥与管理接口的 MFA/组/CSRF 拒绝。
这些验证不等于生产 Gitea 账号关联、真实新链路人类操作或 Native 验收。
下一步生产切换仍需完成真实 subject 映射审查、AD/WebAuthn/Hydra Native 验收、部署网络规则,
以及从 Gitea 返回原账号与原仓库权限的实际验收。现役 Go/Authelia 登录入口继续作为已验收路径。
上游接口依据:[Hydra Login/Consent](https://www.ory.com/docs/oauth2-oidc/custom-login-consent/flow)、
[客户端管理能力](https://www.ory.com/hydra)。