# IWish Auth 完整 AI 接入上下文 生成来源:apps/docs/content/registry.json(17 篇规范文档) 本文档由 scripts/generate-phase-7-docs.mjs 生成,禁止手工编辑。 # 快速开始 > 完成第一个业务 App 的统一登录与权限接入 在 10 分钟内完成一个服务端业务 App 的统一登录、组织上下文和权限检查。开始前,管理员需要先注册 App、同步 Manifest 并创建 confidential OAuth client。 > 前端项目不能独立完成 IWish Auth 接入。授权码交换、client secret 和 App session 必须在服务端处理。 ## 1. 选择技术栈 | 技术栈 | 推荐入口 | 完整示例 | | --- | --- | --- | | Next.js App Router | `@iwish/auth-sdk/next` | `examples/nextjs-app` | | Express / Node.js | `@iwish/auth-sdk/express` | [Express 接入指南](/docs/express) | | FastAPI / Python | `iwish_auth.fastapi` | `examples/fastapi-app` | ## 2. 安装固定版本 JavaScript 制品 ```bash npm install --save-exact https://auth-docs-staging.iwishapp.cn/packages/iwish-auth-sdk-0.2.0.tgz npm install --save-dev --save-exact https://auth-docs-staging.iwishapp.cn/packages/iwish-auth-cli-0.2.0.tgz ``` 从 `/packages/index.json` 和 `/packages/SHA256SUMS` 核对版本与 SHA256。必须提交 lockfile,禁止 `workspace:`、`file:`、`link:` 或 Auth 仓库路径依赖。 ## 3. 准备 App 和 Manifest 在业务 App 根目录创建 `auth.manifest.json`: ```json { "$schema": "https://auth-docs-staging.iwishapp.cn/auth-manifest.schema.json", "schemaVersion": "1.0", "appKey": "reporting", "name": "数据报表", "environment": "dev", "manifestVersion": "2026.07.20-1", "permissions": [ { "key": "reporting.access", "name": "访问数据报表" }, { "key": "reporting.report.read", "name": "查看报表" } ], "roles": [ { "key": "reporting.viewer", "name": "报表查看者", "permissions": ["reporting.access", "reporting.report.read"] } ] } ``` 本地校验: ```powershell npx iwish-auth manifest validate ./auth.manifest.json --environment dev ``` 管理员确认预检差异后同步: ```powershell $env:IWISH_AUTH_API_URL='http://127.0.0.1:8788' $env:IWISH_AUTH_ADMIN_ORGANIZATION_ID='' $env:IWISH_AUTH_ADMIN_TOKEN='' npx iwish-auth manifest sync ./auth.manifest.json --yes ``` ## 3. 配置服务端环境变量 OAuth client 由 IWish Auth 管理员创建。`clientSecret` 只显示一次,必须写入业务 App 的服务端 secret 管理,不得使用 `NEXT_PUBLIC_*` 或提交到 Git。 ```dotenv IWISH_AUTH_API_URL=http://127.0.0.1:8788 IWISH_AUTH_PORTAL_URL=http://localhost:3035 IWISH_AUTH_APP_KEY=reporting IWISH_AUTH_CLIENT_ID=iwish_replace_with_real_client IWISH_AUTH_CLIENT_SECRET=iwish_secret_replace_with_real_secret IWISH_AUTH_REDIRECT_URI=http://localhost:3000/auth/callback ``` `IWISH_AUTH_REDIRECT_URI` 必须与 OAuth client 中登记的 callback 完全一致,包括协议、端口、路径和尾部斜杠。 ## 4. 安装 SDK Next.js 或 Express: ```powershell pnpm add @iwish/auth-sdk ``` FastAPI: ```powershell pip install "iwish-auth[fastapi]" ``` ## 5. 接入登录和权限 Next.js 服务端配置: ```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: process.env.IWISH_AUTH_APP_KEY!, clientId: process.env.IWISH_AUTH_CLIENT_ID!, clientSecret: process.env.IWISH_AUTH_CLIENT_SECRET!, redirectUri: process.env.IWISH_AUTH_REDIRECT_URI! }); ``` 保护业务 API: ```ts export async function GET(request: Request) { const session = await auth.requirePermission(request, "reporting.report.read"); return Response.json({ user: session.user, organization: session.organization, clientAssignments: session.clientAssignments }); } ``` ## 6. 完成验收 必须验证以下路径: 1. 未登录访问受保护接口返回 401。 2. 登录后 callback 能建立 HttpOnly App session。 3. 未分配 App 角色时返回 403。 4. 分配 `reporting.viewer` 后可以读取报表。 5. 使用其他 App 的 session 访问当前 App 被拒绝。 6. 登出后原 session 不再可用。 7. `clientAssignments` 缺失或为空时,业务 App 仍按权限正常工作。 下一步阅读对应的[框架指南](/docs/nextjs)和[接入 Checklist](/docs/integration-checklist)。 --- # 接入流程概览 > 管理员、开发者和业务 App 的完整协作流程 IWish Auth 接入分为平台配置、业务 App 开发、授权和上线验收四部分。不要让业务 App 开发者自行创建飞书应用或复制 Supabase 配置。 ## 1. 角色分工 | 角色 | 负责内容 | | --- | --- | | IWish Auth 平台管理员 | 注册 App、创建 OAuth client、开通组织、审核 Manifest、分配角色 | | 业务 App 开发者 | 接入 SDK、替换本地登录与角色判断、保留业务数据授权 | | 业务负责人 | 确认权限颗粒度、角色模板和迁移窗口 | | 安全/运维 | 管理 secrets、callback、生产发布和审计证据 | ## 2. 标准接入顺序 1. 为 App 确定唯一、稳定、全小写的 `appKey`。 2. 盘点现有角色和保护动作,形成权限映射表。 3. 编写并本地校验 `auth.manifest.json`。 4. 管理员在 IWish Auth 注册 App、同步 Manifest。 5. 管理员创建 confidential OAuth client 和精确 callback。 6. App 服务端接入 SDK 登录、callback 和 App session。 7. 使用 `requirePermission` 替换本地角色判断。 8. 按需读取 `clientAssignments`,但保留业务数据授权。 9. 删除本地密码、登录入口和角色分配写入口。 10. 在 dev/staging 完成安全、回归和回滚演练后直接切换。 ## 3. 登录数据流 ```text 业务 App -> IWish Auth Portal -> 飞书或邮箱登录 <- authorization code + state 业务 App 服务端 -> /v1/sso/token -> opaque App session 业务请求 -> /v1/sso/session -> App scoped context ``` 业务 App 浏览器只保存自身 HttpOnly session cookie。它不接触飞书 token、Supabase refresh token、OAuth client secret 或 Auth 数据库连接。 ## 4. 权限数据流 ```text auth.manifest.json -> Manifest CLI 预检/同步 -> IWish Auth permissions + roles -> 管理员按组织给用户分配 App 角色 -> App session 返回当前 App permissions -> 业务路由 requirePermission ``` 权限由 IWish Auth 统一定义和分配,业务 App 只执行权限结果。业务记录归属、字段可见性和客户数据隔离继续由业务 App 自己处理。 ## 5. 客户项目数据流 飞书同步提供公司部门和员工目录;IWish Auth 管理员独立创建客户项目,再把 active 飞书员工以多个项目职责绑定到项目。业务 App 从 session 的 `clientAssignments` 获取当前员工的有效项目分工。 外部客户账号不是创建客户项目的前置条件。一个客户项目可以在没有任何外部账号的情况下存在和分配内部服务团队。 ## 6. 禁止的接入方式 - 业务 App 直接调用飞书通讯录或飞书 OAuth。 - 从浏览器直接调用 `/v1/sso/token`。 - 把 `clientSecret` 放入前端环境变量。 - 信任 `x-user-id`、`x-role` 等可伪造身份 Header。 - 继续在业务 App 创建密码或分配统一角色。 - 用 `clientAssignments` 代替 `requirePermission`。 - 让多个 App 共用同一个 OAuth client 或 session cookie。 --- # Next.js > 使用 App Router 和 TypeScript SDK 接入 Next.js App Router 使用 `@iwish/auth-sdk/next`。Adapter 负责 state、PKCE、短时 cookie、授权码交换、App session cookie 和稳定错误类型。 ## 1. 创建服务端 Auth 实例 ```ts // app/lib/auth.ts import { createIWishAuth } from "@iwish/auth-sdk/next"; export function getAuth() { return createIWishAuth({ apiUrl: process.env.IWISH_AUTH_API_URL!, portalUrl: process.env.IWISH_AUTH_PORTAL_URL!, appKey: process.env.IWISH_AUTH_APP_KEY!, clientId: process.env.IWISH_AUTH_CLIENT_ID!, clientSecret: process.env.IWISH_AUTH_CLIENT_SECRET!, redirectUri: process.env.IWISH_AUTH_REDIRECT_URI!, secureCookies: process.env.NODE_ENV === "production" }); } ``` 该模块只能被 Server Component、Route Handler 或 Server Action 导入。不要添加 `"use client"`。 ## 2. 登录 Route Handler ```ts // app/auth/login/route.ts import { getAuth } from "../../lib/auth"; export async function GET(request: Request) { const returnTo = new URL(request.url).searchParams.get("returnTo") ?? "/"; return getAuth().beginLogin(request, { returnTo }); } ``` `returnTo` 只允许当前 App 同源路径。SDK 会把外部 URL 归一化为 `/`。 ## 3. Callback Route Handler ```ts // app/auth/callback/route.ts import { getAuth } from "../../lib/auth"; export async function GET(request: Request) { return getAuth().handleCallback(request); } ``` Callback 会校验 state,使用 PKCE verifier 和 confidential client secret 交换一次性 code,并设置 HttpOnly App session cookie。 ## 4. 登出 ```ts // app/auth/logout/route.ts import { getAuth } from "../../lib/auth"; export async function POST(request: Request) { return getAuth().logout(request, { redirectTo: "/" }); } ``` SDK 会尽力撤销服务端 session;即使 Auth API 暂时不可达,也会删除本 App cookie。 ## 5. 读取 Session ```ts // app/api/session/route.ts import { IWishAuthError } from "@iwish/auth-sdk"; import { getAuth } from "../../lib/auth"; export async function GET(request: Request) { try { return Response.json(await getAuth().requireAppAccess(request)); } catch (error) { if (error instanceof IWishAuthError) { return Response.json({ error: error.code }, { status: error.status }); } throw error; } } ``` 不要只调用 `getSession()` 后信任本地 cookie。`requireAppAccess()` 会通过 Auth API 解析 opaque session 并确认该 session 属于当前 App。 ## 6. 权限保护 ```ts export async function GET(request: Request) { const session = await getAuth().requirePermission(request, "reporting.report.read"); const visibleProjectIds = session.clientAssignments.map((item) => item.clientId); return Response.json(await loadReports({ userId: session.user.id, visibleProjectIds })); } ``` `requirePermission` 同时验证 session、App 和权限命名空间。跨 App 权限 key 即使意外出现在上下文中也会被拒绝。 ## 7. Server Component 边界 Route Handler 接收标准 `Request`,可以直接使用 Adapter。Server Component 没有原始 `Request` 时,建议通过受保护的内部 Route Handler 获取 session,或使用 Next.js `headers()` 构造只读 Request;不要把 opaque session token传给 Client Component。 ## 8. 测试清单 - `beginLogin` 返回 302,并设置 state/verifier cookie。 - callback state 不匹配返回 401。 - callback 后 cookie 包含 `HttpOnly`、`SameSite=Lax`,生产环境包含 `Secure`。 - `requireAppAccess` 拒绝其他 `appKey` 的 session。 - `requirePermission` 拒绝缺少权限和跨 App 权限。 - logout 删除 cookie,原 session 再解析失败。 完整代码见仓库的 `examples/nextjs-app`。 --- # Express > 使用统一中间件保护 Node.js API Express 使用 `@iwish/auth-sdk/express` 验证 App session。Adapter 只信任 IWish Auth 的 session introspection,不读取 `x-iwish-user-id`、`x-role` 或其他调用方可伪造 Header。 ## 1. 创建中间件 ```ts import { createIWishExpressAuth } from "@iwish/auth-sdk/express"; export const iwishAuth = createIWishExpressAuth({ apiUrl: process.env.IWISH_AUTH_API_URL!, appKey: process.env.IWISH_AUTH_APP_KEY!, sessionCookieName: "iwish_app_session", sessionCacheTtlMs: 5_000 }); ``` 登录和 callback 可以使用 TypeScript SDK 的 `buildAuthorizeUrl`、`createPkcePair`、`createSsoState` 和 `exchangeAuthorizationCode`,也可以由现有服务端 OAuth controller 封装。 ## 2. 中间件顺序 ```ts app.get( "/api/reports", iwishAuth.requireAuth(), iwishAuth.requireAppAccess(), iwishAuth.requirePermission("reporting.report.read"), async (request, response) => { const context = request.iwishAuth!.context; response.json({ reports: await loadReports(context.user.id), clientAssignments: context.clientAssignments }); } ); ``` 顺序不可调整: 1. `requireAuth()` 从 Bearer token 或 App session cookie 读取 opaque token,并调用 `/v1/sso/session`。 2. `requireAppAccess()` 确认 session 的 `app.appKey` 与当前服务配置一致。 3. `requirePermission()` 检查当前 App 命名空间内的权限。 ## 3. 类型扩展 SDK 提供 `ExpressRequestLike`。真实 Express 项目可以使用 declaration merging 把 `iwishAuth` 加到 `Express.Request`: ```ts import type { AppSessionContext } from "@iwish/auth-sdk"; declare global { namespace Express { interface Request { iwishAuth?: { context: AppSessionContext; token: string }; } } } ``` ## 4. Cookie 与反向代理 - App session cookie 必须设置 `HttpOnly`、`SameSite=Lax`、`Path=/`。 - HTTPS 环境必须设置 `Secure`。 - 反向代理需要保留 `Cookie` 或 `Authorization`,但必须移除外部传入的内部身份 Header。 - 每个 App 使用独立 cookie 名称或独立域,避免 session 串用。 ## 5. 错误处理 Adapter 默认返回 `{ "error": "" }`。业务 App 可以在最外层统一记录 request id,但不要把 token、client secret 或 Auth API 响应详情写入日志。 ## 6. 验收重点 - 伪造身份 Header 不改变 `request.iwishAuth.context.user`。 - 缺少 token 返回 401。 - 其他 App session 返回 403。 - 缺少权限或跨 App 权限返回 403。 - 角色撤销后,短 TTL 到期或强制刷新能立即反映权限变化。 --- # FastAPI > 使用 Python SDK 和依赖注入接入 FastAPI 使用 `iwish_auth.fastapi.IWishFastAPIAuth`。Adapter 提供登录、callback、登出和依赖注入,并将 SDK 稳定错误转换为 HTTP 状态。 ## 1. 配置 Adapter ```python import os from iwish_auth.fastapi import IWishFastAPIAuth, IWishFastAPIAuthOptions auth = IWishFastAPIAuth(IWishFastAPIAuthOptions( api_url=os.environ["IWISH_AUTH_API_URL"], portal_url=os.environ["IWISH_AUTH_PORTAL_URL"], app_key=os.environ["IWISH_AUTH_APP_KEY"], client_id=os.environ["IWISH_AUTH_CLIENT_ID"], client_secret=os.environ["IWISH_AUTH_CLIENT_SECRET"], redirect_uri=os.environ["IWISH_AUTH_REDIRECT_URI"], )) ``` `client_secret` 只能从服务端环境或 secret manager 读取。 ## 2. 登录、Callback 和登出 ```python from fastapi import FastAPI, Request app = FastAPI() @app.get("/auth/login") async def login(request: Request, return_to: str = "/"): return await auth.login(request, return_to=return_to) @app.get("/auth/callback") async def callback(request: Request): return await auth.callback(request) @app.post("/auth/logout") async def logout(request: Request): return await auth.logout(request) ``` ## 3. 保护接口 ```python from fastapi import Depends @app.get("/api/reports") async def reports( session=Depends(auth.require_permission("reporting.report.read")), ): return { "user": session["user"], "organization": session["organization"], "clientAssignments": session.get("clientAssignments", []), } ``` 可用依赖: - `auth.require_auth`:要求有效 App session。 - `auth.require_app_access`:同时确认 session 属于当前 App。 - `auth.require_organization`:返回当前授权域。 - `auth.require_permission(permission)`:检查 App scoped permission。 - `auth.get_client_assignments`:返回当前有效项目分工。 ## 4. 直接使用 Python Client 后台任务或非 FastAPI 服务可以直接使用异步 Client: ```python from iwish_auth import IWishAuthClient async with IWishAuthClient( api_url=IWISH_AUTH_API_URL, token=app_session_token, ) as client: session = await client.get_app_session() decision = await client.check_permission("reporting.report.read") ``` Client 使用短 TTL session 缓存,且缓存有效期不会超过 Auth session 的 `expiresAt`。 ## 5. 错误映射 | SDK 错误 | HTTP | 业务处理 | | --- | --- | --- | | `IWishAuthUnauthorizedError` | 401 | 清理本 App cookie 并重新登录 | | `IWishAuthForbiddenError` | 403 | 展示无权限,不循环登录 | | `IWishAuthError` | 原始状态 | 记录稳定错误码和 request id | ## 6. 测试 FastAPI `TestClient` 或 `httpx.AsyncClient` 测试至少覆盖:登录 302、callback state、session cookie、允许权限、缺少权限、跨 App 权限和登出。完整实现见 `examples/fastapi-app`。 --- # 跨 App SSO > 授权码、PKCE、App session 和登出边界 IWish Auth 使用 Portal 作为统一登录入口,业务 App 使用 OAuth 风格授权码和不透明 App session。当前协议不是公开标准 OIDC issuer,业务 App 应通过 SDK 接入,避免依赖 Supabase token 细节。 ## 1. 授权流程 ```text App /auth/login -> 生成随机 state 和 PKCE verifier/challenge -> 将 state/verifier 写入 10 分钟 HttpOnly cookie -> 跳转 Portal authorize URL Portal -> 恢复统一登录或要求飞书/邮箱登录 -> 用户选择授权组织 -> POST /v1/sso/authorize App /auth/callback -> 校验 state -> POST /v1/sso/token 交换一次性 code -> 设置本 App HttpOnly session cookie 业务请求 -> GET /v1/sso/session 解析上下文 ``` ## 2. Authorization Code - 有效期 60 秒。 - 只能消费一次。 - 与 `clientId`、精确 `redirectUri` 和 PKCE challenge 绑定。 - 数据库使用原子 RPC 消费,避免并发重放。 业务 App 不得记录 code,也不得在浏览器调用 token endpoint。 ## 3. PKCE 和 State 只允许 `S256`。`codeVerifier` 和 `state` 必须是密码学安全随机值,保存在短时 HttpOnly、SameSite=Lax cookie。Callback 必须先校验 state,再交换 code。 缺少 state、state 不匹配、verifier 缺失都必须终止登录,不允许降级继续。 ## 4. App Session `POST /v1/sso/token` 返回 opaque `accessToken`。它不是 JWT,业务 App 不应自行解析。每个受保护请求通过 SDK 调用 `GET /v1/sso/session` 获取: - `session`:ID 和过期时间。 - `user`:统一身份。 - `employee`:飞书员工上下文或 `null`。 - `organization`:本次授权域。 - `app`:当前 App。 - `roles`、`permissions`:当前 App scoped 权限。 - `clientAssignments`:当前有效客户项目分工。 ## 5. 立即失效条件 以下状态变化会使后续 session introspection 失败或不再包含旧权限: - 用户、组织成员、组织或 App 被停用。 - 飞书员工离职、冻结或不再有效。 - App 角色被撤销。 - OAuth client 被停用或轮换 secret。 - 用户执行全局登出。 - App session 被局部撤销或过期。 业务 App 可以使用最多 5 秒短缓存降低重复 introspection,但不得无限缓存身份和权限。 ## 6. 登出模型 - App 局部登出:`POST /v1/sso/logout`,删除本 App cookie。 - Portal 全局登出:`POST /v1/sso/logout-all`,撤销全部 App sessions,再退出统一登录。 - 单个 App 不得直接清理其他 App cookie。 ## 7. 稳定错误码 `sso_client_not_found`、`sso_client_disabled`、`sso_redirect_uri_not_allowed`、`sso_organization_not_allowed`、`sso_app_not_allowed`、`sso_invalid_client`、`sso_invalid_grant`、`sso_session_invalid`、`sso_session_expired`、`sso_session_revoked`。 401 表示登录/session 需要恢复;403 表示用户已登录但没有当前授权,不应循环重试登录。 --- # 飞书身份边界 > 内部员工身份、组织同步和业务 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` 不是权限,仍需检查 `permissions`。 ## 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` 不是权限凭证,不能替代 permission 或业务数据授权。下面的判断是错误的: ```ts // 错误:绑定项目不代表拥有删除权限 if (session.clientAssignments.some((item) => item.clientId === clientId)) { await deleteReport(reportId); } ``` 正确顺序: ```ts const session = await auth.requirePermission(request, "reporting.report.delete"); const assignment = session.clientAssignments.find((item) => item.clientId === clientId); await assertBusinessDataAccess({ user: session.user, assignment, clientId, reportId }); await deleteReport(reportId); ``` 第一层是 IWish Auth App 权限,第二层是业务 App 自己的数据授权。两层都必须通过。 ## 5. 空数据和外部账号 业务 App 必须把空数组视为正常状态: - 某些内部岗位不参与任何客户项目。 - 新员工尚未分配项目。 - 外部邮箱账号的项目可见范围由业务 App 自身模型决定,IWish Auth 不会把外部账号自动变成内部项目团队成员。 不要因为 `clientAssignments` 为空就把用户当成未登录,也不要自动授予全部项目。 ## 6. 缓存与变更 项目团队会动态调整。优先使用当前 App session 返回值;长任务应在执行关键写操作前重新解析 session。不要永久存储 `clientAssignments` 快照作为授权依据。 --- # Manifest 规范 > 声明 App、权限和角色的稳定契约 `auth.manifest.json` 是 App 权限和角色的唯一声明契约。它属于业务 App 代码仓库,必须和功能代码一起评审、测试和版本化。 ## 1. 完整示例 ```json { "$schema": "https://auth-docs-staging.iwishapp.cn/auth-manifest.schema.json", "schemaVersion": "1.0", "appKey": "crm", "name": "CRM 系统", "description": "销售客户与联系人管理", "environment": "dev", "manifestVersion": "2026.07.20-1", "permissions": [ { "key": "crm.access", "name": "访问 CRM" }, { "key": "crm.contact.read", "name": "查看联系人" }, { "key": "crm.contact.write", "name": "编辑联系人" } ], "roles": [ { "key": "crm.viewer", "name": "CRM 查看者", "permissions": ["crm.access", "crm.contact.read"] }, { "key": "crm.admin", "name": "CRM 管理员", "permissions": ["crm.access", "crm.contact.read", "crm.contact.write"] } ] } ``` ## 2. 顶层字段 | 字段 | 规则 | | --- | --- | | `$schema` | 指向 IWish Auth 发布的 Manifest JSON Schema | | `schemaVersion` | Manifest 契约版本,当前为 `1.0` | | `appKey` | 全局唯一、全小写,发布后不可随意变更 | | `name` | 中文产品名称,用于 Admin、Portal 和审计 | | `description` | 说明 App 业务边界,不写营销文案 | | `environment` | `dev`、`staging` 或 `prod` | | `manifestVersion` | 每次同步唯一,推荐日期加递增序号 | | `permissions` | 当前版本声明的权限点 | | `roles` | App 提供的角色模板 | ## 3. 权限字段 权限 key 使用 `{app}.{resource_path}.{action}`,每个 App 必须包含 `{app}.access`。`resource_path` 至少包含一个稳定资源段,必要时可以使用多段层级资源,例如 `auth.apps.sso.manage`;Auth 将最后一段解析为 `action`,中间所有段合并为 `resource`。这些派生值不在权限对象中重复声明;`name` 是管理后台展示的中文名称。 不要把部门、客户 ID、用户 ID 或环境写入 permission key。 ## 4. 角色字段 角色 key 必须位于当前 App 命名空间,例如 `crm.viewer`。角色只引用同一 Manifest 中存在的 permission key,不能引用其他 App 权限,也不能重复引用。 角色是权限集合模板,不是业务数据范围。不要创建 `client_acme_viewer`、`east_china_sales` 等带数据范围的角色。 ## 5. 本地校验 ```powershell npx iwish-auth manifest validate ./auth.manifest.json npx iwish-auth manifest validate ./auth.manifest.json --environment staging --json ``` 校验会检查 JSON、Schema、重复权限/角色、命名空间、角色引用和环境。 ## 6. 远程预检和同步 先执行不带 `--yes` 的远程预检: ```powershell npx iwish-auth manifest sync ./auth.manifest.json ``` 确认 diff 和风险后同步: ```powershell npx iwish-auth manifest sync ./auth.manifest.json --yes ``` CLI 只从 `IWISH_AUTH_ADMIN_TOKEN` 或 `--token-env` 指定的环境变量读取 token,不支持命令行明文 token 参数。 Manifest 写入必须走 Admin API。CLI 的远程预检调用 `POST /v1/admin/apps/{appKey}/manifest/validate`,确认同步调用 `POST /v1/admin/apps/{appKey}/manifest/sync`;调用者必须具有 `auth.manifest.write`。旧公开写入端点只返回迁移错误,不允许业务 App 绕过治理流程直接写入。 ## 7. 删除和废弃 从 Manifest 移除权限或角色是高风险变更。同步只会给出风险提示,不会自动删除或停用数据库中的既有权限和授权关系。 正确流程: 1. 新权限与旧权限并行发布。 2. 更新业务代码和角色模板。 3. 迁移已有授权。 4. 观察一个完整迁移周期。 5. 标记旧权限 deprecated。 6. 确认无引用后由平台管理员处理停用。 ## 8. 生产变更要求 - Manifest diff 必须进入代码审查。 - `manifestVersion` 不得覆盖已同步版本。 - prod 同步需要平台管理员或 App owner 审核。 - 破坏性变更必须提供影响清单和回滚 Manifest。 - 同步操作会写入审计日志。 --- # 权限命名规范 > 统一权限 key、粒度和兼容策略 统一命名让 Auth、业务 App、管理员和 AI Agent 对同一个权限含义保持一致。权限 key 是稳定 API,不是临时 UI 文案。 ## 1. 标准格式 ```text {app}.{resource_path}.{action} ``` 示例: ```text crm.access crm.contact.read crm.contact.write reporting.dashboard.export training.course.publish auth.apps.sso.manage ``` `{app}.access` 是特例,也是每个 App 的必需权限。 ## 2. App 段 - 使用 Manifest 的 `appKey`。 - 全小写,推荐字母、数字和下划线的稳定短名称。 - 不包含环境、域名、部门或公司名称。 - 发布后变更 `appKey` 等同新 App 迁移。 ## 3. Resource 段 Resource path 表示业务对象或能力边界,至少包含一个资源段。普通业务权限优先使用单段稳定名词: ```text contact report campaign course requirement ``` 只有确实存在稳定层级时才使用多段资源,例如 `auth.apps.sso.manage` 的资源是 `apps.sso`、动作是 `manage`。Auth 始终把最后一段解析为 action,把 app 与 action 之间的所有段解析为 resource。不要为了页面分组随意增加层级。 避免 `page1`、`module_new`、`misc`、`button` 等 UI 或临时实现名称。 ## 4. Action 段 优先复用标准动作: | 动作 | 含义 | | --- | --- | | `access` | 进入 App | | `read` | 查看资源 | | `create` | 创建资源 | | `write` / `update` | 编辑资源 | | `delete` | 永久删除资源 | | `disable` / `restore` | 生命周期停用和恢复 | | `approve` | 审批 | | `export` | 导出 | | `publish` | 发布 | | `assign` | 分配关系 | 同一个 App 内不要同时使用 `view`、`read`、`get` 表示相同动作。 ## 5. 粒度原则 一个权限应该对应可独立审计和授权的业务动作。不要为每个按钮创建权限,也不要用一个 `admin` 权限覆盖所有高风险操作。 推荐: ```text crm.contact.read crm.contact.write crm.contact.export ``` 不推荐: ```text crm.button_17.click crm.all.manage crm.client_123.read ``` ## 6. 角色命名 角色 key 使用 `{app}.{role}`: ```text crm.viewer crm.operator crm.admin ``` 角色名称用中文明确职责。角色只聚合权限,不编码客户项目、部门、地区或单个用户。 ## 7. 兼容性 - 权限显示名称和说明可以更新,不视为破坏性变更。 - permission key 的含义不得静默改变。 - 拆分权限时先新增,再迁移,最后废弃旧 key。 - 合并权限也必须保留旧 key 至少一个迁移周期。 - 业务 App 遇到未知权限应忽略,不应报错或自动授权。 ## 8. 审查问题 提交 Manifest 前回答: 1. 这个动作是否需要独立授权和审计? 2. Resource path 是否是稳定业务对象层级,而不是页面或组件? 3. Action 是否复用了 App 内已有词汇? 4. 是否错误地把客户/部门数据范围编码进 key? 5. 是否包含 `{app}.access`? 6. 角色是否只引用当前 App 权限? --- # SDK API Reference > TypeScript 和 Python SDK 的稳定接口 SDK 是 IWish Auth HTTP/SSO 协议的稳定封装。业务 App 应优先使用 Adapter;只有非框架服务或后台任务才直接使用 Client。 当前 JavaScript SDK 固定版本为 `0.2.0`,从文档站 `/packages/iwish-auth-sdk-0.2.0.tgz` 安装;制品元数据和 SHA256 见 `/packages/index.json`。 ## 1. TypeScript Client ```ts import { IWishAuthClient } from "@iwish/auth-sdk"; ``` ### `new IWishAuthClient(options)` | 选项 | 类型 | 说明 | | --- | --- | --- | | `apiUrl` | `string` | Auth API 基础地址 | | `token` | `string?` | opaque App session 或受支持 Bearer token | | `fetcher` | `typeof fetch?` | 测试或运行时自定义 fetch | | `sessionCacheTtlMs` | `number?` | 默认 5000,设置 0 可关闭缓存 | ### Client 方法 - `getCurrentUser()`:使用统一登录 token 读取 `/v1/me`。 - `exchangeAuthorizationCode(request)`:服务端交换一次性 SSO code。 - `getAppSession({ forceRefresh? })`:解析 opaque App session。 - `checkPermission(permission, options?)`:当前 App 命名空间内本地判断。 - `checkPermissionsBatch(permissions, options?)`:一次 session 解析批量判断。 - `getClientAssignments(options?)`:返回当前有效项目分工。 - `revokeAppSession()`:撤销当前 App session。 - `clearSessionCache()`:清理 Client 短缓存。 - `withToken(token)`:保留配置并创建带 token 的 Client。 `getBoundClients()` 是兼容别名,已 deprecated;新代码使用 `getClientAssignments()`。 ## 2. Next.js Adapter ```ts import { createIWishAuth } from "@iwish/auth-sdk/next"; ``` 返回方法: - `beginLogin(request, { organizationId?, returnTo? })` - `handleCallback(request)` - `logout(request, { redirectTo? })` - `getSession(request, forceRefresh?)` - `requireAuth(request)` - `requireOrganization(request)` - `requireAppAccess(request)` - `requirePermission(request, permission)` - `getClientAssignments(request)` ## 3. Express Adapter ```ts import { createIWishExpressAuth } from "@iwish/auth-sdk/express"; ``` 返回 Express-like middleware factory:`requireAuth()`、`requireAppAccess()`、`requireOrganization()`、`requirePermission(permission)`。`requireAuth` 成功后把 `{ context, token }` 写入 `request.iwishAuth`。 ## 4. TypeScript 错误 - `IWishAuthError(status, code, details?)` - `IWishAuthUnauthorizedError`:固定 HTTP 401。 - `IWishAuthForbiddenError`:固定 HTTP 403。 业务日志只记录 `status`、`code` 和自身 request id,不记录 token 或 `details` 中可能存在的敏感值。 ## 5. Python Client ```python from iwish_auth import IWishAuthClient ``` 主要方法与 TypeScript 一致,使用 snake_case: - `get_current_user()` - `exchange_authorization_code(...)` - `get_app_session(force_refresh=False)` - `check_permission(permission, force_refresh=False)` - `check_permissions_batch(permissions, force_refresh=False)` - `get_client_assignments(force_refresh=False)` - `revoke_app_session()` - `clear_session_cache()` - `with_token(token)` - `aclose()` 推荐使用 `async with IWishAuthClient(...) as client` 管理 `httpx.AsyncClient` 生命周期。 ## 6. FastAPI Adapter `IWishFastAPIAuthOptions` 配置 API、Portal、App、client 和 callback。`IWishFastAPIAuth` 提供 `login`、`callback`、`logout`、`get_session`、`require_auth`、`require_app_access`、`require_organization`、`get_client_assignments` 和 `require_permission`。 ## 7. 上下文稳定字段 App session 的稳定顶层字段为: ```text session user employee clientAssignments organization app roles permissions ``` ## 8. Service Token 与 Webhook - TypeScript `checkUserPermission(request)`:使用服务端 `iwa_` Token 调用远程权限检查。 - Python `check_user_permission(...)`:使用服务端 Token 调用远程权限检查。 - TypeScript `verifyIWishWebhook(...)`:校验时间戳、HMAC-SHA256 签名、事件 ID 和 Envelope。 - Python `verify_iwish_webhook(...)`:与 TypeScript 验签规则一致。 完整协议见 [Service Token 与 Webhook](/docs/webhooks-service-tokens)。 新字段可能向后兼容地增加。业务 App 不得对未知字段报错,也不得假设可空字段始终存在。 --- # Service Token 与 Webhook > 机器身份、签名验签、幂等投递和缓存失效 # Service Token 与 Webhook 业务 App 的服务端任务使用 Service Token 调用 IWish Auth;权限或身份上下文发生变化时,IWish Auth 通过 Webhook 通知业务 App 清理缓存。两者都只允许在服务端使用。 ## 1. Service Token 管理员在“系统集成”中先创建 Service Account,再签发 `iwa_` 开头的 Token。 - Token 明文只显示一次,必须立即写入业务 App 的服务端 Secret 管理。 - 数据库只保存带服务端 pepper 的 HMAC-SHA256 hash。 - Service Account 可以绑定 App、组织、scope 和到期时间。 - Token scope 必须是 Service Account scope 的子集。 - 停用 Service Account 或吊销 Token 后立即失效,不依赖员工会话。 - 禁止把 Token 写入浏览器、`NEXT_PUBLIC_*`、日志、Git 或 Manifest。 服务端权限检查: ```ts import { IWishAuthClient } from "@iwish/auth-sdk"; const auth = new IWishAuthClient({ apiUrl: process.env.IWISH_AUTH_API_URL!, token: process.env.IWISH_AUTH_SERVICE_TOKEN! }); const decision = await auth.checkUserPermission({ userId, organizationId, appKey: "crm", permission: "crm.contact.read" }); ``` Python 使用 `check_user_permission(...)`,字段语义相同。 ## 2. Webhook 请求 创建或轮换 endpoint 时,管理后台只显示一次 `whsec_` 开头的签名密钥;接收方必须立即保存到服务端 Secret 管理中。 请求头: ```text IWish-Event-Id: IWish-Event-Type: permission.changed IWish-Delivery-Id: IWish-Delivery-Attempt: 1 IWish-Timestamp: IWish-Signature: v1= ``` 签名原文必须使用未解析、未重新序列化的原始请求体: ```text .. ``` TypeScript 验签: ```ts import { verifyIWishWebhook } from "@iwish/auth-sdk/webhook"; const rawBody = await request.text(); const event = await verifyIWishWebhook({ secret: process.env.IWISH_AUTH_WEBHOOK_SECRET!, rawBody, headers: request.headers }); ``` Python 验签: ```python from iwish_auth import verify_iwish_webhook event = verify_iwish_webhook( secret=webhook_secret, raw_body=await request.body(), headers=request.headers, ) ``` SDK 默认拒绝超过 300 秒的时间戳、签名不匹配、事件 ID 不一致和无效 Envelope。 ## 3. 幂等与缓存失效 接收方必须持久化 `IWish-Event-Id`,重复事件直接返回 2xx。不要按 Delivery ID 幂等,因为同一个 Event 可能重试多次。 Envelope 的 `cacheInvalidation` 包含: - `userIds` - `organizationIds` - `appKeys` - `clientProjectIds` 业务 App 收到 `user.disabled`、`user.role_changed`、`permission.changed`、`organization.app_disabled`、`employee.disabled` 或客户项目团队变更时,应清理与上述维度相交的权限和上下文缓存。业务 App 是否消费客户项目数据仍由自身业务决定。 ## 4. 投递策略 投递状态依次使用 `pending`、`processing`、`delivered`、`failed` 和 `dead_letter`,便于业务 App 和运维工具统一识别。 - 成功:HTTP 2xx。 - 自动重试:1 分钟、5 分钟、30 分钟、2 小时,最多 5 次。 - HTTP 410:立即进入死信并停用 endpoint。 - 其他失败:耗尽次数后进入死信,可在管理后台人工重试。 - 接收方应在 10 秒内返回;耗时业务先入本地队列再返回 2xx。 ## 5. 安全边界 - 生产 endpoint 只接受公网 HTTPS URL。 - 密钥轮换后旧版本立即失效。 - 不记录 Authorization、Token、签名密钥或完整敏感 payload。 - CORS 使用精确 allowlist;基于 Cookie 的状态变更请求不属于 IWish Auth API 协议。 - 受保护写接口使用 Cloudflare Rate Limiting binding。 --- # 迁移指南 > 从独立登录与权限体系一刀切迁移 IWish Auth 采用一刀切迁移,不提供 Auth Proxy 或长期双登录。迁移需要先在 staging 完整验收,再在单个 App 的维护窗口直接切换。 ## 1. 迁移前盘点 建立清单: - 本地用户表、密码字段、session 表和登录入口。 - 本地角色、权限节点、路由守卫和前端可见性判断。 - 用户与员工、部门、客户或项目的映射。 - 服务账号、定时任务和 API token。 - 登录相关邮件、找回密码、邀请和管理员页面。 - 所有 callback、域名、反向代理和 cookie 设置。 ## 2. 权限映射 不要直接把旧角色名当成新 permission。先把每个受保护业务动作映射为 `{app}.{resource}.{action}`,再把权限组合成 App 角色。 | 旧判断 | 新权限 | 备注 | | --- | --- | --- | | `role === "sales"` 可看联系人 | `crm.contact.read` | 角色成为 `crm.sales` 模板 | | `isAdmin` 可导出 | `crm.contact.export` | 高风险动作独立授权 | | 负责 Acme 项目 | 不做 permission | 使用 `clientAssignments` + 业务数据授权 | ## 3. 建立接入配置 1. 确定 `appKey` 和 Manifest。 2. 在 dev 同步 Manifest。 3. 创建 dev OAuth client 和精确 callback。 4. 给试点组织开通 App。 5. 给试点用户分配角色。 ## 4. 替换登录 - 新增 `/auth/login`、`/auth/callback`、`/auth/logout`。 - 使用 SDK 创建 App session。 - 把全部受保护服务端路由迁移到 `requireAuth` / `requireAppAccess`。 - 删除或禁用本地登录、注册、找回密码和管理员重置密码入口。 - 删除浏览器可访问的 client secret 或 Supabase 配置。 外部账号的邀请和邮箱密码登录发生在 IWish Auth Portal,不在业务 App 重建。 ## 5. 替换权限 - 把服务端本地角色判断替换为 `requirePermission`。 - 前端菜单可根据 session permissions 隐藏,但服务端必须再次检查。 - 移除本地统一角色分配写入口。 - 保留业务记录归属、字段级规则和客户数据过滤。 - 按需消费 `clientAssignments`,不强制所有 App 使用。 ## 6. 用户映射 内部员工以飞书身份为准,使用 IWish Auth `user.id` 作为跨 App 稳定主体。旧 App 需要建立一次性 `legacy_user_id -> iwish_user_id` 映射,不得根据邮箱在请求时动态猜测。 外部账号由管理员邀请或创建,明确映射后再开放业务数据。 ## 7. 切换策略 在 staging 完成所有验证后: 1. 冻结旧用户和角色管理写入。 2. 导入并复核用户映射。 3. 同步 prod Manifest,创建 prod OAuth client。 4. 部署新版本并切换登录入口。 5. 立即运行未登录、登录、权限、登出和关键业务 smoke test。 6. 观察 Auth 错误率和业务关键指标。 7. 在确认窗口结束后删除旧密码和 session 数据。 ## 8. 回滚边界 回滚是应用版本回滚,不是长期双系统。回滚包必须在切换前准备,并包含: - 上一个应用版本。 - 配置和 callback 回滚步骤。 - 数据写入兼容说明。 - 触发阈值和负责人。 已经删除的密码不能恢复。若必须回滚到旧登录,应在迁移前明确安全批准和短时恢复方案。 ## 9. 迁移完成证据 - 旧登录和本地角色写入口不可访问。 - 关键路由使用 SDK 权限检查。 - Manifest、OAuth client 和角色分配有审计记录。 - 内部员工飞书登录、外部账号邮箱登录均通过。 - 角色撤销、员工离职和 App 停用能使访问失效。 - 业务数据授权和客户项目范围没有扩大。 - 密钥扫描、回归、监控和回滚演练通过。 --- # 迁移 Playbook > 按阶段执行、回滚和验收真实 App 迁移 本 Playbook 用于真实业务 App 迁移执行。每个 App 建立独立迁移记录,填写负责人、时间、证据链接和最终结论。 ## 阶段 A:立项与风险分级 - [ ] 指定业务负责人、App owner、Auth 平台负责人和回滚负责人。 - [ ] 记录技术栈、生产域名、用户规模、关键业务时段。 - [ ] 标记是否包含高风险数据导出、审批、删除或客户隔离。 - [ ] 确定 dev、staging、prod 的独立 App/client 配置。 - [ ] 确定维护窗口、回滚窗口和通知对象。 输出:迁移卡片、风险等级、时间表。 ## 阶段 B:身份与权限盘点 - [ ] 导出旧用户、角色和用户角色关系。 - [ ] 列出全部服务端保护点和前端角色判断。 - [ ] 区分 App 权限、业务数据授权和客户项目上下文。 - [ ] 形成旧角色到新 permission/role 的映射表。 - [ ] 复核内部员工与飞书身份映射,外部账号单独列出。 输出:`permission-mapping.md`、用户映射清单、异常账号清单。 ## 阶段 C:Dev 接入 - [ ] 编写 `auth.manifest.json` 并通过本地校验。 - [ ] 管理员完成 Manifest 预检和同步。 - [ ] 创建 dev OAuth client,登记精确 callback。 - [ ] SDK 登录、callback、App session、登出接入完成。 - [ ] 服务端权限检查替换完成。 - [ ] `clientAssignments` 仅在需要的流程中消费。 - [ ] 本地密码、角色写入口已从新代码路径移除。 输出:代码 PR、Manifest diff、自动测试。 ## 阶段 D:Staging 验收 - [ ] 内部员工飞书登录。 - [ ] 外部测试账号邀请/邮箱登录。 - [ ] 无权限、跨 App session、停用 App 和撤销角色路径。 - [ ] 客户项目为空、多个职责、职责过期和员工离职路径。 - [ ] 关键业务数据范围和字段授权回归。 - [ ] 全局登出、局部登出、secret 轮换演练。 - [ ] 日志中不含 token、secret、密码和飞书凭据。 输出:staging 验收记录、缺陷清单、回滚演练记录。 ## 阶段 E:生产切换 1. 冻结旧身份和角色写入。 2. 备份必要映射与业务数据。 3. 同步 prod Manifest,创建 prod client 和 secrets。 4. 部署迁移版本。 5. 执行生产 smoke test。 6. 观察 401/403、callback、关键 API 和业务指标。 7. 由业务负责人确认继续或触发回滚。 生产 secret 不得通过聊天、截图、工单正文或 Git 传递。 ## 阶段 F:收尾 - [ ] 删除旧登录、注册、找回密码和角色管理路由。 - [ ] 清理旧密码 hash、session 和不再使用的 secrets。 - [ ] 更新运行手册、值班手册和用户帮助。 - [ ] 保存 Manifest、OpenAPI、测试和审计证据。 - [ ] 记录后续改进项,但不保留长期双系统。 输出:迁移完成报告和正式下线确认。 ## 回滚触发条件 至少定义: - 登录成功率低于阈值。 - callback 或 token exchange 持续失败。 - 权限错误导致越权或关键岗位无法工作。 - 客户数据范围扩大或错误收窄。 - 无法在约定时间内恢复关键业务。 越权或客户数据泄露风险出现时立即停止切换,不等待业务指标确认。 --- # 接入 Checklist > 上线前逐项检查身份、权限与安全边界 在 App 进入 staging 和 production 前逐项确认。未标明“不适用”的项目必须有可复核证据。 ## App 与 Manifest - [ ] `appKey` 唯一、稳定、全小写,所有 permission/role 位于该命名空间。 - [ ] Manifest 包含 `{app}.access`。 - [ ] 权限是业务动作,不包含客户、部门、用户或 UI 组件。 - [ ] 角色只引用当前 Manifest 已声明权限。 - [ ] 本地 `manifest validate` 通过。 - [ ] 远程预检 diff 已审核,`manifestVersion` 唯一。 - [ ] prod Manifest 有 owner/管理员审批和回滚版本。 ## OAuth 与 Session - [ ] 每个环境、每个 App 使用独立 OAuth client。 - [ ] callback 使用 HTTPS(本地开发除外)并精确登记。 - [ ] `clientSecret` 只在服务端 secret manager。 - [ ] state、PKCE S256 和一次性 code 均由 SDK/服务端处理。 - [ ] App session cookie 包含 HttpOnly、Secure、SameSite=Lax、Path。 - [ ] 业务 App 不解析 opaque token,不信任身份 Header。 - [ ] 局部登出和全局登出均已验证。 ## 身份边界 - [ ] 内部员工只走飞书统一身份,不维护第二套 Auth 员工账号。 - [ ] 外部访问账号走邀请/邮箱密码,不强制飞书。 - [ ] 业务 App 不接飞书 OAuth、通讯录或保存飞书 token。 - [ ] 业务 App 不直接查询 Supabase Auth 或 IWish Auth 数据库。 - [ ] 员工离职/冻结后现有 App session 失效。 ## 权限与业务数据 - [ ] 每个受保护服务端路由执行 `requireAppAccess` 和必要的 `requirePermission`。 - [ ] 前端隐藏按钮不能替代服务端权限检查。 - [ ] 跨 App permission 和 session 被拒绝。 - [ ] 业务数据归属、客户隔离和字段权限保留在 App 内。 - [ ] `clientAssignments` 只用于项目上下文,不替代 permission。 - [ ] 空 `clientAssignments` 不会自动扩大权限或导致登录失败。 ## 迁移清理 - [ ] 本地登录、注册、找回密码和密码重置入口已移除。 - [ ] 本地统一角色分配写入口已移除。 - [ ] 旧用户到 IWish Auth user ID 映射已复核。 - [ ] 旧密码 hash、session 和 secrets 有明确下线时间。 - [ ] 不保留 Auth Proxy 或长期双登录。 ## 测试与运维 - [ ] 401、403、允许访问、角色撤销、App 停用和 secret 轮换测试通过。 - [ ] 内部员工和外部账号登录路径通过。 - [ ] 桌面和移动端 callback/登出流程通过。 - [ ] 日志和错误页面不泄漏 token、secret、密码或敏感个人信息。 - [ ] Auth API 超时、不可达和稳定错误码有明确用户体验。 - [ ] 监控覆盖登录成功率、401/403 异常、callback 错误和关键业务指标。 - [ ] 生产回滚命令、负责人和触发阈值已经演练。 ## AI Agent 交付 - [ ] Agent 读取 `/llms.txt` 和 `/llms-full.txt` 后再修改代码。 - [ ] Agent 没有创建飞书/Supabase 直连或自定义 token 解析。 - [ ] Agent 使用真实 SDK 导出和当前 Manifest schema。 - [ ] Agent 输出变更文件、权限映射、验证命令和未决风险。 - [ ] 人工复核身份、权限、secrets 和数据授权边界。 --- # 故障排查 > 稳定错误码、诊断顺序和常见问题 先按错误发生阶段定位,不要通过关闭 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` 校验。 ### `permission_not_granted` 用户已登录但没有权限。检查 Manifest 是否同步、组织是否开通 App、角色是否分配、角色是否包含该 permission。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 可以完成 IWish Auth 接入代码,但必须把公开文档和机器契约作为唯一协议来源,并由人工复核身份、权限、secrets 和业务数据授权。 ## 1. 必须读取的输入 按顺序读取: 1. `/llms.txt`:边界、入口和文件索引。 2. `/llms-full.txt`:完整接入规范。 3. `/openapi.json`:HTTP 契约。 4. `/auth-manifest.schema.json`:Manifest 契约。 5. 当前技术栈框架指南。 6. 当前 App 的代码、依赖、路由和权限盘点。 如果文档与代码导出冲突,停止修改并报告冲突,不要猜测 API。 ## 2. Agent 执行顺序 1. 识别技术栈和服务端入口。 2. 搜索本地登录、密码、session、角色和权限判断。 3. 输出旧角色到新 permission/role 映射,等待或记录人工确认。 4. 创建 `auth.manifest.json` 并通过 schema/CLI 校验。 5. 安装官方 SDK。 6. 接入服务端 login、callback、logout 和 App session。 7. 用 `requirePermission` 替换服务端角色判断。 8. 仅在业务需要时使用 `clientAssignments`。 9. 移除本地身份和统一角色写入口。 10. 添加 401、403、跨 App、登出和关键业务测试。 11. 运行 typecheck、test、build、lint 和 Manifest validate。 12. 输出变更、验证证据、迁移风险和人工操作项。 ## 3. 不可突破的边界 - 不直接接飞书 OAuth 或通讯录。 - 不直接使用 Supabase Auth SDK 完成业务 App 登录。 - 不读取或写入 IWish Auth 数据库。 - 不把 client secret、token、密码写入代码、日志或回复。 - 不创建 Auth Proxy、长期双登录或降级身份 Header。 - 不自行解析 opaque App session。 - 不用 `clientAssignments` 代替 App permission 或业务数据授权。 - 不根据邮箱自动合并飞书员工与外部账号。 - 不静默扩大 callback allowlist、CORS 或 cookie Domain。 ## 4. 期望交付格式 Agent 最终输出: ```text 变更文件: - ... 权限映射: - 旧角色/判断 -> permission -> role 验证结果: - manifest validate - test - typecheck - build - lint 人工操作: - 创建/更新 OAuth client - 同步 Manifest - 配置服务端 secrets - 分配试点角色 剩余风险: - ... ``` ## 5. Agent 自检问题 提交前必须回答: 1. 所有身份恢复是否都来自 IWish Auth session introspection? 2. 是否有任何浏览器代码可以读取 client secret 或 App session? 3. 每个高风险服务端动作是否有 permission 检查? 4. 是否错误删除了业务 App 自身的数据授权? 5. 是否把项目分工当成权限? 6. 是否保留了旧密码、角色写入口或双登录? 7. 是否验证了跨 App session/permission 被拒绝? 8. 所有示例 API 是否存在于 OpenAPI 或 SDK 导出? ## 6. 可验证示例 仓库中的 `examples/ai-agent-app` 是按本指南生成的最小 Express-like 接入成果。它只使用官方 SDK、Manifest 和 App session context,并由 `verify:phase-7` 自动检查。该示例不是业务模板,真实迁移仍需盘点本地数据授权和回滚边界。 --- # 机器契约 - OpenAPI: /openapi.json - Manifest Schema: /auth-manifest.schema.json - SDK examples: examples/nextjs-app, examples/fastapi-app, examples/ai-agent-app