---
url: /wave-agent/specs/enterprise/sso-auth.md
description: '`/login` 浏览器 SSO 登录、token 自动刷新与网关路由，含 IDE/桌面登录入口与未登录输入禁用'
---

# 功能规格说明：SSO 认证

**创建日期**：2026-05-12

## 用户场景与测试 *（必填）*

### 用户故事：通过浏览器进行 SSO 登录（优先级：P1）

作为在本地机器上工作的开发者，我希望在 Wave 中输入 `/login` 通过公司 SSO 进行认证，这样就不必手动配置 API 密钥。

**为什么是这个优先级**：这是主要的认证流程——大多数用户在本地机器上可以使用浏览器。

**独立测试**：设置 `WAVE_SERVER_URL`，运行 Wave，输入 `/login`，浏览器打开，完成 SSO 登录，验证 token 已保存且 API 请求成功。

**验收场景**：

1. **假设** `WAVE_SERVER_URL` 已设置且没有现有的 SSO token，**当**用户输入 `/login` 时，**则** Wave 打开浏览器到 SSO 登录页面并在终端中显示认证 URL。
2. **假设**用户在浏览器中完成 SSO 登录，**当**浏览器重定向到带有 `?code={code}` 的 localhost 回调 URL 时，**则** Wave 通过 `POST /api/auth/token` 使用 `{ grant_type: "authorization_code", code }` 交换 code 获取 JWT，将 token 和 refresh token 保存到 `~/.wave/auth.json`，并显示"Login successful"。
3. **假设**用户已通过 SSO 认证，**当**用户发送消息时，**则**所有 LLM API 请求发送到 `WAVE_SERVER_URL/api/v1`，使用 SSO token 作为 Bearer 认证。
4. **假设**用户已通过 SSO 认证，**当**用户输入 `/login` 时，**则** Wave 显示当前认证状态（截断的 token、AI URL）并提供按 Enter 退出的选项。
5. **假设**用户已认证并在登录 UI 中按 Enter，**当**他们确认退出时，**则** SSO token 从 `~/.wave/auth.json` 中移除，Wave 显示"Logged out successfully"。

***

### 用户故事：远程服务器的手动 Token 输入（优先级：P1）

作为通过 SSH 在远程服务器上工作的开发者，我希望在本地浏览器完成登录后手动粘贴 SSO token，以便在没有 localhost 回调转发的情况下进行认证。

**为什么是这个优先级**：大量开发者使用远程服务器（SSH、容器、devbox），localhost 回调无法到达 CLI。没有这个功能，`/login` 对他们来说无法使用。

**独立测试**：SSH 到远程服务器，设置 `WAVE_SERVER_URL`，运行 Wave，输入 `/login`，在本地浏览器中打开 URL，完成 SSO，从浏览器 URL 栏复制 token，粘贴到终端。

**验收场景**：

1. **假设** Wave 在远程服务器上运行，**当**用户输入 `/login` 时，**则** Wave 显示 SSO 认证 URL 并提示"Paste the authorization code from your browser URL bar:"。
2. **假设**用户粘贴有效的授权码并按 Enter，**则** Wave 通过 `POST /api/auth/token` 使用 `{ grant_type: "authorization_code", code }` 交换 code 获取 JWT，将 token 和 refresh token 保存到 `~/.wave/auth.json`，并显示"Login successful"。
3. **假设**用户粘贴空行，**则** Wave 清除输入并继续等待 token 输入。
4. **假设**用户在 token 输入期间按 Escape，**则**登录流程被取消，Wave 返回空闲状态。

***

### 用户故事：自动 SSO API 路由（优先级：P1）

作为已通过 SSO 认证的开发者，我希望所有 LLM API 请求自动使用 Wave AI 代理，这样就不必配置 `WAVE_API_KEY` 或 `WAVE_BASE_URL`。

**为什么是这个优先级**：这是核心价值主张——SSO 认证应透明地通过 Wave AI 路由 API 流量而无需额外配置。

**独立测试**：通过 SSO 登录，发送消息，验证 API 请求发送到 `WAVE_SERVER_URL/api/v1/chat/completions`，使用 Bearer SSO token。

**验收场景**：

1. **假设** `~/.wave/auth.json` 包含有效的 `SSO_TOKEN`，**当** Agent 解析网关配置时，**则**它返回 `{ apiKey: SSO_TOKEN, baseURL: "${WAVE_SERVER_URL}/api/v1" }`，无论 `WAVE_API_KEY` 或 `WAVE_BASE_URL` 设置。
2. **假设**不存在 `SSO_TOKEN`，**当** Agent 解析网关配置时，**则**它回退到现有行为（读取 `WAVE_API_KEY`/`WAVE_BASE_URL`）。
3. **假设** `SSO_TOKEN` 存在但 `WAVE_SERVER_URL` 未设置，**当**解析网关配置时，**则**抛出配置错误并附带清晰消息。
4. **假设** `SSO_TOKEN` 存在且未过期、但服务端以 401/403 拒绝（典型：换过 `WAVE_SERVER_URL` 后 token 与签发环境错位，或 token 被吊销），**当**带该 token 的请求返回 401/403 时，**则**系统**无视本地过期判断**强制用 refresh token 刷新一次并重试；刷新被服务端拒绝（400/401）时清除认证，此后请求回退到 `WAVE_API_KEY`/`WAVE_BASE_URL`（即场景 2），刷新遇到瞬时网络错误时保留现有 token。
5. **假设**同一台机器上有多个 Wave 进程共享 `~/.wave/auth.json`（桌面端 + IDE 插件 + 终端 CLI + daemon 各自独立进程、同一个 home），**当**其中两个同时 401 并刷新时，**则**refresh token 轮转只让一个成功：判定「是否已被别人刷过」以**磁盘上的 token 值是否不同于自己失败的那个**为准（不看文件 mtime），刷新请求被判 400/401 时先重读磁盘，若已存在不同的有效 token 就复用它并**保持登录**，只有磁盘上仍是同一个失效 token 才清认证。
6. **假设**刷新请求在途时用户点了退出登录（token 键被删除），**当**刷新响应返回成功时，**则**丢弃这次结果、不写回 `auth.json` —— 否则会把用户刚结束的会话复活。
7. **假设**用户在用户级 `settings.json` 里同时设置了 `WAVE_SERVER_URL` 和 `WAVE_BASE_URL`/`WAVE_API_KEY`，**当**解析网关配置时，**则**当前选中的分支（SSO 还是直连）连同 `baseURL` 与「是否带 API key」写入 INFO 日志，用户把日志发回来即可判断请求打到哪、带没带凭据。

***

***

### 用户故事：插件端更多菜单的登录/退出登录入口（优先级：P2）

作为在 VS Code 或 JetBrains 插件中使用 Wave 的开发者，我希望在聊天头部"更多"菜单中按当前认证状态切换显示"登录"或"退出登录"入口，并在已有对话时退出登录不跳转到欢迎页，这样在 GUI 环境下也能便捷地完成认证状态切换且不丢失当前对话上下文。

**为什么是这个优先级**：插件端（VS Code 扩展 + JetBrains 插件，共用同一 webview）没有 CLI 的 `/login`、`/logout` 斜杠命令入口的便利性，需要在 GUI 菜单中提供等价入口；已有对话时退出登录跳欢迎页会丢失上下文，违背用户预期。

**独立测试**：在 VS Code 扩展或 JetBrains 插件中打开已有对话，点击头部"更多"按钮，验证认证态显示"退出登录"、未认证态显示"登录"；点击"退出登录"后对话消息保留且不跳欢迎页、菜单切换为"登录"。

**验收场景**：

1. **假设**用户已通过 SSO 认证且当前会话已有消息，**当**用户点击头部"更多"按钮打开菜单时，**则**菜单显示"退出登录"项（不显示"登录"项）。
2. **假设**用户已认证且当前会话已有消息，**当**用户点击"退出登录"时，**则**系统清除 SSO token、菜单关闭、认证状态切换为未认证，且当前对话消息保留、不跳转到欢迎页。
3. **假设**用户未认证（已退出或从未登录），**当**用户打开"更多"菜单时，**则**菜单显示"登录"项（不显示"退出登录"项）。
4. **假设**用户未认证，**当**用户点击"登录"时，**则**系统发起 SSO 登录流程（浏览器/手动 token 输入），登录成功后认证状态切换为已认证、菜单切换回"退出登录"项。
5. **假设**用户未认证且欢迎页（空对话引导页）显示"登录"按钮，**当**用户点击该按钮时，**则**它与"更多"菜单中的"登录"项触发同一登录流程，二者行为一致。

***

### 用户故事：插件端更多菜单与欢迎页（优先级：P2）

作为插件端用户，我希望在"更多"菜单中直达设置、企业控制台和登录入口，并在没有对话时看到引导性的欢迎页，以便快速完成初始配置和日常使用。

**为什么是这个优先级**：菜单与欢迎页是 GUI 环境的入口框架，缺失时用户找不到设置与控制台入口；但核心对话能力不受影响，属于体验增强。

**独立测试**：打开插件，验证空对话欢迎页在未认证时显示"登录"按钮与「登录后即可开始使用~」引导文案、已认证时仅显示品牌引导；打开"更多"菜单验证包含设置、企业控制台和登录入口；聊天头部不显示独立的"登录"按钮（登录入口只存在于欢迎页与"更多"菜单）；验证插件不再提供 API Key / Base URL 直连配置入口（设置页无凭据控件、宿主报文不带这三个键——注意这**不等于**「未登录即不可用」：已写在 `settings.json` `env` / OS 环境变量里的凭据仍然有效，界面须按 `credential.kind` 如实渲染）。

**验证分层（2026-09-10 追加拍板）**：本故事场景 5/7 的「未认证即显示登录入口、遗留直连配置不再关掉它」→ 单测 `packages/webview/tests/webview/welcomeViewFlash.test.tsx` + **e2e**（真浏览器 + 真 webview bundle）`packages/webview/e2e/plugin-welcome-login.e2e.ts`；场景 8 的「未认证时发送入口禁用 + 禁用原因写在占位文案 + 登录后恢复」→ 单测 `packages/webview/tests/webview/unauthenticatedInputDisabled.test.tsx`（IDE 未登录）与 `packages/webview/tests/webview/desktopApp.test.tsx`（桌面未选目录「should disable the input area when no workdir is selected」两条原因独立 ＋「shows the login reason, not the workdir reason, when both reasons apply」登录优先）+ **e2e** `packages/webview/e2e/plugin-welcome-login.e2e.ts` 与 `packages/webview/e2e/desktop-unauthenticated-input-disabled.e2e.ts`（真 bundle 验证两条原因互不覆盖、登录原因优先）；「设置页不得存在凭据输入控件」→ **e2e** `packages/webview/e2e/plugin-settings-credentials-removed.e2e.ts`（7 个视图全扫，且带遗留 `apiKey`/`baseURL` 的 `configurationResponse` 不得长出控件）；「宿主→CLI 报文不得携带三个键、未登录时宿主如实回带未登录且不发模型请求」→ **真 host** `packages/desktop/tests/integration/realHostCredentialWire.integration.test.ts`（tee 透明代理真 `wave --stdio`，断言 `initialize` / `updateConfig` / `sendMessage` 双向真报文）。逐条对照见 [agent-config.md](../core/agent-config.md) 边界说明「验证分层（单测 / e2e / 真 host）」。

**验收场景**：

1. **假设**用户打开"更多"菜单，**当**菜单渲染时，**则**包含"设置"、"企业控制台"、以及按认证状态切换的"登录/退出登录"入口（行为见下文对应条目）。
2. **假设**用户点击"更多"菜单中的"设置"，**当**操作发生时，**则**打开设置页「全局设置」选项卡（见 agent-config.md「插件端配置入口」用户故事）。
3. **假设**用户点击"更多"菜单中的"企业控制台"，**当**操作发生时，**则**在系统浏览器中打开当前服务端地址对应的控制台页面。
4. **假设**当前会话没有任何可见消息（隐藏的 meta 用户消息不计入，如 SessionStart 钩子注入的 `additionalContext`）且初始化已完成，**当**界面渲染时，**则**显示欢迎页（而非空白对话区）；初始化未完成时显示扫光加载动画，**且**加载动画显示期间不展示输入区域（输入框与目录选择器等一并隐藏），动画结束、初始化完成后输入区域随欢迎页一同出现。
5. **假设**欢迎页已显示（当前无任何可见消息）且初始化已完成、用户**无任何可用凭据**（未 SSO 认证、也没有 `WAVE_API_KEY`），**当**欢迎页渲染时，**则**欢迎页在品牌引导下方显示「登录后即可开始使用~」引导文案与"登录"按钮（仅插件端显示；desktop 的登录入口在侧边栏账户卡片，欢迎页不显示登录按钮）；用户已认证**或已配置网关凭据**（`credential.kind = "gateway"`）时欢迎页仅显示品牌引导、不显示登录按钮与引导文案——「登录后即可开始使用」对有网关凭据的人来说是假话（2026-11-09 随发送门控同批修正）。插件端**不再提供 API Key / Base URL 直连配置入口**（2026-09-10 拍板：宿主端凭据链路整体下线）——设置页各视图内**不得存在任何凭据输入控件**（API Key / Base URL / headers 语义的输入框、下拉或文案），宿主向会话进程发送的 stdio 报文（`initialize` / `updateConfig` 等）也**不得携带** `apiKey` / `baseURL` / `defaultHeaders`（宿主→CLI→回包的整条链路都不得出现这三个键；保留的是 SDK / CLI 层的构造参数、`WAVE_API_KEY` / `WAVE_BASE_URL` 环境变量与企业侧下发，见边界情况）。因此**界面上**不再存在「在设置页填 key 即可免登录」的旁路；**但已经写在 `settings.json` 的 `env` / OS 环境变量 / 企业下发里的凭据仍然有效**（SDK 照旧认它们，见边界情况），宿主据此下发 `credential.kind = "gateway"` 时界面按「有可用凭据」渲染——欢迎页不显示登录 CTA、发送入口不锁（2026-11-09 修正原先「IDE 未登录即不可用」的一刀切表述，见场景 8）。
6. **假设**用户在欢迎页输入并发送第一条消息，**当**消息发出后，**则**欢迎页切换为正常对话视图。
7. **假设**欢迎页显示"登录"按钮且用户点击它，**当**登录流程完成、认证状态切换为已认证时，**则**欢迎页的"登录"按钮与「登录后即可开始使用~」引导文案消失，欢迎页仅剩品牌引导；登录流程与"更多"菜单的"登录"项一致（见「插件端更多菜单的登录/退出登录入口」）。
8. **假设**用户**没有任何可用凭据**（既无 SSO token，也未配置自定义网关的 `WAVE_API_KEY`，判据见 [desktop-account.md](../desktop/desktop-account.md)「认证方式」故事的 `credential.kind = "none"`），**当**对话界面渲染时，**则**发送入口整体禁用（输入区不可编辑，发送 / 附件 / 快捷指令 / 权限模式按钮均置灰），输入框占位文案显示禁用原因「请先登录后再发送消息」；输入区内不出现登录按钮、也不出现指路文案。**假设**用户随后完成登录（或此前已配置网关凭据、宿主因此下发 `credential.kind = "gateway"`），**当**可用凭据存在时，**则**输入框可编辑、占位文案为默认提示（`/快捷指令，@添加上下文，粘贴图片，Enter发送...`）——**判据是「有没有可用凭据」，不是「有没有 SSO token」**：把 key 写在 `settings.json` 的 `env` 里（或 OS 环境变量 / 企业下发）同样能发消息，界面不得因未登录 SSO 就把入口锁死（2026-11-09 修正，此前按 `isAuthenticated` 判定，导致有凭据的用户被锁在门外）。该禁用在桌面端与插件端同一形态。桌面端另有「未选工作目录」这一独立禁用原因，其占位文案为「请先选择项目目录」；两条原因互斥且**凭据优先**——无凭据且同时未选目录时显示凭据原因，有凭据但未选目录时占位文案切换为目录原因，选定目录后解除禁用并回到默认提示。禁用原因文案本身保持不变：无凭据时一键可达的动作只有登录（网关凭据是手改文件路径，不在输入区指路）。

***

### 用户故事：CLI 欢迎页的登录引导（优先级：P2）

作为使用 CLI 的开发者，我希望在未通过 SSO 认证、也未配置直连 API 时，欢迎页提示我输入 `/login` 进行登录，这样即使不记得斜杠命令也能发现登录入口。

**为什么是这个优先级**：登录是可选的（直连 API 配置也可正常使用），缺少提示只是降低入口的发现性，不影响核心对话能力。

**独立测试**：在没有 SSO token 也没有 `WAVE_API_KEY` 的环境启动 CLI，验证欢迎页显示 `/login` 登录引导；配置 `WAVE_API_KEY` 后（或完成 SSO 登录后）重启 CLI，验证引导不再显示。

**验收场景**：

1. **假设** CLI 已启动且用户未通过 SSO 认证、也未配置直连 API，**当**欢迎页渲染时，**则**在欢迎页显示登录引导文案（提示输入 `/login`）。
2. **假设**用户已通过 SSO 认证，**当**欢迎页渲染时，**则**不显示登录引导。
3. **假设**用户未通过 SSO 认证但已配置直连 API（`WAVE_API_KEY`/`WAVE_BASE_URL` 环境变量，或 settings.json `env` 中的同名变量），**当**欢迎页渲染时，**则**不显示登录引导（本条原为 CLI 语义；2026-11-09 起插件端与桌面端同口径——它们没有直连**配置入口**，但文件/环境里已有的凭据仍有效，界面不得按「未登录」渲染，见上文场景 5 / 8 与边界情况「插件端不再有直连免登录旁路」）。
4. **假设**欢迎页显示登录引导且用户输入 `/login` 完成登录，**当**认证状态变化为已认证时，**则**登录引导实时消失。

***

### 边界情况

* **插件端不再有「直连免登录」旁路（2026-09-10 拍板）**：宿主端（VS Code 扩展 / JetBrains 插件 / 桌面端）的 API Key / Base URL / headers 用户配置链路**整体下线**——宿主设置页与插件状态里不再有这三项，webview→宿主→stdio 的透传与回包字段一并删除，IDE 因此**不再有「在界面里配 key 就免登录」的入口**（此前残留在插件 globalState / 插件状态里的旧 `baseURL` + `apiKey` 仍可免登录聊天，该后门随之下线；注意这**不是**「凡未登录即不可用」，文件与环境里的凭据见本段末）。**SDK / CLI 层不受影响**：`WAVE_API_KEY` / `WAVE_BASE_URL` 环境变量、构造参数（`apiKey` / `baseURL` / `defaultHeaders`）与企业侧下发仍保留（stdio 协议里的同名字段也因此保留，宿主只是不再填充），故 CLI 欢迎页的「已配置直连 API 则不提示登录」语义不变（见「CLI 欢迎页的登录引导」）。**但界面必须如实反映「env 里已有的凭据仍然有效」（2026-11-09 补）**：SDK / CLI 层照旧认 `WAVE_API_KEY` / `WAVE_BASE_URL`（含企业下发与 OS 环境变量），宿主就不能把「没有 SSO token」渲染成「没有凭据」——桌面卡片的「自定义网关」形态、发送入口禁用判据、欢迎页登录 CTA 一并改读宿主下发的 `credential.kind`（见上文场景 5 / 8 与 [desktop-account.md](../desktop/desktop-account.md)「认证方式」故事）。下线的始终是**配置入口**（设置页控件、宿主→stdio 的 `apiKey` / `baseURL` 透传字段），不是**已经写在文件或环境里的凭据本身的能力**。
* **无可用凭据时发送入口禁用，禁用原因写在输入框里（2026-09-10 拍板，2026-11-09 把判据从「未认证」修正为「无可用凭据」）**：判据是**有没有可用凭据**（SSO token **或** 自定义网关的 `WAVE_API_KEY`，由宿主下发的 `credential.kind` 表达，见 [desktop-account.md](../desktop/desktop-account.md)「认证方式」故事），不是 `isAuthenticated`——用户把 key 写进 `settings.json` 的 `env` / OS 环境变量 / 企业下发后本就**能正常发消息**（SDK 的 `resolveGatewayConfig` 在没有 SSO token 时就是这么取凭据的），旧判据会把这批人锁在门外。只有 `kind = "none"` 时才**发送入口整体禁用**——复用 `MessageInput` 既有的 `disabled` 语义（输入区 `contentEditable=false`，发送 / 附件（"+"）/ 快捷指令（"/"）/ 权限模式等按钮一并置灰），桌面端与插件端**同一形态**。**禁用原因只写在输入框的占位文案处**（`data-placeholder`）：无凭据显示「请先登录后再发送消息」，桌面端「未选工作目录」显示「请先选择项目目录」（此前该禁用态沿用了通用的 `/快捷指令，@添加上下文…` 提示，属"禁用但不说原因"，2026-09-10 一并改掉）；两条原因**互斥**（同一时刻只有一条生效）且**凭据优先**（无凭据必然发不出消息，目录未选只是还没定位到项目，故两者同时成立时先说更根本的那条）。**不新增提示行、不在输入区放登录按钮、不写「请去某处登录」的指路文案**——登录入口本身用户能看见（IDE 在欢迎页与"更多"菜单，桌面在左侧账户卡片），有可用凭据的用户完全不受影响；文案由调用方按当前原因派生后传入 `MessageInput` 的可选占位覆盖参数，组件本身不硬编码任何禁用原因，未禁用时不传（回落默认 `/快捷指令…` 提示）。这样做的依据是实测事实：无凭据必然发不出消息（此时**不产生任何模型请求**，且不报错、只是静默挂住），所以把入口直接置灰并说明原因是唯一有意义的用户可见反馈；仍**不新增**「无凭据即拒绝新建对话/拒绝恢复历史」这类宿主或 CLI 层硬门禁（三端均无，若产品需要须另立规格）。
* **如果 SSO 回调服务器端口已被占用会怎样？** 服务器使用 `localhost:0`（系统分配的随机端口），避免端口冲突。
* **如果 Wave AI 上没有配置 SSO 提供程序会怎样？** 登录失败并显示清晰的错误消息（"No SSO providers available"）。
* **如果登录时 `WAVE_SERVER_URL` 未设置会怎样？** 登录失败并显示清晰的错误，指示用户设置环境变量。
* **如果浏览器无法打开（无头服务器）会怎样？** 认证服务器保持活动，用户可以手动打开 URL 并粘贴授权码。
* **如果用户将来在 `auth.json` 中保存额外字段会怎样？** `saveAuth` 方法与现有配置合并，保留非 SSO_TOKEN 字段。
* **如果 token 过期（JWT 默认 8 小时）会怎样？** 系统在过期前 5 分钟使用存储的 refresh token 主动刷新 token。如果 refresh token 被撤销（400/401），认证被清除，用户必须重新运行 `/login`。在刷新期间的瞬时网络错误中，保留现有 token，重试在下次请求时进行。
* **如果 token 还没过期、服务端却不认（换过 server 环境、token 被吊销）会怎样？** 「过期判断」只是本地时钟推算，不能代表服务端是否接受——所以请求返回 401/403 时**一律强制刷新**再重试，不看本地 `SSO_TOKEN_EXPIRES_AT`（此前把「无需刷新」误当成「刷新成功」，导致拿同一个死 token 反复重试、也永远降级不到直连）。刷新被拒即清认证，后续请求自动落到 `WAVE_API_KEY`/`WAVE_BASE_URL`；token 文件里没有 `SSO_TOKEN_EXPIRES_AT` 的旧格式同样适用。
* **用户报「设了 `WAVE_BASE_URL` 却不生效」怎么定位？** 判据只有一条——`auth.json` 里有没有非空 `SSO_TOKEN`。有则 SSO 分支压过一切 base URL（场景 1）；要回退直连必须显式退出登录。当前分支与目标地址记在 INFO 日志里（场景 5），不再靠猜。
* **如果 SSO 登录超时（5 分钟）会怎样？** 认证服务器关闭并显示错误。用户可以重试 `/login`。
