Files
iam-login/docs/hydra-login.md
T

132 lines
8.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)。