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

8.7 KiB
Raw Blame History

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 与 WebAuthn,再提供以下配置:

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 是客户端与密钥的唯一持久化来源,不读其内部表、不建第二份注册表。 使用方式和边界见 客户端管理接口。

每次 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 恢复边界见 第二因素。

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、 客户端管理能力。