# IWish Auth 完整 AI 接入上下文 生成来源:apps/docs/content/registry.json(23 篇规范文档) 本文档由 scripts/generate-phase-7-docs.mjs 生成,禁止手工编辑。 # 快速开始 > 完成统一登录、上下文与 App 本地授权接入 # 快速开始 本指南用于把一个有服务端的业务 App 接入 IWish Auth。接入完成后,内部员工使用飞书统一登录,外部账号使用邀请和邮箱密码登录;业务 App 获得稳定的 `iwish_auth_user_id`、员工目录和客户项目上下文。业务 App 的成员、角色、页面动作和数据范围仍由业务 App 自己管理。 > 浏览器不能持有 Client Secret,也不能直接交换授权码。登录、callback、App Session Cookie 和服务端授权都必须在业务 App 服务端完成。 ## 1. 选择接入档案 - 新应用且没有需要保留的生产账号、身份外键和旧 Session:选择 `greenfield`,阅读[新应用接入](/docs/greenfield-integration)。 - 已有生产用户、角色、业务外键和历史数据:选择 `legacy-migration`,阅读[存量账号迁移](/docs/account-migration)。 `greenfield` 不生成迁移计划或 mapping ledger。`legacy-migration` 不允许首次登录自动创建空账号,也不允许运行期按邮箱关联。 ## 2. 管理员准备 管理员在 Auth Admin 完成应用注册、组织开通、OAuth Client 和精确回调地址配置。Client Secret 只显示一次,应保存到 staging 或 production 的 Secret 管理中,不得写入仓库、前端变量或聊天记录。 ## 3. 创建 Manifest 2.0 ```json { "$schema": "https://auth-docs-staging.iwishapp.cn/auth-manifest.schema.json", "schemaVersion": "2.0", "appKey": "reporting", "name": "数据报表", "description": "跨境电商数据报表系统", "appUrl": "https://reporting-staging.example.com", "environment": "staging", "manifestVersion": "2026.07.22-1", "authorizationMode": "application_owned", "requiredContext": ["identity", "employee", "departments", "clientAssignments"] } ``` Manifest 只声明应用元数据和需要的 Auth 上下文,不声明业务 `roles` 或 `permissions`。通过 CLI 先执行 `validate`,确认差异后再执行 `sync`。 ## 4. 接入登录与 Session 使用官方 SDK 实现 `/auth/login`、`/auth/callback` 和 `/auth/logout`。SDK 使用 PKCE,callback 在服务端交换一次性 code,并把 opaque App Session 写入 `HttpOnly`、`SameSite=Lax` Cookie。每个受保护请求调用 `requireAppAccess`;它会验证 session 有效、组织和 App 已启用、员工没有离职,以及 session 确实属于当前 `appKey`。 ```ts export async function GET(request: Request) { const session = await auth.requireAppAccess(request); const member = await localMembers.findByAuthUserId(session.user.id); if (!member || member.status !== "active") { return Response.json({ error: "local_member_required" }, { status: 403 }); } await localAuthorization.require(member, "report.read"); return Response.json({ user: session.user, projects: session.clientAssignments }); } ``` ## 5. 建立本地授权 业务 App 使用 `session.user.id` 作为 `iwish_auth_user_id`,建立本地成员记录并分配本地角色。greenfield App 由管理员从 Auth 受控目录选择身份,不能手工复制 User ID,也不能在第一次登录时自动授予 admin/viewer。legacy-migration App 则必须在切换前把 Auth User ID 预绑定到原本地成员。无本地成员、成员停用、无角色或权限不足时一律返回 403。前端隐藏按钮只是体验优化,不能代替服务端检查。`clientAssignments` 可以用于预选客户项目或缩小查询范围,但它不是权限凭证,不能自动授予全量数据。 ## 6. 验收 至少验证:无本地成员 403、角色不足 403、跨 App session 401/403、回调地址不匹配失败、Client Secret 不出现在浏览器、飞书离职后旧 session 被拒绝、外部邮箱账号可登录但仍需要 App 本地成员和角色。接入不得直接调用飞书、Supabase Auth 或 Auth 数据库。 --- # 接入流程概览 > 管理员、开发者和业务 App 的完整协作流程 # 接入流程 ## 1. 职责边界 | 参与方 | 负责内容 | | --- | --- | | IWish Auth 平台管理员 | App 注册、组织开通、OAuth Client、callback、Manifest、飞书目录、客户项目和平台内部权限 | | 业务 App 开发者 | SSO 接入、本地成员、本地角色、本地权限、业务数据范围和本地审计 | | 业务负责人 | 确认本地角色颗粒度、敏感动作、数据隔离和迁移窗口 | Auth 是统一身份和公共上下文来源,不是所有业务 App 的业务授权中心。`appKey=auth` 的角色权限仅用于 Auth 管理后台。 ## 2. 先选择接入档案 | 档案 | 适用条件 | 身份关联方式 | 迁移工件 | | --- | --- | --- | --- | | `greenfield` | 没有需要保留的生产账号、身份外键和旧 Session | 管理员从受控 Auth 目录选择用户并创建 App 本地成员 | 不需要 | | `legacy-migration` | 已有生产用户、角色、业务外键和历史数据 | 切换前把 Auth User ID 预绑定到原本地成员 | Migration Plan 1.0 + 私有 ledger | 不得把新应用伪装成存量迁移,也不得把真实存量应用当作新应用自动创建空账号。 ## 3. 两条标准顺序 `greenfield`: 1. 创建新 App 数据库和独立 staging。 2. 创建 Manifest 2.0、App 注册、组织开通、OAuth Client 和精确 callback。 3. App 服务端接入 PKCE、callback、HttpOnly Cookie、session introspection 和登出。 4. 建立本地成员、角色、permission 和 append-only 授权审计。 5. App 管理员从受控身份目录选择用户并在一个事务中分配本地角色。 6. 验证首次登录不自动创建 admin/viewer,无本地成员和无角色均 fail closed。 `legacy-migration`: 1. 盘点旧登录、用户主键、业务外键、角色、权限、字段规则和数据范围。 2. 创建独立 staging,禁止直接修改 production 数据。 3. 完成共享的 Manifest、OAuth Client 和 SSO 接入。 4. 生成迁移计划 1.0 和私有 mapping ledger,在切换前预绑定存量用户。 5. 保留本地用户主键、业务外键、角色与权限;所有服务端动作 fail closed。 6. 验证预绑定用户命中原账号、原角色和原业务数据。 7. 完成 staging、回滚和切换闸门后,一刀切关闭旧登录、本地密码和旧 Session。 ## 4. 数据流 ```text 飞书 / 外部邮箱 -> IWish Auth -> 一次性授权码 + PKCE -> 业务 App 服务端 callback -> opaque App Session 0.3 -> iwish_auth_user_id -> App 本地成员 -> App 本地角色与权限 -> 业务数据范围检查 -> 返回结果 ``` `iwish_auth_user_id` 只用于稳定定位原本地成员。项目负责人、创建人、审批人和审计记录继续引用 App 原用户主键,不替换为 Auth User ID。运行期禁止按邮箱自动合并存量账号。 Manifest 2.0 的 `authorizationMode` 固定为 `application_owned`。Auth Session 不返回业务 `roles` 或 `permissions`。业务 App 不上传本地角色树,也不调用 Auth 远程权限判断接口。 ## 5. 客户项目上下文 飞书同步提供公司部门和员工目录;Auth 管理员独立创建客户项目,并把 active 员工按销售、技术、运营、优化师等职责绑定到项目。业务 App 可从 `clientAssignments` 读取当前员工参与的项目和职责,用于默认筛选、工作台入口或流程路由。最终是否能查看、编辑、导出某条业务数据,仍由 App 本地授权和数据规则判断。 ## 6. 禁止事项 - 业务 App 直接接飞书、Supabase Auth 或 Auth 数据库。 - 信任 `x-user-id`、`x-role` 等浏览器可伪造 Header。 - 在前端保存 Client Secret 或 opaque App Session。 - 把 Auth 员工状态、本地角色或项目分工混成一个布尔值。 - 为业务 App 在 Auth 创建业务角色、权限节点或用户角色映射。 - 多个 App 共用 OAuth Client、callback 或 session cookie。 - 让管理员手工复制 Auth User ID,或首次登录自动授予 admin/viewer。 失效链路必须覆盖用户停用、员工离职、组织停用、App 关闭、OAuth Client 停用、单 App 登出和全局登出;本地成员停用和本地角色撤销则由业务 App 自己立即生效。 --- # Next.js > 使用 App Router 和 TypeScript SDK 接入 # Next.js 接入 Next.js App Router 必须在 Route Handler 或 Server Component 的服务端边界接入 IWish Auth。Client Secret、PKCE verifier 和 opaque App Session 不进入客户端组件。 ## 配置 ```ts import { createIWishAuth } from "@iwish/auth-sdk/next"; export const auth = createIWishAuth({ apiUrl: process.env.IWISH_AUTH_API_URL!, portalUrl: process.env.IWISH_AUTH_PORTAL_URL!, appKey: "reporting", clientId: process.env.IWISH_AUTH_CLIENT_ID!, clientSecret: process.env.IWISH_AUTH_CLIENT_SECRET!, redirectUri: "https://reporting-staging.example.com/auth/callback" }); ``` 分别创建 login、callback 和 logout Route Handler,直接返回 `beginLogin`、`handleCallback` 和 `logout` 的 Response。SDK 自动使用 PKCE,并把 App Session 写入 `HttpOnly`、`SameSite=Lax` Cookie;HTTPS 环境同时启用 `Secure`。 ## 受保护接口 ```ts export async function GET(request: Request) { const session = await auth.requireAppAccess(request); const member = await db.appMember.findUnique({ where: { iwishAuthUserId: session.user.id } }); if (!member || member.status !== "active") { return Response.json({ error: "local_member_required" }, { status: 403 }); } const allowed = await localPermissions.has(member.id, "report.read"); if (!allowed) return Response.json({ error: "local_permission_denied" }, { status: 403 }); return Response.json(await loadReports({ member, clientAssignments: session.clientAssignments })); } ``` `requireAppAccess` 验证 session、当前 App、组织开通和 Auth 生命周期;本地成员与 `report.read` 由业务 App 自己检查。不要使用已返回 410 的 `requirePermission`。 ## Server Component Server Component 可以通过受保护的服务端函数读取 session,但不要把 opaque token、Client Secret 或完整员工敏感字段传给 Client Component。只序列化页面确实需要的展示字段。缓存 key 必须至少包含 `iwish_auth_user_id`、App、本地授权版本和必要的数据范围;收到停用或客户团队 Webhook 后清理相关缓存。 ## 验收 验证 callback 精确匹配、跨 App cookie 被拒绝、无本地成员 403、本地角色不足 403、assignment 不自动授权、飞书离职后旧 session 失效、外部邮箱账号仍需本地成员。production 不得配置通配 callback、宽泛 Cookie Domain 或浏览器可读 session cookie。 --- # Express > 使用统一中间件保护 Node.js API # Express 接入 Express Adapter 负责解析 IWish Auth App Session 和恢复统一身份。业务 App 必须在其后连接自己的成员与授权 middleware。 ```ts import { createIWishExpressAuth } from "@iwish/auth-sdk/express"; const iwishAuth = createIWishExpressAuth({ apiUrl: process.env.IWISH_AUTH_API_URL!, appKey: "reporting", cookieName: "iwish_reporting_session" }); app.get( "/api/reports", iwishAuth.requireAuth(), iwishAuth.requireAppAccess(), requireLocalMember(), requireLocalPermission("report.read"), async (request, response) => { const session = request.iwishAuth!.context; response.json(await loadReports({ iwishAuthUserId: session.user.id, clientAssignments: session.clientAssignments })); } ); ``` `requireAuth()` 从 Bearer token 或 HttpOnly Cookie 读取 opaque token并调用 `/v1/sso/session`。`requireAppAccess()` 确认 `sessionVersion=0.3`、`authorizationDomain=application`、session 的 `appKey` 与当前服务配置一致,并拒绝已失效的统一身份。`requireLocalMember` 和 `requireLocalPermission` 是业务 App 自己实现的 middleware,不属于 IWish Auth SDK。 建议在 Request 类型中扩展两组字段:`request.iwishAuth` 保存统一身份和上下文;`request.appMember` 保存本地成员、角色和计算后的数据范围。不要把二者合成一个不透明布尔值,否则无法正确处理员工离职、本地角色撤销和项目分工变化。 旧版 `iwishAuth.requirePermission()` 只返回 410 `application_owned_authorization`,用于暴露未完成迁移的代码路径。不得捕获该错误后放行,也不得降级到 `x-role`、邮箱或前端声明角色。 错误策略:401 可引导登录;Auth 403 表示统一身份或 App 生命周期无效;本地成员不存在、停用、无角色或权限不足返回业务 App 自己的 403;Auth 网络异常与解析异常默认拒绝并记录 request ID。对高风险写操作,每次请求都重新执行本地授权和数据范围检查。 --- # FastAPI > 使用 Python SDK 和依赖注入接入 # FastAPI 接入 Python SDK 0.3 与 TypeScript 使用同一 App Session 契约。FastAPI Adapter 负责统一登录、callback、HttpOnly Cookie、session introspection 和登出;业务角色与权限依赖由业务 App 自己实现。 ```python from fastapi import Depends, FastAPI, HTTPException, Request from iwish_auth.fastapi import IWishFastAPIAuth, IWishFastAPIAuthOptions app = FastAPI() auth = IWishFastAPIAuth(IWishFastAPIAuthOptions( api_url=IWISH_AUTH_API_URL, portal_url=IWISH_AUTH_PORTAL_URL, app_key="reporting", client_id=IWISH_AUTH_CLIENT_ID, client_secret=IWISH_AUTH_CLIENT_SECRET, redirect_uri="https://reporting-staging.example.com/auth/callback", )) @app.get("/api/reports") async def reports(request: Request, session=Depends(auth.require_app_access)): member = await local_members.find_by_auth_user_id(session["user"]["id"]) if not member or member.status != "active": raise HTTPException(status_code=403, detail="local_member_required") if not await local_permissions.has(member.id, "report.read"): raise HTTPException(status_code=403, detail="local_permission_denied") return {"reports": await load_reports(member), "clientAssignments": session["clientAssignments"]} ``` Adapter 提供 `login`、`callback`、`logout`、`get_session`、`require_auth`、`require_app_access`、`require_organization` 和 `get_client_assignments`。Client 提供 `get_app_session`、`get_identity`、`get_employee_context`、`get_client_assignments` 和 `revoke_app_session`。 旧 `require_permission`、`check_permission`、`check_permissions_batch` 和 `check_user_permission` 会返回 410 `application_owned_authorization`。它们仅用于阻止旧接入静默继续,不得作为生产授权路径。 本地成员表使用 `iwish_auth_user_id` 唯一关联 `session["user"]["id"]`。无成员、停用或本地角色不足默认 403。`clientAssignments` 只能参与业务筛选;assignment 存在不能绕过本地权限,assignment 为空也不能自动允许所有客户。内部员工飞书离职后 Auth session 必须失败,即使本地成员仍为 active。 生产环境必须使用 HTTPS、精确 callback、Secure Cookie 和服务端 Secret 管理。不要直接调用飞书或 Supabase Auth,不要信任来自浏览器的用户 ID、角色 Header 或项目范围。 --- # 跨 App SSO > 授权码、PKCE、App session 和登出边界 # 跨 App SSO 用户先在 IWish Auth Portal 完成飞书或邮箱登录。访问已开通业务 App 时,Portal 为目标 OAuth Client 生成一次性授权码;业务 App 服务端使用 Client Secret 与 PKCE verifier 交换 opaque App Session。用户不需要在每个 App 再输入密码。 ## 标准流程 1. App 服务端生成 `state`、PKCE verifier 与 challenge,并把短期值放入 HttpOnly Cookie。 2. 浏览器跳转 `/v1/sso/authorize`,参数包含 client、精确 redirect URI、state 与 challenge。 3. Auth 验证用户、员工状态、组织、App、OAuth Client 和 callback。 4. callback 获得一次性 code,服务端校验 state 后调用 `/v1/sso/token`。 5. 服务端把返回的 opaque token 存入独立 HttpOnly App Cookie。 6. 每个受保护请求通过 `/v1/sso/session` 或 SDK 恢复 App Session 0.3。 授权码短时、一次性且绑定 Client、callback 与 PKCE challenge。Client Secret 只能在业务 App 服务端 Secret 管理中。不同 App 使用不同 OAuth Client 和 Cookie 名称,不能共享 session。 ## App Session 0.3 响应包含 `sessionVersion="0.3"`、`authorizationDomain="application"`、`session`、`user`、`employee`、`clientAssignments`、`organization` 和 `app`。`app.authorizationMode` 固定为 `application_owned`。响应不包含业务 `roles`、`permissions` 或 `matchedRoles`。 业务 App 使用 `session.user.id` 作为 `iwish_auth_user_id` 查找本地成员,再执行本地角色、permission 与业务数据范围检查。`clientAssignments` 只是客户项目与职责上下文,不是业务授权凭证。 ## 内部与外部身份 内部员工通常为 `identitySource=feishu`,并返回员工、部门、岗位和在职状态。外部客户账号为 `identitySource=email`,`employee=null`,`clientAssignments=[]`。两类身份都必须经过当前 App 的本地成员与角色管理;不能因为邮箱属于客户域名就自动授权。 ## 登出与撤权 单 App 登出撤销当前 App Session 并删除 Cookie;全局登出撤销用户所有 App Session。用户停用、飞书离职或冻结、组织停用、组织关闭 App、OAuth Client 停用等事件必须使新旧 session fail closed。本地角色撤销由业务 App 自己立即生效,不要求修改 Auth。 ## 安全验收 必须测试 state/PKCE 错误、callback 不匹配、code 重放、跨 Client code、跨 App session、过期和撤销 session、伪造 Header、浏览器泄漏 Client Secret、无本地成员 403、外部账号无本地角色 403。业务 App 禁止直接接飞书、Supabase Auth 或 Auth 数据库。 --- # 飞书身份边界 > 内部员工身份、组织同步和业务 App 禁止事项 内部员工的飞书身份就是 IWish Auth 的统一员工身份。`auth_users` 只是权限、会话、审计和外键所需的不可见技术主体,管理后台不得要求维护第二套“Auth 内部员工账号”。业务 App 禁止直接接飞书,只能读取 IWish Auth 返回的稳定身份上下文。 ## 1. 身份来源 | 使用者 | 登录方式 | `identitySource` | `employee` | | --- | --- | --- | --- | | 公司内部员工 | 飞书 OAuth | `feishu` | 飞书员工档案 | | 外部客户或协作账号 | 邀请/邮箱密码 | `email` | `null` | 飞书真实工作邮箱不作为内部身份主键。Auth 使用稳定飞书标识和受控 synthetic email,避免与外部邮箱身份错误合并。 Supabase Auth 的企业登录 provider 标识为 `custom:feishu`。该值只属于 IWish Auth 平台配置,业务 App 不得直接使用它发起飞书授权。 ## 2. 飞书同步内容 IWish Auth 同步: - 公司部门层级和部门状态。 - 员工、工号、职位、上级、部门关系和在职状态。 - 离职、冻结、未入职等状态变化。 同步采用全量分页、批次写入和成功后 finalize。任何分页或字段权限错误都不能把本轮未读到的员工误判为离职。 ## 3. 业务 App 可以获取什么 App session 的 `employee` 可能包含: ```json { "feishuUserId": "ou_xxx", "openId": "ou_xxx", "unionId": "on_xxx", "employeeNo": "E1001", "workEmail": "staff@example.com", "jobTitle": "广告优化师", "managerUserId": "ou_manager", "departmentIds": ["od_marketing"], "departmentNames": ["广告投放部"], "employmentStatus": "active", "lastSyncedAt": "2026-07-20T08:00:00Z" } ``` 字段可能为 `null` 或空数组,业务 App 必须容忍。`employmentStatus` 不是业务权限;统一 session 有效后仍需检查 App 本地成员、角色、permission 和数据范围。 ## 4. 业务 App 禁止事项 - 不创建自己的飞书应用来完成统一登录。 - 不请求飞书 OAuth、通讯录或 tenant token。 - 不保存飞书 access token、refresh token、App Secret 或用户凭据。 - 不通过飞书邮箱自动合并外部账号。 - 不复制部门/员工为另一套可编辑主数据。 - 不通过直接查询 Auth/Supabase 表获取员工。 业务 App 只使用 IWish Auth SDK 返回的稳定上下文。是否展示部门、按部门筛选或使用职位属于业务 App 自身决策。 ## 5. 离职与组织变化 员工离职或冻结后,IWish Auth 会使统一登录和 App session 失效,并停止返回有效 `clientAssignments`。业务 App 不应等待本地人工禁用。 部门调整会在后续 session 上下文中反映。业务 App 如果缓存部门用于报表,需要设置明确的同步/失效策略,不能把缓存作为身份来源。 ## 6. 外部账号边界 外部客户不要求拥有飞书账号。管理员可以先独立创建客户项目,再按需邀请外部访问账号。外部账号是否能查看某个项目取决于 App 权限和业务 App 数据授权,不由“客户项目存在”自动推导。 ## 7. 平台管理边界 飞书目录状态由 `GET /v1/admin/feishu/status` 查询,手动同步由受保护的 Admin API 发起。只读管理需要 `auth.feishu.read`,执行同步需要 `auth.feishu.sync`。这些权限只授予平台管理员,不属于业务 App Manifest。 --- # 客户项目上下文 > 正确消费 clientAssignments 与业务授权 `clientAssignments` 是 IWish Auth 对“当前内部员工正在服务哪些客户项目、承担什么职责”的统一表达。各业务 App 可以按需消费,不要求所有 App 都使用。 ## 1. 数据来源 管理员独立创建客户项目,然后从 active 飞书员工目录选择成员,并为一个员工配置一个或多个项目职责、主责标记、平台范围和生效期。 创建客户项目不需要外部客户账号,也不需要先创建客户组织。 平台管理员通过 `POST /v1/admin/client-projects` 创建项目,通过 `POST /v1/admin/client-team-assignments` 维护员工职责。飞书身份就是统一内部身份,项目团队成员必须从已同步的 active 飞书员工目录选择。 ## 2. Session 结构 ```json { "clientAssignments": [ { "clientId": "00000000-0000-0000-0000-000000000401", "clientCode": "acme", "clientName": "Acme 出海项目", "shortName": "Acme", "roles": [ { "assignmentId": "00000000-0000-0000-0000-000000000501", "code": "ad_optimizer", "name": "广告平台优化师", "category": "advertising", "isPrimary": false, "platformScopes": ["google", "meta"], "validFrom": "2026-07-01", "validUntil": null } ] } ] } ``` 只返回项目和职责均为 active、当前日期在有效期内、员工仍为 active 的记录。同一项目的多个职责聚合到一个项目条目。 ## 3. 推荐用法 - 为项目选择器提供默认可见项目。 - 在工作台展示当前员工的服务关系和职责。 - 缩小默认查询范围,减少误选客户。 - 根据 `platformScopes` 预选 Google、Meta 等广告平台。 - 在创建任务或需求时自动填充项目负责人候选人。 ## 4. 不能做什么 `clientAssignments` 不是权限凭证,不能替代 App 本地 permission 或业务数据授权。下面的判断是错误的: ```ts // 错误:绑定项目不代表拥有删除权限 if (session.clientAssignments.some((item) => item.clientId === clientId)) { await deleteReport(reportId); } ``` 正确顺序: ```ts const session = await auth.requireAppAccess(request); const member = await localMembers.requireActive(session.user.id); await localPermissions.require(member.id, "report.delete"); const assignment = session.clientAssignments.find((item) => item.clientId === clientId); await assertBusinessDataAccess({ member, assignment, clientId, reportId }); await deleteReport(reportId); ``` 第一层是 IWish Auth App 权限,第二层是业务 App 自己的数据授权。两层都必须通过。 ## 5. 空数据和外部账号 业务 App 必须把空数组视为正常状态: - 某些内部岗位不参与任何客户项目。 - 新员工尚未分配项目。 - 外部邮箱账号的项目可见范围由业务 App 自身模型决定,IWish Auth 不会把外部账号自动变成内部项目团队成员。 不要因为 `clientAssignments` 为空就把用户当成未登录,也不要自动授予全部项目。 ## 6. 缓存与变更 项目团队会动态调整。优先使用当前 App session 返回值;长任务应在执行关键写操作前重新解析 session。不要永久存储 `clientAssignments` 快照作为授权依据。 --- # Manifest 2.0 > 声明 App 元数据和所需统一上下文 # Manifest 2.0 Manifest 是业务 App 向 IWish Auth 声明“我是谁、入口在哪里、需要哪些统一上下文”的机器契约。它不是业务角色树,也不承载页面、按钮、字段或数据范围权限。 ```json { "$schema": "https://auth-docs-staging.iwishapp.cn/auth-manifest.schema.json", "schemaVersion": "2.0", "appKey": "crm", "name": "CRM 系统", "description": "销售客户关系管理", "appUrl": "https://crm-staging.example.com", "environment": "staging", "manifestVersion": "2026.07.22-1", "authorizationMode": "application_owned", "requiredContext": ["identity", "employee", "departments", "clientAssignments"] } ``` ## 字段说明 | 字段 | 规则 | | --- | --- | | `schemaVersion` | 当前公开版本固定为 `2.0` | | `appKey` | 全局唯一、全小写,发布后保持稳定 | | `name` / `description` | 中文产品名称与用途说明 | | `appUrl` | HTTPS 业务入口;仅本地 dev 可按规则使用 localhost | | `environment` | `dev`、`staging` 或 `prod` | | `manifestVersion` | 每次提交唯一,建议日期加序号 | | `authorizationMode` | 业务 App 固定为 `application_owned` | | `requiredContext` | `identity`、`employee`、`departments`、`clientAssignments` 的去重子集 | `identity` 提供稳定用户身份;`employee` 提供飞书员工资料与在职状态;`departments` 提供部门链路;`clientAssignments` 提供客户项目与服务职责。业务 App 只声明确实会消费的上下文,字段为空或数组为空时必须正常处理。 ## 明确禁止 Manifest 2.0 不允许 `roles`、`permissions`、Client Secret、数据库连接、环境密钥、真实客户 ID 或用户 ID。若提交这些字段,Schema 和 CLI 必须拒绝。业务 App 的 `viewer/member/manager/admin` 等角色应保存在业务 App 自己的数据库,由业务 App 自己提供管理界面、服务端检查和审计。 ## 预检与同步 管理 API 为 `POST /v1/admin/apps/{appKey}/manifest/validate` 和 `POST /v1/admin/apps/{appKey}/manifest/sync`。同步请求必须显式包含 `confirmed=true`,并使用 Auth 管理员 Bearer Token 或具备 `auth.manifest.write` 的 Service Token。 ```powershell iwish-auth manifest validate --file auth.manifest.json iwish-auth manifest sync --file auth.manifest.json --confirm ``` 预检差异只包含名称、描述、应用地址、授权模式和上下文增删。同步不会写入业务角色或权限表。生产同步应由具备 `auth.manifest.write` 的管理员或 Service Token 执行,并记录版本、环境、提交人和时间。 ## 版本策略 新增上下文字段通常向后兼容;删除业务正在依赖的上下文需要先修改 App,再发布新 Manifest。`appKey`、授权模式语义或身份关联键不能静默变更。`auth` 自身使用独立的内部 Manifest 管理平台权限,不能套用业务 Manifest 2.0。 --- # App 本地权限设计 > 本地成员、角色、权限和数据范围模型 # App 本地权限设计 业务 App 的成员、角色和权限属于业务 App,不上传到 IWish Auth。统一身份并不等于统一业务授权:Auth 只证明“这个人是谁、属于哪个组织、员工是否有效、可以进入哪个 App”,业务 App 决定“他在本 App 能做什么、能看哪些数据”。 ## 1. 建议数据模型 ```text app_members(id, iwish_auth_user_id, status, created_at) app_roles(id, key, name, status) app_permissions(id, key, name) app_role_permissions(role_id, permission_id) app_member_roles(member_id, role_id) app_authorization_audit(actor_member_id, action, target_id, before, after, created_at) ``` `iwish_auth_user_id` 必须唯一并直接来自 App Session 的 `session.user.id`。不要在每次请求中按邮箱猜测身份,也不要保存飞书 token、Supabase token 或员工密码。 ## 2. 权限命名 本地 permission 建议使用稳定业务动作,例如 `contact.read`、`contact.write`、`report.export`、`settings.manage`。不要把客户 ID、部门 ID、用户 ID、环境名或 UI 组件写进 key。数据范围应通过独立规则表达,例如 owner、team、assigned_client、all,而不是创建成千上万个带客户名的角色。 ## 3. 服务端顺序 ```text requireAppAccess -> 根据 iwish_auth_user_id 查 active 本地成员 -> 计算本地角色与 permission -> 校验资源归属、客户范围和字段规则 -> 执行业务动作 ``` 任何一步缺失都默认拒绝。前端菜单、按钮或路由守卫只能改善体验,不能替代服务端授权。高风险操作如导出、配置、成员管理和删除应使用独立 permission,并写入本地审计日志。 ## 4. 客户项目上下文 `clientAssignments` 可用于默认客户筛选或确认员工参与某项目,但它不是 permission。即使存在 assignment,用户没有本地角色或本地权限时仍返回 403;即使本地角色足够,assignment 为空时也不能自动扩大到全部客户。每个 App 应明确自己的组合规则并增加测试。 ## 5. 生命周期 本地角色变化由 App 自己立即生效并清理缓存。Auth 用户停用、飞书离职、组织或 App 停用会使统一 session 失效,即使本地角色仍存在也不能访问。角色 key 的语义不得静默改变;拆分或合并权限时先新增、迁移、双读验证,再删除旧值,并保留审计与回滚证据。 --- # SDK API Reference > TypeScript 和 Python SDK 0.3 稳定接口 # SDK 0.3 参考 SDK 0.3 负责登录、授权码交换、App Session 解析、身份与公共上下文读取、登出和 Webhook 验签。它不判断业务 App 的本地角色或权限。 ## TypeScript Client - `exchangeAuthorizationCode(request)`:服务端使用 Client Secret 与 PKCE verifier 交换一次性 code。 - `getAppSession({ forceRefresh? })`:解析 opaque App Session 0.3。 - `getIdentity()`:读取稳定 IWish Auth 用户身份。 - `getEmployeeContext()`:读取飞书员工、部门、岗位、上级和在职状态。 - `getClientAssignments()`:读取当前有效客户项目职责。 - `revokeAppSession()`:撤销当前 App session。 `checkPermission`、`checkPermissionsBatch` 和 `checkUserPermission` 已退出公开业务授权契约,调用会返回 410 `application_owned_authorization`。新代码不得依赖这些方法。 ## Next.js Adapter `createIWishAuth(options)` 提供 `beginLogin`、`handleCallback`、`logout`、`getSession`、`requireAuth`、`requireAppAccess`、`requireOrganization` 和 `getClientAssignments`。受保护路由先调用 `requireAppAccess`,再使用 `session.user.id` 查询 App 本地成员并执行本地授权。 ```ts const session = await auth.requireAppAccess(request); const member = await members.requireActive(session.user.id); await permissions.require(member.id, "report.read"); ``` 旧 `requirePermission` 仅保留为明确失败的迁移护栏,不是可用授权 API。 ## Express Adapter `createIWishExpressAuth(options)` 提供 `requireAuth()`、`requireAppAccess()` 和 `requireOrganization()` middleware。成功后把 `{ context, token }` 写入 `request.iwishAuth`。随后调用业务 App 自己的成员与权限 middleware。不要从浏览器 Header 接收角色。 ## Python 与 FastAPI Python Client 提供 `exchange_authorization_code`、`get_app_session`、`get_identity`、`get_employee_context`、`get_client_assignments` 和 `revoke_app_session`。FastAPI Adapter 提供 `login`、`callback`、`logout`、`get_session`、`require_auth`、`require_app_access`、`require_organization` 和 `get_client_assignments`。 `check_permission`、`check_permissions_batch`、`check_user_permission` 和 `require_permission` 同样只返回 410 迁移错误。 ## App Session 0.3 稳定字段为:`sessionVersion="0.3"`、`authorizationDomain="application"`、`session`、`user`、`employee`、`clientAssignments`、`organization` 和 `app`。`app.authorizationMode` 为 `application_owned`。响应中不存在业务 `roles`、`permissions` 或 `matchedRoles`;SDK 发现旧结构或跨 App session 时必须 fail closed。 ## 错误处理 401 表示没有有效统一身份,通常跳转登录;403 表示 Auth 层用户、员工、组织、App 或 OAuth Client 不可用,不应循环登录;410 `application_owned_authorization` 表示仍在调用已退出的中央业务权限 API;429 应按 `Retry-After` 重试;5xx 应记录 request ID 并默认拒绝。业务 App 本地成员或权限不足统一由 App 返回自己的 403 错误码。 Webhook 使用 `verifyIWishWebhook` 或 `verify_iwish_webhook` 校验时间戳、HMAC-SHA256 签名和事件 ID。所有 Client Secret、Service Token 和 Webhook Secret 仅放在服务端 Secret 管理中。 --- # Service Token 与 Webhook > 机器身份、签名验签、幂等投递和上下文缓存失效 # Webhook 与 Service Token Service Account 用于 Manifest 自动化和平台级后端任务;Webhook 用于通知业务 App 统一身份、员工状态、组织开通和客户项目上下文变化。两者都不能代替业务 App 本地授权。 ## Service Token 管理员创建 Service Account 后签发 `iwa_` Token。明文只显示一次,数据库仅保存 hash。为自动同步 Manifest 的任务授予最小 `auth.manifest.write` scope,并绑定目标 App 或组织。业务 App 不使用 Service Token 远程检查本地角色或 permission;旧 `auth.permission.check` 不属于公开业务契约。 Token 只能保存在服务端 Secret 管理中,必须设置用途、负责人、最小 scope 和到期时间。轮换时先部署新 Token,再吊销旧 Token,并检查最近使用记录。浏览器、移动端和公共仓库不得持有 Token。 ## Webhook 请求 ```text IWish-Event-Id: IWish-Event-Type: client_team.changed IWish-Delivery-Id: IWish-Delivery-Attempt: 1 IWish-Timestamp: IWish-Signature: v1= ``` 使用 SDK 的 `verifyIWishWebhook` 或 `verify_iwish_webhook`,按原始请求体和时间戳校验 HMAC-SHA256。必须设置时间漂移窗口,并用 Event ID 做幂等;不能先解析再重新序列化 JSON 后验签。 Envelope 包含事件、actor、subject、组织/App 范围、`cacheInvalidation` 和 data。常用事件包括 `user.disabled`、`organization.app_disabled`、`employee.disabled`、`client_project.changed` 和 `client_team.changed`。收到事件后清理与 `userIds`、`organizationIds`、`appKeys` 或 `clientProjectIds` 相交的 session/context 缓存。 本地角色变化不由 Auth Webhook 发布,业务 App 应在自己的角色管理事务中更新本地审计并清理本地授权缓存。Auth 客户项目团队变化只刷新上下文,不得自动增加本地角色或 permission。 ## 投递与失败 平台使用退避重试,超过上限进入死信。接收端应快速验签、落幂等记录后返回 2xx,把重任务放入队列。4xx 表示配置或签名问题,5xx 表示临时失败。管理员可查看投递、重试死信和轮换 Secret。生产 URL 必须为公网 HTTPS,禁止通配目标、跳过验签或在错误时默认放行业务请求。 验收至少覆盖正确签名、错误签名、过期时间戳、重复 Event ID、Secret 轮换、接收端超时、死信重试,以及客户团队变化不会静默扩大 App 本地权限。 --- # 受控身份目录 > 业务 App 通过绑定的 Service Token 查询最小身份信息 # 受控身份目录 业务 App 使用受控身份目录同步或按需查询当前组织内可用的员工与外部账号。该接口只提供身份和组织上下文,不提供业务角色、权限或业务数据范围。 ## 前置条件 1. Auth 管理员已经为业务 App 创建 Service Account。 2. Service Account 绑定目标 App 和目标组织。 3. Service Token 包含 `auth.directory.read` scope。 4. 目标组织已开通该 App。 Service Token 只能放在业务 App 服务端 Secret 管理中,不得发送给浏览器或写入仓库。 ## API ```http GET /v1/directory/users?organizationId={organizationId}&appKey={appKey}&limit=50 Authorization: Bearer iwa_... ``` 可选参数: - `query`:按姓名或邮箱模糊搜索,最长 120 个字符。 - `cursor`:上一页返回的 `nextCursor`。 - `limit`:1 至 100,默认 50。 响应只包含: - 稳定的 IWish Auth User ID。 - 邮箱、显示名称和头像。 - `email` 或 `feishu` 身份来源。 - 账号状态。 - 飞书员工的在职状态、职位和部门名称。 不返回手机号、飞书原始凭据、OAuth Token、客户项目数据、平台角色或业务权限。 ## TypeScript ```ts import { createIWishAuthClient } from "@iwish/auth-sdk"; const auth = createIWishAuthClient({ apiUrl: process.env.IWISH_AUTH_API_URL!, token: process.env.IWISH_AUTH_SERVICE_TOKEN! }); const page = await auth.listIdentityDirectory({ organizationId: process.env.IWISH_AUTH_ORGANIZATION_ID!, appKey: "crm", limit: 50 }); ``` ## Python ```python from iwish_auth import IWishAuthClient async with IWishAuthClient( api_url=AUTH_API_URL, token=AUTH_SERVICE_TOKEN, ) as auth: page = await auth.list_identity_directory( organization_id=AUTH_ORGANIZATION_ID, app_key="crm", limit=50, ) ``` ## 拒绝规则 - `401 invalid_service_token`:Token 无效、撤销或过期。 - `403 insufficient_service_scope`:缺少 `auth.directory.read`。 - `403 service_scope_mismatch`:Token 与目标组织或 App 不匹配。 - 组织、App、成员、账号或飞书员工状态不可用时,该身份不会进入可用目录。 业务 App 必须用 `user.id` 建立本地成员外键,并继续在本地管理角色、权限和数据范围。 --- # OAuth Client 生命周期 > 创建、环境隔离、Secret 轮换与停用恢复 # OAuth Client 生命周期 每个业务 App 的每个环境必须使用独立 OAuth Client。`dev`、`staging` 和 `prod` 的 Client ID、Client Secret 与回调地址不得复用。 ## 创建 在 Auth 管理后台的“应用管理”中选择 App,创建对应环境的 OAuth Client,并填写至少一个精确回调地址。 创建成功后只展示一次明文 Client Secret。立即将其写入目标环境的服务端 Secret 管理,不得写入浏览器、日志、回执文件或 Git。 ## 环境隔离 Auth API Worker 只接受与自身 `ENVIRONMENT` 相同的 OAuth Client: - dev Worker 只接受 dev Client。 - staging Worker 只接受 staging Client。 - prod Worker 只接受 prod Client。 环境不匹配必须拒绝授权码创建、授权码交换和 App Session 创建。 ## 回调地址 - 只允许精确匹配,不支持通配符。 - 必须使用业务 App 服务端回调路由。 - 生产环境必须使用 HTTPS。 - 删除时必须至少保留一个有效回调地址。 ## Secret 轮换 轮换后: 1. 新 Secret 只展示一次。 2. 数据库只保存新 Secret 的哈希。 3. 旧 Secret 立即失效。 4. 该 Client 的未消费授权码和活跃 App Session 立即撤销。 业务 App 应先准备新 Secret 的安全发布窗口,再执行轮换并重新登录验收。 ## 停用与恢复 停用 Client 会阻止新授权,并立即撤销未消费授权码和活跃 App Session。恢复只允许创建新会话,不会恢复旧授权码或旧 Session。 账号、组织、组织成员、全局 App 或组织级 App 被停用,账号进入强制密码重置,以及飞书员工离职、冻结或同步为不可用时,也会在同一数据库事务中撤销受影响的授权码和 App Session。 ## 管理 API - `GET /v1/admin/apps/{appId}/sso-clients` - `POST /v1/admin/apps/{appId}/sso-clients` - `POST /v1/admin/sso-clients/{clientId}/rotate-secret` - `PATCH /v1/admin/sso-clients/{clientId}/disable` - `PATCH /v1/admin/sso-clients/{clientId}/restore` - `POST /v1/admin/sso-clients/{clientId}/redirect-uris` - `DELETE /v1/admin/sso-clients/{clientId}/redirect-uris` 所有生命周期操作都必须写入审计日志。 --- # 外部账号与 Portal > 邀请、密码恢复、账号生命周期和最近访问 App # 外部账号与 Portal 外部客户或合作方不要求使用飞书。他们通过 Auth 管理员邀请后使用邮箱密码登录,并与内部飞书员工共用同一套跨 App SSO。 ## 邀请 管理员在“用户管理”中选择目标组织,填写邮箱、有效期和回调地址后发送邀请。 邀请具有以下状态: - `pending`:待接受,可以重发或撤销。 - `accepted`:已完成注册。 - `revoked`:已撤销,不可继续使用。 - `expired`:已过期,可以生成新 Token 后重发。 邀请 Token 只保存哈希。撤销邀请会同时停用尚未完成接入的目标成员,并撤销其在目标组织中的 App Session。 ## 密码流程 - 登录页提供“忘记密码”入口。 - 恢复链接进入 Portal 后必须设置新密码。 - 管理员可以为外部账号发送密码重置邮件。 - `pending_password_reset` 用户完成密码更新后,Portal 调用 `POST /v1/me/password-changed` 恢复正常状态。 - 已登录外部账号可以在“账号与安全”中修改密码。 内部飞书员工不得使用邮箱密码创建、邀请或密码管理入口;其身份、组织关系和在职状态由飞书负责。 ## Portal Portal 展示: - 当前身份与身份来源。 - 飞书部门和在职信息。 - 客户项目职责上下文。 - 可切换组织。 - 可访问、未配置 SSO、缺少 URL 或已停用的 App 状态。 - 跨设备记录的最近访问 App。 - 外部账号的密码管理入口。 `clientAssignments` 只表示客户项目职责,不代表业务 App 权限。进入业务 App 后,业务 App 仍需根据 Auth User ID 查询自己的本地成员、角色、权限和数据范围。 ## 管理 API - `GET /v1/admin/invitations` - `PATCH /v1/admin/invitations/{invitationId}/revoke` - `POST /v1/admin/invitations/{invitationId}/resend` - `POST /v1/admin/users/{userId}/password-reset` 邀请、重发、撤销、密码重置、账号停用与恢复都必须进入审计日志。 --- # V1 RC 契约与发布 > 版本冻结、兼容规则、制品校验和破坏性变更要求 # V1 RC 契约与发布策略 ## 当前冻结版本 IWish Auth 的 V1 RC 标识为 `v1.0.0-rc.1`。冻结范围包括: - OpenAPI `1.0.0-rc.1` - App Session `0.3` - Manifest Schema `2.0` - Account Migration Schema `1.0` - TypeScript SDK `0.3.0` - Python SDK `0.3.0` - CLI `0.4.0` SDK 和 CLI 的包版本不等于平台 RC 版本。业务 App 必须同时记录平台 RC 和所用 SDK/CLI 版本。 ## 兼容性规则 以下改动允许在同一主版本发布: - 新增可选响应字段。 - 新增可选请求字段。 - 新增 API 路径或 Webhook 事件。 - 修复实现但不改变已声明语义。 - 扩充枚举前已明确要求调用方容忍未知值。 以下改动属于破坏性变更: - 删除或重命名字段、路径、错误码或事件。 - 把可选字段改为必填。 - 改变字段类型、Session 语义、身份主键或授权边界。 - 缩短兼容窗口,导致已发布 SDK 或现有 App 不能工作。 - 改变 Manifest/迁移 Schema 且旧文档无法通过校验。 破坏性变更必须提升主版本,并同时提供迁移说明、兼容时间窗、双版本测试和回滚方案。不得静默覆盖既有版本化制品。 ## 制品验证 每个 RC 的公开入口为: ```text /releases//release.json /releases//SHA256SUMS /releases//artifacts/* ``` 业务 App 或 AI Agent 应先读取 `release.json`,再按 SHA256 校验所需 SDK、CLI、Schema 和 OpenAPI。不要仅依赖 `/packages/` 的兼容入口判断某次 RC 的完整内容。 ## 数据库兼容 RC 期间的 migration 默认只做向前兼容变更。代码回滚不能恢复数据库结构,因此: - 先新增,再回填,再切换读取,最后在后续主版本清理。 - 禁止在同一发布中删除仍被上一 Worker 版本使用的列、函数或表。 - 破坏性 DDL 必须有独立审批、可恢复备份和隔离恢复演练。 ## 接入方记录 每个业务 App 的 staging/production 发布证据至少记录: - IWish Auth RC 版本。 - SDK/CLI 版本与 SHA256。 - OAuth Client 环境和 callback。 - Manifest 版本。 - App 自身 Git commit、数据库 migration 和回滚点。 --- # 新应用接入 > 从第一天使用 Auth 身份并建立 App 本地成员与角色 # 新应用接入 本指南适用于 `greenfield`:业务 App 没有需要保留的生产账号、本地身份主键、历史身份外键或旧 Session。新应用从第一天使用 IWish Auth 作为唯一身份来源,但业务成员、角色、permission、字段和数据范围仍由 App 自己管理。 ## 1. 数据模型 新应用使用自己的数据库,并至少建立本地成员、角色分配和授权审计: ```text app_members id iwish_auth_user_id # 唯一,不建立跨数据库外键 status created_at app_member_roles member_id role_id assigned_by assigned_at ``` `iwish_auth_user_id` 来自受控 Auth 身份目录或经过验证的 App Session,不允许管理员手工复制粘贴 User ID。业务表继续引用 App 自己的 `app_members.id`,不要把 Auth User ID 扩散为所有业务外键。 ## 2. 成员开通 标准流程是“管理员明确选择身份并分配本地角色”: 1. App 管理员打开业务 App 自己的成员管理页面。 2. App 服务端使用最小权限 Service Token 查询当前组织的 Auth 受控身份目录。 3. 管理员选择一个 active 用户,并选择 App 本地角色。 4. App 在单个事务中创建成员、角色分配和 append-only 审计。 5. 用户通过 IWish Auth SSO 登录;`requireAppAccess` 成功后,App 按 `session.user.id` 加载本地成员。 第一次登录不能自动授予 `admin`、`viewer` 或其他角色,也不能创建一个没有角色但能进入业务数据的空成员。首个 App 管理员必须通过受控 bootstrap、部署审批或双人复核创建,不能采用“第一个登录的人自动成为管理员”。 ## 3. SSO 与本地授权 Manifest 2.0 固定使用 `authorizationMode=application_owned`。服务端接入 PKCE login、callback、HttpOnly Cookie、App Session 0.3、`requireAppAccess` 和登出。每个业务请求按以下顺序处理: ```text Auth Session -> App access -> 员工状态 -> App 本地成员 -> App 本地角色与 permission -> 字段/记录/数据范围 -> 业务响应 ``` `employee`、`departments` 和 `clientAssignments` 是统一上下文,不是业务 permission。App 可以用客户项目分工做默认筛选、工作台入口或流程路由,但敏感查询、编辑、导出和审批必须再次执行 App 本地授权。 ## 4. 不需要迁移工件 `greenfield` 不生成 `auth-migration-plan.json`、mapping ledger、旧密码清理计划或旧 Session 回滚演练。不要为了通过模板而伪造存量账号和迁移数据。只有已经拥有真实生产用户、角色、业务外键和历史数据的 App 才使用 `legacy-migration`。 新应用仍需创建独立 dev、staging、prod OAuth Client、callback 和 Secret,并在 staging 验证飞书员工、外部邮箱、无本地成员、成员停用、角色撤销、跨 App Session、离职和全局登出。 ## 5. 完成标准 - 业务 App 没有本地登录、第二套员工密码或直接飞书接入。 - 浏览器无法读取 Client Secret 或 opaque App Session。 - 管理员不需要手工输入 Auth User ID。 - 无本地成员、成员停用、无角色和权限不足全部 fail closed。 - 首次登录不会自动创建 admin/viewer 或扩大权限。 - 本地角色变化立即生效,Auth 客户项目上下文变化不会静默扩大权限。 - 飞书离职、Auth 停用、组织停用或关闭 App 后,即使本地角色仍存在也不能访问。 - 接入实现只依赖公开 SDK、OpenAPI、Manifest Schema 和文档,不读取 Auth 仓库源码。 下一步阅读[接入流程](/docs/integration-flow)、[快速开始](/docs/quickstart)和[接入验收清单](/docs/integration-checklist)。存量系统请改用[存量账号迁移与无感切换](/docs/account-migration)。 --- # 存量账号迁移与无感切换 > 预绑定原账号并保持角色、业务外键和历史数据不变 # 存量账号迁移与无感切换 本规范只适用于 `legacy-migration`:已经在线运行、拥有本地用户和历史业务数据的 App。目标是替换身份提供方,同时保持原用户主键、角色、客户归属、项目负责人、创建人、审批人和审计历史不变。没有需要保留的生产账号和历史身份数据时,请使用[新应用接入](/docs/greenfield-integration),不要伪造迁移计划。 ## 1. 最终用户体验 完成预绑定后,用户在切换日只经历标准 SSO: ```text 访问原业务 App URL -> 跳转 IWish Auth -> 已有 Auth 会话时直接返回;否则完成飞书或外部邮箱登录 -> App 从 Session 0.3 取得 session.user.id -> 按 iwish_auth_user_id 命中原本地成员 -> 继续使用原角色、原业务数据和原操作记录 ``` 用户不需要重新创建业务账号,不需要重新分配角色,不需要迁移历史数据,也不需要把旧密码交给 Auth。首次切换可能需要重新登录一次;已经登录 Auth Portal 或其他 App 的用户通常只经历自动跳转。 ## 2. 不变量 迁移必须保持: - App 原本地用户主键不变。 - 所有业务外键继续引用原本地用户主键。 - App 本地角色、permission、字段和记录范围不变。 - 历史创建人、负责人、审批人和审计记录不变。 - Auth 只成为身份和 SSO 来源,不接管业务授权。 - 每个 App 的 `iwish_auth_user_id` 唯一关联一个本地成员。 - production 切换前完成预绑定,不在运行期按邮箱猜测身份。 禁止: - 用 Auth User ID 替换全部业务表的本地用户外键。 - 导入旧密码或密码 hash 到 IWish Auth。 - 根据姓名、手机号、未验证邮箱或模糊文本自动合并账号。 - 登录失败时自动创建一个没有历史数据的新本地账号。 - 把逐用户映射、邮箱、姓名、Cookie、Session 或 Secret 提交到 Git。 - 通过长期双登录、Auth Proxy 或可伪造 Header 兜底。 ## 3. App 本地数据模型 推荐保留原成员表并新增稳定关联: ```sql alter table public.app_members add column if not exists iwish_auth_user_id uuid; create unique index if not exists app_members_iwish_auth_user_id_unique on public.app_members (iwish_auth_user_id) where iwish_auth_user_id is not null; ``` 不要为 `iwish_auth_user_id` 建立跨数据库外键。Auth 数据库与业务 App 数据库是独立系统,关联完整性由预绑定、Session 校验和本地唯一索引保证。 迁移期间建议建立 App 私有 ledger: ```sql create table private.auth_identity_migration_ledger ( legacy_user_id text primary key, iwish_auth_user_id uuid unique, state text not null check ( state in ('discovered', 'prelinked', 'conflict', 'excluded', 'failed') ), match_method text check ( match_method in ( 'feishu_employee_id', 'verified_work_email', 'verified_external_email', 'manual' ) ), source_fingerprint text not null, evidence_hash text, reviewed_by text, reviewed_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now() ); ``` Ledger 必须位于不可通过浏览器 Data API 访问的私有 schema,或保存在加密离线工件中。公开迁移计划只保存计数和证据路径,不保存逐用户 PII。 ## 4. 身份匹配优先级 | 优先级 | 方法 | 自动预绑定条件 | 冲突处理 | | --- | --- | --- | --- | | 1 | `feishu_employee_id` | 本地员工号已验证,Auth 中唯一,组织一致 | 人工核对飞书人员档案 | | 2 | `verified_work_email` | 本地企业邮箱已验证,Auth 飞书员工邮箱唯一,组织一致 | 禁止自动选择 | | 3 | `verified_external_email` | 外部账号已完成 Auth 邀请和邮箱验证,本地邮箱唯一 | 重新邀请或人工确认 | | 4 | `manual` | 管理员核对业务归属和 Auth 身份 | 双人复核高权限账号 | 姓名、手机号、显示名和未验证邮箱不能成为自动匹配依据。一个本地用户匹配多个 Auth 用户、多个本地用户匹配一个 Auth 用户、共享账号、缺失身份或组织不一致都必须进入 `conflict`。 ## 5. 七个迁移闸门 ### Gate 1:Inventory - 记录本地用户总数、启用数、停用数、外部用户数和共享账号数。 - 盘点旧登录、密码重置、Session、API Token 和身份 Header。 - 盘点所有引用本地用户主键的业务表和外键。 - 盘点本地角色、permission、字段权限、数据范围和管理员入口。 ### Gate 2:Identity Mapping - 从受控 Auth 管理流程取得组织用户目录。 - 生成只读匹配预览,不直接修改 production。 - 将每个存量用户分类为 `prelinked`、`conflict`、`excluded` 或 `pending`。 - 对管理员、财务、导出和删除权限账号进行人工复核。 - production `cutover_ready` 前 `conflict=0` 且 `pending=0`。 ### Gate 3:Authorization Preservation - 保留本地用户主键和业务外键。 - 对比迁移前后每个角色的权限集合。 - 对比项目负责人、客户归属、创建人、审批人和审计历史计数。 - 本地角色变化仍只在业务 App 生效。 ### Gate 4:Staging SSO - 使用独立 staging App、数据库、OAuth Client、callback 和 Secret。 - 使用脱敏快照或覆盖边界情况的合成数据。 - 验证飞书、外部邮箱、无映射、停用、离职、跨 App 和 App access。 - 验证 viewer/member/manager/admin 以及记录和字段范围。 - staging 登录必须使用在线 Auth 公共文档和 SDK,不读取 Auth 源码。 ### Gate 5:Rollback - 保存上一稳定部署、数据库备份和映射快照。 - 演练回滚部署与恢复映射数据。 - 回滚不能恢复长期双登录或绕过 Auth 的 Header。 - 如果 Auth 身份不可用,App 必须 fail closed。 ### Gate 6:Cutover - 短时冻结本地成员和角色变更。 - 重新生成 inventory 与 mapping diff。 - 原子写入预绑定映射并校验唯一索引。 - 部署新登录版本。 - 关闭旧登录、旧密码重置和旧 Session 接受路径。 - 撤销旧人员 Session 和旧机器凭证。 - 立即验证管理员、普通成员、外部用户和关键 API。 ### Gate 7:Observation - 观察至少 24 小时,推荐 168 小时。 - 监控 SSO callback、401、403、无映射、冲突和本地授权拒绝率。 - 将用户问题区分为身份失败、App access 失败、本地成员失败和本地权限失败。 - 观察期通过后才删除旧密码 hash、旧 Session 表和旧登录代码。 ## 6. 无感切换如何实现 无感来自切换前预绑定,不来自运行期邮箱猜测: 1. 切换前把 `iwish_auth_user_id` 写入原本地成员。 2. 原项目、客户和审计表继续引用本地成员 ID。 3. 切换后 Session 0.3 的 `user.id` 直接命中原成员。 4. App 使用原成员继续执行原本地授权。 未预绑定用户必须显示“账号尚未完成迁移,请联系管理员”,不能创建空账号。切换后新增用户可以走 App 明确设计的新成员 onboarding,但该流程与存量迁移分开。 ## 7. 内部员工和外部账号 内部员工: - 使用飞书 SSO。 - Auth `employee.employmentStatus` 必须为 `active`。 - 离职或停用优先阻断,即使 App 本地角色仍存在。 - 历史业务记录不删除。 外部账号: - 先通过 Auth 邀请并完成邮箱验证。 - 再预绑定到 App 原外部成员。 - 旧 App 密码不迁移;首次使用 Auth 时由 Auth 完成激活或密码设置。 - 不要求外部客户使用飞书。 共享账号必须在迁移前拆分。机器任务必须迁移到 Service Account/Service Token,不能绑定人员 SSO。 ## 8. 迁移计划 1.0 每个 App 必须提交一个无 PII 的 `auth-migration-plan.json`: ```json { "$schema": "https://auth-docs-staging.iwishapp.cn/account-migration.schema.json", "schemaVersion": "1.0", "planVersion": "2026.07.24-1", "environment": "staging", "appKey": "legacy_crm", "strategy": "direct_cutover_prelinked", "status": "mapped", "identityLink": { "localUserIdColumn": "id", "authUserIdColumn": "iwish_auth_user_id", "preserveLocalUserId": true, "localAuthUserIdUnique": true, "runtimeEmailAutolink": false, "allowedMatchMethods": ["verified_work_email", "manual"] }, "authorization": { "owner": "application", "preserveLocalRoles": true, "preserveBusinessForeignKeys": true }, "inventory": { "total": 12, "prelinked": 10, "conflicts": 0, "excluded": 2, "pending": 0 }, "cutover": { "mode": "direct", "oldLogin": "disable_at_cutover", "oldSessions": "revoke_at_cutover", "passwordHashes": "retain_encrypted_until_observation_end", "rollback": "previous_deploy_and_mapping_snapshot", "observationHours": 168 }, "dataHandling": { "mappingLedgerLocation": "app_private_database", "piiInGit": false, "secretsInGit": false }, "gates": [ { "id": "inventory", "status": "passed", "evidence": ["evidence/inventory.json"] }, { "id": "identity_mapping", "status": "passed", "evidence": ["evidence/mapping-summary.json"] }, { "id": "authorization_preservation", "status": "pending", "evidence": [] }, { "id": "staging_sso", "status": "pending", "evidence": [] }, { "id": "rollback", "status": "pending", "evidence": [] }, { "id": "cutover", "status": "pending", "evidence": [] }, { "id": "observation", "status": "pending", "evidence": [] } ] } ``` 校验命令: ```bash iwish-auth migration validate ./auth-migration-plan.json --environment staging ``` Schema 只能保证格式和跨字段闸门,不替代人工身份复核、数据库备份和真实 E2E。 ## 9. 最低验收矩阵 - 原本地用户主键和所有关键业务外键前后完全一致。 - 已预绑定内部员工通过 SSO 返回原账号。 - 已预绑定外部用户通过邮箱登录返回原账号。 - 无映射、重复映射和冲突账号 fail closed。 - App access 撤销、Auth 停用和飞书离职立即阻断。 - 本地角色和数据范围迁移前后一致。 - 旧 Session 在切换后不可继续使用。 - 旧密码登录和密码重置入口不可访问。 - 回滚可以恢复上一部署和映射快照。 - 逐用户映射、Secret、Session 和 Cookie 不进入 Git、构建产物或日志。 ## 10. AI Agent 执行规则 AI 必须先生成 inventory 和只读计划,再修改认证代码。AI 不得根据猜测生成真实 Auth User ID,不得把 synthetic ID 写入 production seed,不得要求用户在聊天中提供密码、Cookie、Session 或 Client Secret。 如果目标 App 存在重复邮箱、共享账号、缺少稳定身份、未知权限分支或无法解释的业务外键,AI 必须把账号标记为冲突并停止 production 切换,不能用“先上线再修复”绕过。 --- # 迁移指南 > 从独立登录与权限体系一刀切迁移 # 存量应用迁移指南 本指南只适用于 `legacy-migration`。目标是把身份提供方统一到 IWish Auth,同时保留业务 App 对自身业务授权的所有权。不要把“统一登录”误解成“把所有角色搬到 Auth”。没有需要保留的生产账号和历史身份数据时,使用[新应用接入](/docs/greenfield-integration),不要生成迁移计划或 mapping ledger。 ## 1. 盘点 列出旧登录入口、密码与 session、用户主键、所有引用用户的业务外键、本地成员、角色、permission、字段规则、记录归属、客户范围、管理员界面和审计。标记哪些是身份事实,哪些是 App 业务规则。建立 staging 数据副本与回滚方案,production 暂不变更。 ## 2. 身份关联 为本地成员增加唯一 `iwish_auth_user_id`,来源只能是 Auth 受控目录或 App Session 的 `user.id`。保留原本地用户主键和所有业务外键。通过私有 migration ledger 在切换前预绑定现有用户,人工处理邮箱冲突;运行时禁止按邮箱自动合并。内部员工不再维护本地密码,外部账号也只在 Auth 使用邀请与邮箱密码。 公开仓库只提交无 PII 的迁移计划 1.0,逐用户邮箱、姓名、Auth User ID 和匹配证据留在 App 私有数据库或加密离线工件。使用 `iwish-auth migration validate` 校验计数和切换闸门。 ## 3. 保留本地授权 保留或重建 `viewer/member/manager/admin`、permission、数据范围和角色管理 UI。把旧授权逻辑整理成服务端统一函数,并增加无成员、停用、权限不足和越权测试。不要把本地角色上传到 Auth,也不要调用旧 `requirePermission` 或远程权限检查。 ## 4. 接入 SSO 创建 Manifest 2.0、组织级 App 开通、OAuth Client 和 callback。接入 PKCE login/callback/logout、HttpOnly Cookie 和 `requireAppAccess`。随后按 `iwish_auth_user_id` 加载本地成员并执行本地授权。需要项目信息时读取 `clientAssignments`,但不把 assignment 直接作为高风险动作权限。 ## 5. Staging 验收 验证真实飞书登录、外部邮箱登录、免重复登录、跨 App 隔离、无本地成员 403、本地角色变化、项目分工变化、员工离职、全局登出和错误 callback。对比迁移前后本地用户 ID、角色、项目负责人、创建人、审批人和历史审计。特别验证 assignment 存在但本地权限不足仍拒绝,以及 assignment 为空不会全量授权。 ## 6. 一刀切上线 冻结成员和权限配置,确认冲突与待处理账号为 0,备份映射和本地授权数据,原子写入预绑定后部署新版本,并立即关闭旧登录入口、本地密码登录和旧 session 接受逻辑。保留本地角色管理,不删除业务授权表。监控 401/403、callback、无映射和本地授权拒绝率。出现身份级故障按上一部署和映射快照回滚;不得通过恢复长期双登录或信任伪造 Header 临时放行。 ## 完成标准 所有身份只来自 Auth;所有业务授权只来自 App 本地;预绑定用户命中原本地用户和原业务数据;Auth 停用和离职能阻断访问;本地角色撤销能立即阻断业务动作;客户项目上下文变化不会静默扩大权限;Client Secret、App Session、逐用户映射和员工敏感字段没有泄漏。 --- # 迁移 Playbook > 按阶段执行、回滚和验收真实 App 迁移 # 一刀切迁移 Playbook 本 Playbook 用于把已有业务 App 迁移到 IWish Auth SSO,同时保留 App 本地授权。所有演练先在独立 staging 完成,production 必须有明确变更窗口、负责人和回滚证据。 ## A. 基线 - [ ] 记录生产版本、数据库备份、用户数、本地成员数、角色数和关键权限测试。 - [ ] 列出旧登录、密码重置、session、身份 Header 与旁路入口。 - [ ] 列出本地角色、permission、字段和数据范围规则,不把它们迁入 Auth。 - [ ] 列出所有引用本地用户主键的业务外键和历史审计字段。 - [ ] 建立旧用户到 `iwish_auth_user_id` 的私有映射 ledger 和异常清单。 - [ ] 生成无 PII 的迁移计划 1.0,并运行 `iwish-auth migration validate`。 ## B. Staging 接入 - [ ] 创建 Manifest 2.0、独立 OAuth Client、精确 callback 和组织开通。 - [ ] 接入 PKCE、服务端 code exchange、HttpOnly Cookie、`requireAppAccess`、单 App/全局登出。 - [ ] 本地成员按 Auth ID 查找;无成员、停用、无角色一律 403。 - [ ] 预绑定用户 SSO 后命中原本地用户主键、原角色和原业务数据。 - [ ] 未映射用户不创建空账号;运行期邮箱关联已禁用。 - [ ] 恢复本地角色管理 UI、服务端 permission 与数据范围检查、变更审计。 - [ ] `clientAssignments` 仅按业务需要消费,不自动授权。 ## C. 真实验收 - [ ] 内部飞书员工和外部邮箱账号均完成 SSO。 - [ ] viewer/member/manager/admin 的页面、接口、字段、导出和设置规则通过。 - [ ] assignment 存在但本地权限不足 403;assignment 为空不扩大权限。 - [ ] 本地角色撤销立即生效;Auth 客户分工变化不修改本地角色。 - [ ] 专用飞书账号 active -> inactive/resigned 后新旧 session 都被阻断。 - [ ] callback 错误、code 重放、跨 App session 和伪造 Header 被拒绝。 - [ ] 本地用户 ID、项目负责人、创建人、审批人和历史审计前后一致。 - [ ] 旧登录和旧 Session 在模拟切换后不可继续使用。 ## D. 上线窗口 - [ ] 冻结身份映射和本地角色配置,确认 `conflicts=0`、`pending=0`,执行最终备份并记录 SHA256。 - [ ] 原子写入最终预绑定映射并校验唯一索引、总数和业务外键。 - [ ] 部署后关闭旧登录、本地密码和旧 session 接受路径。 - [ ] 验证真实登录、关键 API、本地管理员角色、登出和撤权。 - [ ] 监控 Auth 401/403、callback 错误和 App 本地授权拒绝率。 ## E. 回滚 回滚只恢复上一稳定部署和映射/数据库快照,不恢复长期双登录、Auth Proxy 或可伪造身份 Header。若 Auth 身份不可用,业务 App 应 fail closed。若仅本地映射或权限迁移错误,可回滚 App 代码和本地迁移数据,不需要修改 Auth 客户项目或飞书目录。 完成后进入至少 24 小时、推荐 168 小时观察期。观察期通过后才删除旧密码 hash、旧 Session 表和旧登录代码。归档配置、验证记录、审计日志、版本、负责人、异常账号与回滚结果;production Secret 和逐用户 PII 不得进入交接包。 --- # 接入 Checklist > 上线前逐项检查身份、权限与安全边界 # 接入验收清单 ## 接入档案 - [ ] 已明确选择 `greenfield` 或 `legacy-migration`,没有混用两套流程。 - [ ] greenfield 没有伪造迁移数据;legacy-migration 没有通过首次登录创建空账号。 ## App 与 Manifest - [ ] `appKey` 唯一、稳定、全小写。 - [ ] Manifest 使用 Schema 2.0、`authorizationMode=application_owned`。 - [ ] Manifest 只包含元数据和需要的 `requiredContext`,没有业务 `roles`、`permissions` 或 Secret。 - [ ] dev、staging、prod 使用独立 OAuth Client、callback 和 Secret。 - [ ] 组织级 App 已开通,应用地址与回调地址均为预期 HTTPS 地址。 ## 登录与 Session - [ ] login 使用随机 state 和 PKCE S256,callback 在服务端交换 code。 - [ ] Client Secret 与 opaque App Session 从不进入浏览器 JavaScript、日志或仓库。 - [ ] Session Cookie 为 HttpOnly、SameSite=Lax,HTTPS 环境为 Secure。 - [ ] 每个受保护请求调用 `requireAppAccess`,并校验当前 `appKey`。 - [ ] App Session 为 0.3,`authorizationDomain=application`,不依赖业务 roles/permissions 字段。 - [ ] 单 App 登出、全局登出、过期、撤销和跨 App session 均正确失败。 ## 本地授权 - [ ] 使用 `session.user.id` 建立唯一 `iwish_auth_user_id` 本地成员关联,不按邮箱即时猜测。 - [ ] 业务 App 自己保存成员、角色、permission、数据范围和变更审计。 - [ ] 无本地成员、成员停用、无角色或权限不足均 fail closed 并返回 403。 - [ ] 每个高风险服务端动作重新执行本地 permission 与资源数据范围检查。 - [ ] 前端隐藏按钮不能替代服务端授权。 - [ ] 已移除 Auth `requirePermission`、远程业务权限检查和中央业务角色分配依赖。 ## Greenfield 新应用 以下项目只适用于 `greenfield`: - [ ] 使用新数据库,不存在需要保留的生产账号、身份外键或旧 Session。 - [ ] App 管理员通过 App 服务端和最小权限 Service Token 查询 Auth 受控身份目录。 - [ ] 管理员选择身份并在一个事务中创建本地成员、角色分配和 append-only 审计。 - [ ] 管理员不手工复制或粘贴 Auth User ID。 - [ ] 第一次登录不会自动授予 admin、viewer 或其他默认角色。 - [ ] 不自动创建没有明确角色和审计的空成员。 - [ ] 首个 App 管理员通过受控 bootstrap 或部署审批创建,不采用首个登录者自动升级。 - [ ] 没有生成 `auth-migration-plan.json`、mapping ledger 或伪造历史迁移数据。 ## Legacy 存量账号迁移 以下项目只适用于 `legacy-migration`: - [ ] 已生成并通过 CLI 校验 `auth-migration-plan.json`,实例文件不包含逐用户 PII。 - [ ] 原本地用户主键保持不变,所有项目负责人、创建人、审批人和审计外键保持不变。 - [ ] `iwish_auth_user_id` 可空但非空值唯一;不建立跨数据库外键。 - [ ] 逐用户 mapping ledger 位于 App 私有数据库或加密离线工件。 - [ ] 只允许飞书员工 ID、已验证企业邮箱、已验证外部邮箱和人工复核四种匹配方式。 - [ ] 运行期邮箱自动关联、姓名/手机号匹配和未映射用户空账号创建均已禁用。 - [ ] production `cutover_ready` 时 `conflicts=0` 且 `pending=0`。 - [ ] 旧密码不导入 Auth;旧密码 hash 只保留到观察期结束或在切换时清除。 - [ ] 旧登录、密码重置和旧 Session 接受路径在切换时同时关闭。 - [ ] 机器任务和共享账号已迁移到 Service Account 或明确拆分。 ## 员工与客户项目 - [ ] 内部员工通过飞书登录,外部账号通过邀请或邮箱密码登录。 - [ ] `employee` 为空时业务 App 能正确处理;外部账号不被伪装为员工。 - [ ] `clientAssignments` 只用于项目上下文,不替代 App 本地 permission。 - [ ] assignment 存在但本地权限不足仍返回 403。 - [ ] assignment 为空时不会自动扩大到全部项目,也不会误判为未登录。 - [ ] 飞书员工离职、冻结或 Auth 用户停用后,即使本地角色仍存在也不能访问。 ## 禁止旁路 - [ ] 业务 App 没有直接接飞书、Supabase Auth 或 Auth 数据库。 - [ ] 不信任 `x-user-id`、`x-role`、邮箱或前端提交的数据范围。 - [ ] 没有 Auth Proxy、长期双登录、第二套内部员工密码或共享 OAuth Client。 - [ ] 网络错误、契约不匹配和未知状态全部默认拒绝。 ## 真实验收 - [ ] 使用真实 staging 飞书员工、外部邮箱账号和专用离职测试账号。 - [ ] App 管理员在业务 App 内创建成员并分配本地 viewer/member/manager/admin。 - [ ] greenfield 通过受控身份目录开通成员;legacy-migration 通过预绑定命中原成员。 - [ ] 本地角色修改立即改变业务权限,而 Auth 客户项目分工保持不变。 - [ ] Auth 客户项目分工修改会更新上下文,但不会自动扩大本地角色。 - [ ] callback 错误、code 重放、跨 Client、跨 App、伪造 Header 和 Secret 泄漏检查通过。 --- # 故障排查 > 稳定错误码、诊断顺序和常见问题 先按错误发生阶段定位,不要通过关闭 state、PKCE、权限检查或扩大 callback allowlist 来“修复”登录。 ## 1. 快速诊断顺序 1. 确认 Auth API `/health` 可访问。 2. 确认 App 服务端环境变量完整,值来自当前环境。 3. 检查 OAuth client 状态、App 状态和组织开通状态。 4. 对比实际 callback 与登记 redirect URI,逐字符检查。 5. 检查浏览器是否保存 state/verifier cookie。 6. 检查 token exchange 的稳定错误码。 7. 检查 App session introspection 和权限上下文。 不要在日志中打印 code、verifier、client secret 或 session token。 ## 2. 登录跳转错误 ### `sso_client_not_found` / `sso_client_disabled` 检查 `IWISH_AUTH_CLIENT_ID` 是否属于当前环境,client 是否启用。不要复用其他 App 或生产环境 client。 ### `sso_redirect_uri_not_allowed` / `invalid_redirect_uri` 实际 `redirectUri` 必须与登记值完全一致。常见差异:`localhost` 与 `127.0.0.1`、端口、HTTP/HTTPS、路径和尾部斜杠。 ### `sso_organization_not_allowed` 用户不属于目标授权域,或组织没有开通当前 App。由管理员检查组织成员和 App 开通关系。 ## 3. Callback 错误 ### `sso_state_mismatch` 确认 login 和 callback 使用同一域、cookie Path 为 `/`、反向代理没有改写 Host/协议、浏览器没有阻止 cookie。不要跳过 state 校验。 ### `sso_callback_incomplete` 缺少 code 或 PKCE verifier。检查短时 cookie、callback route 和中间代理查询参数。 ### `sso_invalid_grant` Code 已过期、被重复消费、verifier 错误或 redirect URI 不一致。重新从登录入口开始,不要重试同一个 code。 ### `sso_invalid_client` 检查 client secret 是否当前版本。Secret 轮换后旧 secret 和现有 sessions 会失效。 ## 4. Session 与权限错误 ### `missing_app_session` 没有 App cookie/Bearer token,或 cookie 名称不一致。返回登录入口。 ### `sso_session_expired` / `sso_session_revoked` 清理本 App cookie并重新登录。不要循环调用 introspection。 ### `app_session_mismatch` 当前 App 收到了其他 App 的 session。检查 cookie 域、名称和反向代理,不要放宽 `appKey` 校验。 ### `application_owned_authorization` 代码仍在调用已退出的 Auth 中央业务权限 API,例如 `requirePermission` 或 `checkPermission`。SDK 0.3 对这些调用返回 410。应改为 `requireAppAccess`,再检查业务 App 自己的本地成员、角色、permission 和数据范围。App 本地 403 不应触发重复登录。 ## 5. 飞书问题 业务 App 不直接排查飞书 API。平台管理员在 IWish Auth Admin 检查同步状态和历史。 `feishu_directory_field_permission_missing` 表示飞书应用字段权限或通讯录可见范围不足。修改权限后必须发布飞书应用版本,并把通讯录数据权限范围设为全部成员。 ## 6. 客户项目问题 `clientAssignments` 为空不一定是错误。检查:员工是否 active、项目/职责是否 active、生效日期、成员是否来自飞书目录。不要通过给用户更高 App 权限来修复项目分工。 ## 7. Dev 环境诊断 平台仓库本地开发可以运行: ```powershell npx pnpm@10.12.4 doctor:auth-dev npx pnpm@10.12.4 doctor:auth-dev -- --strict ``` 首次创建 dev 管理员时密码可自动生成。完整诊断规则见仓库 `docs/AUTH_DEV_DOCTOR.md`。需要由脚本判断当前下一步时运行 `npx pnpm@10.12.4 next:auth`;该命令不会绕过 `SUPABASE_SERVICE_ROLE_KEY` 或 `E2E_INVITE_EMAIL`。 业务 App 不应复制平台的 service role、数据库连接或飞书 secret。 ### Dev 管理员初始化 平台开发者需要真实管理员会话时,使用仓库引导命令创建或复用 `admin@iwish.local`: ```powershell npx pnpm@10.12.4 bootstrap:dev-admin ``` 该流程依赖本地未提交的 `SUPABASE_SERVICE_ROLE_KEY`,并输出仅用于开发验收的 `SUPABASE_ACCESS_TOKEN`。任何 token 或 service role key 都不得写入文档示例值、源代码或 Git。 ### HTTP E2E 验证 准备真实可收信的测试邮箱 `E2E_INVITE_EMAIL`,启动 Auth API 后运行: ```powershell npx pnpm@10.12.4 e2e:auth-api ``` 该命令验证真实 Supabase Auth token、本地 Worker、邀请流程和管理端 API。完整环境说明见仓库 `docs/ENVIRONMENT_SETUP.md` 与 `docs/E2E_AUTH_API.md`。 --- # AI Agent 接入指南 > 让 AI 按确定契约完成 App 改造 # AI Agent 接入指南 本指南约束使用 AI 改造业务 App 时的执行边界。Agent 必须先读取 `/llms-full.txt`、`/openapi.json`、`/auth-manifest.schema.json` 和目标仓库现有授权实现,不得根据旧版本记忆猜测 API。 ## 执行顺序 1. 识别技术栈、服务端入口、数据库、部署方式和测试命令。 2. 搜索旧登录、密码、session、用户主键、引用用户的业务外键、角色、permission、数据范围和管理员 UI。 3. 选择接入档案:没有需要保留的生产账号和身份数据时使用 `greenfield`;已有生产用户、角色、业务外键或旧 Session 时使用 `legacy-migration`。 4. 把内容分成“统一身份/公共上下文”和“App 本地业务授权”,记录边界。 5. 创建 Manifest 2.0,`authorizationMode=application_owned`,只声明 `requiredContext`。 6. 安装 SDK 0.3,接入服务端 login、callback、logout、PKCE、HttpOnly Cookie 和 `requireAppAccess`。 7. greenfield:建立新数据库和本地成员/角色;管理员从受控 Auth 目录选择身份并原子分配角色。 8. legacy-migration:生成迁移计划 1.0、inventory 和私有 mapping ledger;production 切换前完成存量账号预绑定。 9. 使用 `session.user.id` 命中 `iwish_auth_user_id`;legacy-migration 必须保留原本地用户主键和业务外键。 10. 保留或重建 App 本地角色、permission、数据范围、管理 UI 和本地审计。 11. 仅在业务需要时消费 `employee`、`departments` 和 `clientAssignments`。 12. 在独立 staging 完成对应档案的真实 SSO 和负向验收;只有 legacy-migration 执行旧 Session 失效、回滚和一刀切。 ## 禁止事项 - 不创建 Auth Proxy、长期双登录或第二套内部员工密码。 - 不直接接飞书、Supabase Auth 或 Auth 数据库。 - 不自行解析 opaque App Session,不信任 `x-user-id` 或 `x-role`。 - 不在 Auth Manifest 中创建业务 roles/permissions。 - 不调用已返回 410 的 `requirePermission`、`checkPermission` 或远程业务权限 API。 - 不删除 App 本地角色、字段、记录归属和数据范围授权。 - 不用 Auth User ID 替换项目负责人、创建人、审批人等业务外键。 - 不在运行期按邮箱、姓名或手机号自动关联存量账号。 - 不为未映射存量用户自动创建一个没有历史数据的新账号。 - 不要求管理员手工复制 Auth User ID。 - 不在第一次登录时自动授予 admin/viewer 或创建无角色成员。 - 不为 greenfield 伪造 migration plan、mapping ledger 或历史账号。 - 不用 `clientAssignments` 代替 App 本地 permission 或高风险业务授权。 - 不把 Client Secret、Service Token、Webhook Secret 或 production 数据写入文档和提交。 ## 交付格式 Agent 输出必须包含:接入档案、改动文件、身份边界、本地成员模型、本地角色模型、服务端保护点、上下文消费点、环境变量名、测试结果、已知风险和回滚步骤。只有 legacy-migration 需要业务外键清单、迁移计划、映射计数和冲突清单位置;greenfield 必须明确说明这些工件不适用。Manifest 和适用的迁移计划必须可由当前 Schema/CLI 直接验证。逐用户映射、PII 和 Secret 不得出现在输出正文或 Git。 ```text 身份:IWish Auth App Session 0.3 关联:session.user.id -> app_members.iwish_auth_user_id 授权:App 本地角色 + permission + 数据范围 上下文:employee / departments / clientAssignments(按需) 档案:greenfield 或 legacy-migration 迁移:仅 legacy-migration 执行 staging -> 真实验收 -> production 一刀切 ``` ## 自检问题 1. 所有身份恢复是否来自 IWish Auth session introspection? 2. 浏览器是否能读取任何 Secret 或 opaque session? 3. 无本地成员、停用或无角色是否默认 403? 4. 每个高风险服务端动作是否执行本地 permission 和数据范围检查? 5. assignment 存在但本地权限不足是否仍拒绝? 6. assignment 为空是否不会自动全量授权? 7. 飞书离职后即使本地角色仍存在是否不能访问? 8. 所有 API 和字段是否存在于 OpenAPI、Schema 或 SDK 导出? 9. 原本地用户主键和所有业务外键是否保持不变? 10. production 切换前冲突和待处理账号是否为 0? 11. 未映射用户是否明确失败,而不是创建空账号? 12. greenfield 是否通过受控目录创建成员,而不是手工 User ID 或首次登录自动授权? 13. 当前 App 是否确实需要迁移工件,还是应该使用 greenfield? 任何答案不明确时,Agent 应停止发布并补充测试,不能通过放宽 CORS、callback、Cookie Domain 或错误降级来绕过。 --- # 机器契约 - OpenAPI: /openapi.json - Manifest Schema: /auth-manifest.schema.json - Account Migration Schema: /account-migration.schema.json - SDK examples: examples/nextjs-app, examples/fastapi-app, examples/ai-agent-app