# HETA 企业成员身份中心 OIDC Agent 接入指南

- Contract Version: 3.0
- Updated: 2026-09-07
- Canonical URL: 本文档响应中“当前部署值”列出的指南 URL
- Repository Source: `usercenter/apps/workforce-identity/docs/integration/oidc-agent-guide.md`
- Audience: 在其他应用仓库中实施登录接入的编程 Agent

本契约只适用于企业成员登录内部应用，以及经应用单独批准后读取其组织与岗位身份。Agent 或其他非成员调用，以及企业外部客户登录时，不得继续套用本文；先读取 User Center 全局身份路由契约。Customer Identity 当前仍延期，没有可复用的客户登录契约。

3.0 是破坏性契约更新：删除成员级应用准入及 `application_access` Claim；Workforce Identity 只确认企业成员和组织岗位身份，目标应用自行受理权限申请并作出业务授权。3.0 新增按 OIDC Client 显式批准的 `organization_profile` Scope；其 Claims 只允许从 UserInfo 返回，禁止进入 ID Token。

2026-09-07 增加独立的[Agent 员工状态授权扩展](agent-parent-guide.md)，仅供部署配置显式批准的 Agent Identity Client 使用，由真实员工的 OIDC 登录引导。该扩展默认关闭，不改变本 Contract 3.0 的 OIDC Discovery、Grant、Scope 或 Token 类型；普通业务应用继续使用本文定义的登录契约。扩展是否启用以该部署的 `/.well-known/agent-parent-configuration` 为准，未启用时返回 `404`。

## 0. 当前部署值（由服务生成）

本文档所在部署的实际配置如下；协议端点仍以 Discovery 响应为准：

```dotenv
WORKFORCE_IDENTITY_OIDC_ISSUER=https://workforce-id.staging.hetapet.com
WORKFORCE_IDENTITY_DEPLOYMENT_ENVIRONMENT=simulation
WORKFORCE_IDENTITY_ENVIRONMENT=simulation
WORKFORCE_IDENTITY_WECOM_MODE=live
```

- 本指南 URL：`https://workforce-id.staging.hetapet.com/docs/oidc-agent-guide.md`
- Discovery：`https://workforce-id.staging.hetapet.com/.well-known/openid-configuration`
- 管理员登记应用后，还必须向接入方提供该应用的 Client ID、已登记 Redirect URI 和实际获批 Scopes。
- `organization_profile` 不是部署级开关；只有该 Client 获得显式批准时才能请求，并且组织任职只从 UserInfo 返回。


## 1. 文档地位与使用方式

本文档是下游应用接入 HETA 企业成员身份中心的编程契约。收到本 URL 的 Agent 必须完整读取本文档，再阅读目标应用仓库中的协作规则、现有认证、会话和授权实现，并按照本文档适配目标技术栈。

以下信息拥有不同事实来源：

- 本文档定义业务边界、安全约束、Claims 语义、禁止事项和验收标准。
- 当前部署返回的本指南定义该环境的 Issuer、部署环境、运行配置和身份核验模式。
- OIDC Discovery 定义当前环境的协议端点、签名算法和能力元数据。Agent 必须在运行时读取 Discovery，不得从本文档硬编码端点。
- 目标应用自己的权限模型决定谁可以申请、谁可以审批，以及成员能执行哪些业务操作。

如果本文档、目标应用要求和 Discovery 元数据互相冲突，停止实施并报告冲突，不得自行放宽安全约束。

## 2. 每个应用必须取得的输入

实施前由 Workforce Identity 管理员提供：

| 配置 | 示例 | 说明 |
| --- | --- | --- |
| `WORKFORCE_IDENTITY_OIDC_ISSUER` | 见“当前部署值” | Workforce Identity 的 HTTPS Origin，不得带路径 |
| `WORKFORCE_IDENTITY_OIDC_CLIENT_ID` | `application-001` | 管理员登记应用后生成 |
| `WORKFORCE_IDENTITY_OIDC_REDIRECT_URI` | `https://app.example.com/auth/oidc/callback` | 必须与登记值精确一致 |
| `WORKFORCE_IDENTITY_OIDC_SCOPES` | `openid profile` 或 `openid profile organization_profile` | 必须以该 Client 的登记结果为准 |

Discovery 地址固定由 Issuer 推导：

```text
${WORKFORCE_IDENTITY_OIDC_ISSUER}/.well-known/openid-configuration
```

本接入没有 Client Secret。不得索取、生成或配置 `WORKFORCE_IDENTITY_OIDC_CLIENT_SECRET`。

只有应用确需组织与岗位身份，并且 Workforce Identity 管理员已经对该 Client 显式批准时，才能请求 `organization_profile`。批准 Scope 只允许读取身份资料，不授予任何成员业务权限。

如果缺少 Issuer、Client ID、已登记 Redirect URI 或获批 Scope 清单，Agent 可以完成不依赖真实环境的代码与测试，但不得声称真实联调已经通过。

## 3. 系统边界

```text
下游应用（OIDC Client）
        │  Authorization Code + PKCE
        │  openid profile [organization_profile]
        ▼
Workforce Identity
        ├── 企业成员身份与 active 状态
        ├── 自建组织目录、岗位与任职
        └── OIDC Client 登记及每应用 Scope 批准
        │
        ▼
企业微信 live 或非产线 mock adapter

OIDC 登录成功
        │
        ▼
目标应用自己的账号资格、权限申请、审批与业务授权
```

- 下游应用只接入 Workforce Identity，不直接接入企业微信。
- `mock` 与 `live` 使用相同的下游 OIDC 契约；下游应用不得根据当前模式实现两套登录逻辑。
- Workforce Identity 确认成员身份和当前组织任职，不判断成员是否可以进入某项业务或执行某个操作。
- OIDC Client 登记只建立协议信任；它不表示任一成员已经获得该应用的访问权。
- 应用必须自行决定本地账号资格，并拥有权限申请、审批、角色、菜单、数据范围和操作授权。
- 组织岗位可以作为应用授权的输入，但不能被直接等同为应用业务角色。
- 不得增加企业微信登录旁路、共享账号密码或生产后门。

## 4. OIDC 协议契约

| 项目 | 要求 |
| --- | --- |
| Flow | Authorization Code |
| Response Type | `code` |
| Grant Type | `authorization_code` |
| Client 类型 | 无共享 Secret 的 Public Client |
| Token Endpoint Auth Method | `none` |
| PKCE | 必须使用 `S256` |
| 基础 Scopes | `openid profile` |
| 可选 Scope | `organization_profile`，仅限该 Client 已获显式批准 |
| Redirect URI | 管理员登记的精确 HTTPS URL |
| Refresh Token | 不在当前契约内 |
| Dynamic Client Registration | 不支持 |

Agent 应优先使用目标框架中维护良好的 OIDC Client 库，不得手写 JWT 验签、PKCE 或 OIDC 状态机。

### 4.1 发起授权

从 Discovery 取得 `authorization_endpoint`，通过 OIDC 库发起包含以下参数的请求：

```text
response_type=code
client_id=<已登记的 Client ID>
redirect_uri=<已登记的精确回调地址>
scope=openid profile [organization_profile]
state=<高熵一次性随机值>
nonce=<高熵一次性随机值>
code_challenge=<由 code_verifier 计算>
code_challenge_method=S256
```

方括号表示可选项，不是请求中的字面字符。未获批准的 Client 必须省略 `organization_profile`；请求未登记 Scope 应按协议错误失败，应用不得在失败后自动扩大或降级权限语义。

`state`、`nonce` 和 `code_verifier` 必须与发起登录的浏览器会话绑定，短时有效且只能使用一次。

### 4.2 处理回调与换取 Token

回调处理必须先校验 `state`。成功取得授权码后，从 Discovery 取得 `token_endpoint`，由 OIDC 库提交：

```text
grant_type=authorization_code
client_id=<已登记的 Client ID>
code=<回调取得的授权码>
redirect_uri=<发起授权时相同的地址>
code_verifier=<本次事务保存的原始值>
```

不得发送 Client Secret。授权码、`state`、`nonce` 和 `code_verifier` 在成功或失败后都必须作废。

### 4.3 验证 Token

使用 Discovery 中的 `jwks_uri` 和算法元数据验证 ID Token，至少校验：

- 签名有效；
- `iss` 与配置的 Issuer 完全一致；
- `aud` 包含当前 Client ID；
- `exp` 尚未到期；
- `nonce` 与本次登录事务一致。

完成上述验证后，应用必须同时校验 `member_status` 精确等于 `active`；该 Claim 缺失或不为 `active` 时必须拒绝登录，不得创建、恢复或提升本地会话。

必须使用 Token 自带的 `exp` 或 Token 响应中的 `expires_in`，不得硬编码有效期。当前契约不提供 Refresh Token。

需要组织资料时，从 Discovery 读取 `userinfo_endpoint`，使用 `Authorization: Bearer <access_token>` 调用；不得硬编码 `/me`。应用必须验证 UserInfo 的 `sub` 与已验证 ID Token 的 `sub` 完全相同。

## 5. Claims 与本地成员映射

| Scope | Claim | 返回位置 | 语义 | 接入要求 |
| --- | --- | --- | --- | --- |
| `openid` | `sub` | ID Token、UserInfo | 稳定的 Workforce 成员 ID | 与 Issuer 组成唯一外部身份键 |
| `openid` | `member_status` | ID Token、UserInfo | 当前企业成员状态 | 必须精确为 `active`，否则失败关闭 |
| `profile` | `name` | ID Token、UserInfo | 成员显示名称 | 仅展示，可更新，不得作为唯一键 |
| `organization_profile` | `organization_assignments` | **仅 UserInfo** | 当前有效组织任职集合 | 只作为身份资料或授权输入 |

`organization_assignments` 的 JSON 结构固定为：

```json
{
  "organization_assignments": [
    {
      "organization_unit": {
        "id": "stable-unit-id",
        "name": "研发部",
        "path": [
          { "id": "stable-company-id", "name": "HETA" },
          { "id": "stable-unit-id", "name": "研发部" }
        ]
      },
      "position": {
        "id": "stable-position-id",
        "name": "高级经理"
      },
      "primary": true
    }
  ]
}
```

契约约束：

- `organization_assignments` 可以为空，也可以包含多个任职；应用必须读取 `primary` 标志，不得依赖数组顺序推断主任职。
- 组织单元、岗位和路径节点的 `id` 是稳定引用键；`name` 是可变展示值。
- 组织资料只代表 Workforce 当前确认的组织身份，不是权限集合、应用角色或审批结果。
- 即使请求了 `organization_profile`，ID Token 也不得出现 `organization_assignments`；客户端必须通过 UserInfo 获取。
- 未请求 Scope 或 Client 未获批准时，客户端不得期待该 Claim，也不得从其他字段推断组织岗位。

下游应用应以 `(issuer, sub)` 作为完整的外部身份键。不得假设 Workforce Identity 会返回邮箱、手机号、企业微信 UserID、企业微信部门 ID 或完整通讯录。

如果目标应用已有用户数据，且不存在经过确认的稳定映射，不得按姓名、组织名称或岗位名称自动合并账号。Agent 必须报告迁移决策缺失，由用户确定映射规则。

应用必须对 ID Token 和 UserInfo 中的 `member_status` 分别失败关闭校验；缺失或不为 `active` 时必须拒绝登录或资料读取。OIDC 登录成功只表示成员身份有效；它不表示成员已经申请、获批或拥有任何应用业务权限。

## 6. 应用授权与首次联调

### 6.1 应用自己的授权责任

完成 OIDC 回调后，目标应用必须在自身边界内执行：

1. 以 `(issuer, sub)` 创建或查找稳定本地身份映射。
2. 判断该成员是否有资格建立本地账号或进入申请流程。
3. 由应用自己的流程受理权限申请并交给有权审批者决定。
4. 按应用自己的角色、资源、数据范围和操作规则执行授权；默认策略由该应用定义并验证。
5. 在成员组织任职变化时，按应用明确的重评规则处理，不得假设 Workforce 会自动撤销应用会话或业务权限。

应用可以在申请页展示组织与岗位资料，帮助审批人理解申请者身份；但“研发部高级经理”本身不等于任何业务角色，除非目标应用已经明确、测试并拥有对应规则。

### 6.2 `simulation + mock` 联调

公司部署环境为内部环境或仿真环境，且服务运行配置与身份核验模式为 `simulation + mock` 时：

1. 管理员登记应用，选择是否批准 `organization_profile`，并提供 Client ID、精确 Redirect URI 和实际 Scopes。
2. 下游应用发起标准 OIDC 登录；身份中心自动完成共享合成主体 `simulation-tester` 的核验。
3. 下游应用完成 Code + PKCE、验证 ID Token，并以 `(issuer, sub)` 建立本地会话。
4. 下游应用按自己的测试数据验证未授权、申请中、已授权等业务状态；Workforce 不为该合成主体维护成员级应用访问权。

`simulation-tester` 只代表合成登录行为，不是真实企业成员；不得把它与产线成员、邮箱、手机号或企业微信 UserID 建立映射。多个测试者共享同一个 OIDC `sub`，因此该模式不适合验证多人身份隔离。

### 6.3 `live` 联调

1. 管理员登记应用并提供 Client ID、Redirect URI 和实际 Scopes。
2. 测试成员发起 OIDC 登录；Workforce 通过企业微信核验并记录或更新稳定成员身份。
3. 对所有 active 企业成员，Workforce 可以完成已登记 Client 的标准 OIDC 登录；不再存在成员级应用准入授予步骤。
4. 下游应用根据自己的账号与权限状态允许进入、引导申请或拒绝业务访问。
5. 如已批准 `organization_profile`，再调用 UserInfo 验证组织任职结构以及 ID Token 中不存在该 Claim。

Workforce 成员失效会阻止后续身份核验和 UserInfo，但不会主动销毁下游应用已经建立的本地会话，也不撤销应用自己的业务授权。当前契约不提供 Back-Channel Logout 或成员状态变更事件；应用必须按自身风险定义会话时长和重新认证策略。

在 `live` 模式下，Workforce 会在签发新 Token 和处理 UserInfo 时通过当前身份来源重新核验已绑定成员状态。身份来源暂时不可用时请求失败关闭，不会回退使用本地缓存的 `active` 状态。

### 6.4 Redirect URI 变更与应用下线

- Redirect URI 变更是整组替换，不是追加兼容项。管理员与应用负责人必须先确认新回调已经部署，再整体替换 Redirect URI；替换后旧地址立即不能发起新的授权，也不能完成依赖旧登记值的后续协议请求，Client ID 保持不变。
- 应用永久下线时，先停止业务入口并清理应用自己的会话，再由 Workforce Identity 管理员完成显式确认并撤销应用登记。撤销应用登记后，该 Client ID 不再接受新的 OIDC 请求，既有 Access Token 也不能继续读取 UserInfo。
- 撤销不可恢复，也不主动销毁下游应用已经建立的本地会话或业务权限。需要重新接入时必须登记新应用并取得新的 Client ID；下游应用负责按自身风险终止旧会话和撤销本地权限。

## 7. 组织目录来源边界

- 自建 `OrganizationDirectory` 是组织单元、岗位和任职的权威来源；它与 Workforce 身份模块同部署并使用同一 SQLite，但拥有独立管理员配置和审计。
- 企业微信通讯录只形成 `partial` 待审核候选快照，不是完整镜像。企业微信可见范围变化或接口遗漏都不能被解释为权威删除。
- 企业微信部门必须显式映射到稳定组织单元；成员只通过现有的稳定外部身份绑定匹配，不按姓名合并。
- 企业微信岗位文本仅作审核提示，不自动创建或选择权威岗位。
- 导入快照中缺失的组织单元、岗位或任职不会自动停用。结束任职和停用结构必须由组织目录管理员显式操作。
- 下游应用只消费 OIDC UserInfo，不得依赖 OrganizationDirectory 的数据库表、管理端或企业微信导入结构。

## 8. 浏览器行为与身份核验模式

- `mock`：只允许服务运行配置 `local` 或 `simulation`，且产线环境始终禁止；所有浏览器都会自动完成固定合成主体核验，不访问企业微信。
- `live`：企业微信客户端内由身份中心完成静默登录；普通桌面浏览器使用企业微信 Web 登录；普通手机 Safari/Chrome 会被引导到企业微信。
- 下游应用不得复制这些环境判断或直接消费企业微信 code。
- 当前模式以本文档的“当前部署值”为准；产线环境不得使用 `mock`。

## 9. 下游应用安全要求

- 优先采用服务端或 BFF 回调，并把应用会话保存在服务端。
- 如果目标项目是纯 SPA，且没有经过确认的浏览器 Token 交换与安全存储方案，停止并报告架构约束；不得默认把 Token 存入 `localStorage`。
- 登录成功后轮换本地会话 ID。
- 会话 Cookie 使用 `HttpOnly`、`Secure`，并根据实际跳转流程选择严格且可工作的 `SameSite`。
- 授权码、Token、`state`、`nonce`、`code_verifier` 和完整回调查询串不得进入日志、错误页、埋点或监控标签。
- 登录后返回地址只能接受经过校验的站内相对路径，禁止开放重定向。
- 不得只解码而不验证 ID Token。
- 必须校验 ID Token 与 UserInfo 的 `member_status` 精确等于 `active`；缺失或其他值一律失败关闭。
- 不得将 `name`、`member_status`、组织单元、岗位或 `primary` 当作未经应用规则确认的业务角色。
- 只请求完成当前功能所需的 Scope；不需要组织资料的应用保持 `openid profile`。
- 本地退出必须清除应用自己的会话；除非未来契约明确增加，否则不得依赖 Workforce Identity 统一退出。

## 10. 错误处理契约

| 场景 | 应用行为 |
| --- | --- |
| `access_denied` | 提示“企业成员身份不可用，请联系管理员”，不要解释为应用业务权限结论，也不要无限重试 |
| 未获批准却请求 `organization_profile` 或其他 Scope 错误 | 报告 Client 登记/配置错误，不自动重试或猜测 Scope |
| `state` 或 `nonce` 不匹配 | 终止事务、清除临时状态并记录不含敏感值的安全事件 |
| 授权码过期、重复使用或 `invalid_grant` | 终止事务并允许成员重新发起完整登录 |
| Discovery、JWKS、Token Endpoint 或 UserInfo 暂时不可用 | 显示可重试的登录故障，不创建或提升本地会话 |
| Issuer、Audience、签名或 UserInfo `sub` 校验失败 | 拒绝登录并报告安全/配置错误 |
| ID Token 或 UserInfo 的 `member_status` 缺失或不为 `active` | 拒绝登录或资料读取，不创建、恢复或提升本地会话 |
| 回调配置不匹配 | 报告配置错误，不尝试修改或放宽回调校验 |
| Client 已撤销 | 停止发起登录并清理该应用自己的会话；需要恢复接入时申请新的应用登记 |

错误日志只能记录稳定的内部错误分类和请求关联 ID，不能记录凭证或完整协议参数。

## 11. Agent 实施范围

Agent 在目标应用中应完成：

1. 调查并复用现有认证、路由、会话和授权 seam。
2. 增加 Issuer、Client ID、Redirect URI 和明确 Scope 的环境配置与示例文件。
3. 实现登录入口、回调处理、受保护页面或接口和本地退出。
4. 以 `(issuer, sub)` 建立或查找本地成员映射。
5. 对 ID Token 和 UserInfo 中的 `member_status` 执行失败关闭校验。
6. 将显示名称和组织资料视为可变资料；保留稳定 ID，不自动改写应用角色。
7. 如使用 `organization_profile`，通过 UserInfo 读取并验证 Claim 结构与 `sub`，同时确认 ID Token 不包含该 Claim。
8. 接入目标应用现有的账号资格、申请与授权规则；如果这些规则尚未定义，报告缺失决策，不得把 OIDC 成功当作授权替代品。
9. 实现本契约定义的错误页面或错误响应，并补充自动化测试和接入说明。

除非用户另行明确授权，Agent 不得：

- 修改 Workforce Identity 代码或协议；
- 在 Workforce Identity 登记应用、批准 Scope 或修改组织目录；
- 接入企业微信 API 或读取 Workforce SQLite；
- 创建 Client Secret；
- 为目标应用臆造权限审批规则；
- 部署、发布、提交代码或改变外部环境；
- 顺带重构与登录接入无关的模块。

## 12. 最低验证矩阵

Agent 必须在目标应用可用的测试 seam 上覆盖：

- 未登录访问受保护资源会进入 OIDC 登录流程；
- 授权请求使用 `code`、实际获批 Scopes、一次性 `state`/`nonce` 和 PKCE `S256`；
- Token 请求包含 `code_verifier` 且不包含 Client Secret；
- 合法回调完成 Token 验证，并按 `(issuer, sub)` 创建或恢复本地会话；
- ID Token 或 UserInfo 的 `member_status` 缺失、为 `inactive`、`unverified` 或未知值时拒绝登录或资料读取；
- OIDC 登录成功但没有应用业务权限时，应用按自己的规则拒绝或引导申请；
- `state`/`nonce` 不匹配、签名错误、Issuer 错误、Audience 错误和 Token 过期均拒绝登录；
- 未获批准的 Client 不请求 `organization_profile`；
- 获批 Client 从 UserInfo 读取零个、一个和多个任职，并识别 `primary`；
- `organization_assignments` 出现在 ID Token 时拒绝把它当作合规契约；
- 组织显示名称变化不会创建新主体或直接改变应用业务角色；
- 本地退出后应用会话失效；
- 日志与错误响应不泄露授权码、Token 或 PKCE 数据。

完成实现后运行目标仓库中相关测试、类型检查、lint 和构建命令。只有在具备可访问的 Issuer、已登记 Client ID、精确 Redirect URI、获批 Scope 和可用测试成员时，才能把真实端到端登录标记为已验证。

## 13. Agent 完成报告

Agent 最终必须报告：

- 修改的文件与关键行为；
- 使用的 OIDC Client 库及选择依据；
- 需要部署方提供的环境变量和实际 Scopes；
- 实际运行的检查和结果；
- 是否完成真实端到端登录与 UserInfo 验证；
- 尚需 Workforce Identity 管理员完成的 Client 登记或 Scope 批准；
- 尚需目标应用负责人确认的账号资格、申请或授权规则；
- 未解决风险或与本文档的偏差。

如果任务因缺少用户决策、真实配置或安全 seam 无法完成，报告已有证据和所需的最小下一步，不得用未经验证的实现代替。
