nagisa-kunhah commented on issue #1503:
URL: https://github.com/apache/dubbo-admin/issues/1503#issuecomment-5084056001
### 一、Admin的接入
大致流程:用户点击 SSO 后请求 GET /api/v1/auth/oidc/login,Dubbo Admin 生成授权 URL
并把浏览器重定向到外部 OIDC Provider,Provider 登录成功后会把浏览器重定向回 GET
/api/v1/auth/oidc/callback?code=\<authorization-code\>&state=\<state\>,Dubbo
Admin 再用这个 code 调 Provider 的 token endpoint 换token、校验用户身份、写入本地 session,前端随后通过
GET /api/v1/auth/userinfo 获取当前用户信息,后续业务接口继续靠 session 访问。
<details open>
<summary>spec</summary>
```md
# 引入 OAuth2/OIDC 登录能力
## 变更概述
为 Dubbo Admin 引入基于 OAuth2/OIDC 的登录能力,使 Admin 控制台可以接入企业统一身份系统,并为后续 Agent
用户身份识别、审计和权限控制打基础。
第一阶段只聚焦 Admin Web 控制台登录:用户通过 OIDC Provider 完成认证,Dubbo Admin
后端解析用户身份后建立自己的 session。现有用户名密码登录继续保留,用于本地开发、兼容部署和紧急访问。
## 背景
当前 Dubbo Admin 的认证方式是配置文件中的固定用户名密码。后端登录成功后将 `user` 写入 Gin session,后续接口通过
session 判断是否登录。
这种方式实现简单,但在生产环境有明显限制:
- 无法接入公司统一登录系统。
- 只有一个静态账号,不能识别真实用户。
- 不利于后续审计、权限控制和 Agent 代表用户访问。
- 密码由 Dubbo Admin 自己维护,不符合企业 SSO 场景。
Issue `apache/dubbo-admin#1503` 提到 OAuth2 覆盖 Admin 接入和 Agent 用户身份标识。由于
Dubbo Admin 需要知道“当前用户是谁”,本变更应以 OIDC 为主,OAuth2 作为底层授权协议。
## 目标
- 支持 Admin Web 控制台通过 OIDC 登录。
- 保留现有用户名密码登录方式。
- 将认证后的用户身份归一化为统一 Principal 模型。
- 后续业务接口继续使用 Dubbo Admin 自己的 session 校验登录态。
- 前端不保存 `access_token`、`id_token`、`refresh_token`。
- 提供 fake OIDC server,用于本地开发和 e2e 测试。
- 为后续 Agent bearer token 校验、用户身份透传和 RBAC 留出扩展点。
## 非目标
- 第一阶段不维护 refresh token。
- 第一阶段不实现 Agent bearer token 认证。
- 第一阶段不实现 RBAC 权限拦截。
- 第一阶段不支持多个 OIDC Provider。
- 第一阶段不支持手动配置 authorization、token、userinfo、jwks endpoint override。
- 第一阶段不实现 Provider 级别的单点退出。
- 第一阶段不要求接入某个具体公司的 SSO 系统。
## 当前实现位置
当前认证相关代码主要在:
- `pkg/config/console/auth/config.go`:认证配置结构。
- `pkg/console/router/router.go`:`/api/v1/auth/login` 和
`/api/v1/auth/logout` 路由。
- `pkg/console/handler/auth.go`:用户名密码登录和退出逻辑。
- `pkg/console/component.go`:Gin session 初始化和登录态校验中间件。
- `ui-vue3/src/Login.vue`:前端登录页。
- `ui-vue3/src/api/service/login.ts`:前端登录 API 封装。
- `ui-vue3/src/utils/AuthUtil.ts`:前端本地 `auth-state` 状态。
当前后端中间件只跳过路径后缀为 `/login` 的请求,OIDC callback 接入后需要改为显式匿名路径白名单。
## 用户流程
### 用户名密码登录
1. 用户打开 Admin UI。
2. 前端提交用户名密码到 `POST /api/v1/auth/login`。
3. 后端校验配置文件中的 `console.auth.user` 和 `console.auth.password`。
4. 校验成功后写入 Principal session。
5. 后续 API 请求通过 session cookie 识别登录态。
### OIDC 登录
1. 用户打开 Admin UI。
2. 登录页展示 SSO 登录入口。
3. 用户点击 SSO 登录,浏览器访问 `GET /api/v1/auth/oidc/login`。
4. 后端生成 `state`、`nonce` 和 PKCE 参数,并暂存到 session。
5. 后端重定向浏览器到 OIDC Provider 的 authorization endpoint。
6. 用户在 Provider 完成登录。
7. Provider 回调 `GET
/api/v1/auth/oidc/callback?code=<authorization-code>&state=<state>`。
8. 后端校验 `state`,使用 `code` 换取 token。
9. 后端校验 ID Token,或结合 UserInfo endpoint 获取用户信息。
10. 后端创建统一 Principal 并写入 session。
11. 后端重定向用户回 Admin UI。
12. 前端调用 `GET /api/v1/auth/userinfo` 获取当前用户信息。
## 配置变更
建议扩展 `console.auth`:
```yaml
console:
auth:
methods:
- password
- oidc
user: admin
password: dubbo@2025
expirationTime: 3600
sessionSecret: ${DUBBO_ADMIN_SESSION_SECRET}
oidc:
issuer: http://localhost:9999
clientId: dubbo-admin
clientSecret: ${DUBBO_ADMIN_OIDC_CLIENT_SECRET}
redirectUrl: http://localhost:8888/api/v1/auth/oidc/callback
postLoginRedirectUrl: http://localhost:8881/admin/
scopes:
- openid
- profile
- email
usernameClaim: preferred_username
groupsClaim: groups
rolesClaim: roles
```
说明:
- `methods` 控制启用的认证方式。
- `password` 登录沿用现有 `user` 和 `password`。
- `oidc.issuer` 用于 OIDC discovery;第一阶段只支持 discovery,不支持手动 endpoint
override。
- `redirectUrl` 是注册到 Provider 的回调地址。
- `postLoginRedirectUrl` 是 callback 成功后回到前端的地址。
- `sessionSecret` 替代当前硬编码的 `"secret"`。
## 协议库使用
Dubbo Admin 不自行实现 OAuth2/OIDC 协议细节,只实现与自身登录体系对接的 HTTP 接口和 session 逻辑。
OAuth2 授权码流程应使用 Go 官方维护的 `golang.org/x/oauth2`:
- 使用 `oauth2.Config.AuthCodeURL` 生成 Provider 登录跳转地址。
- 使用 `oauth2.Config.Exchange` 将 authorization code 换取 token。
- 使用 `oauth2.Config` 管理 `clientId`、`clientSecret`、`redirectUrl` 和
`scopes`。
OIDC 相关能力应使用成熟 OIDC 库,例如 `github.com/coreos/go-oidc/v3/oidc` 或等价库:
- 通过 `issuer` 做 OIDC discovery。
- 校验 ID Token 的签名、issuer、audience、expiration 和 nonce。
- 解析 claims。
- 必要时调用 UserInfo endpoint 获取用户信息。
Dubbo Admin 自己负责:
- 暴露 `/api/v1/auth/oidc/login` 和 `/api/v1/auth/oidc/callback`。
- 生成、保存和校验 `state`、`nonce`、PKCE 参数。
- 将 OIDC 用户信息映射为 Principal。
- 创建和清理 Dubbo Admin 本地 session。
- 将用户重定向回 Admin UI。
## 新增或调整的接口
新增:
- `GET /api/v1/auth/oidc/login`:发起 OIDC authorization code flow。
- `GET /api/v1/auth/oidc/callback`:处理 Provider 回调并创建本地 session。
- `GET /api/v1/auth/userinfo`:返回当前登录用户 Principal。
保留:
- `POST /api/v1/auth/login`:用户名密码登录。
- `POST /api/v1/auth/logout`:清理 Dubbo Admin 本地 session。
调整:
- auth middleware 使用显式 allowlist。
- 允许匿名访问登录入口、OIDC callback 和 health check。
- `/api/v1` 下业务接口默认要求已登录 session。
## Principal 模型
统一身份模型建议为:
```go
type Principal struct {
Subject string
Username string
Email string
Groups []string
Roles []string
AuthType string
Provider string
}
```
OIDC 登录时:
- `Subject` 来自 `sub`。
- `Username` 默认来自 `preferred_username`,不存在时回退到 `email` 或 `sub`。
- `Groups` 和 `Roles` 只保存,不在第一阶段参与权限拦截。
- `AuthType` 为 `oidc`。
用户名密码登录时:
- `Username` 来自表单中的用户名。
- `AuthType` 为 `password`。
## Fake OIDC Server
为了本地开发和 e2e 测试,需要提供轻量 fake OIDC server。它只模拟协议边界,不模拟完整公司 SSO。
建议位置:
- `test/fakeoidc/`:fake OIDC server 代码。
- `app/dubbo-admin/dubbo-admin-oidc-local.yaml`:本地 OIDC 配置。
最小 endpoint:
- `GET /.well-known/openid-configuration`
- `GET /oauth2/authorize`
- `POST /oauth2/token`
- `GET /userinfo`
- `GET /jwks`
测试用户示例:
```json
{
"sub": "user-123",
"preferred_username": "zhangsan",
"email": "[email protected]",
"groups": ["dubbo-admins"]
}
```
fake server 至少支持:
- 正常登录。
- 无效 authorization code。
- state 不匹配。
- UserInfo 缺失或格式错误。
## 测试要求
后端测试应覆盖:
- 未登录访问业务接口返回 401。
- 用户名密码登录仍然可用。
- OIDC login 正确重定向到 Provider。
- OIDC callback 拒绝错误 state。
- OIDC callback 拒绝错误 code。
- OIDC callback 成功后写入 Principal session。
- 登录后访问业务接口成功。
- logout 后 session 失效。
e2e 测试应覆盖:
- 用户从登录页点击 SSO 登录。
- fake OIDC server 自动完成授权回调。
- 前端显示 fake 用户身份。
- 登录后业务 API 请求成功。
- 退出后再次访问业务页面会回到登录页。
## 安全要求
- 必须校验 `state`。
- 必须使用并校验 `nonce`。
- 推荐使用 authorization code flow + PKCE。
- 必须校验 ID Token 的 issuer、audience、signature、expiration。
- `clientSecret` 只能保存在后端配置中。
- OAuth token 不得写入前端 localStorage 或普通 cookie。
- session cookie 应设置 HttpOnly 和 SameSite,HTTPS 场景应设置 Secure。
- OIDC 开启时,生产环境不得使用默认 session secret。
## 兼容性
现有用户名密码登录继续可用,避免破坏已有部署。默认配置可以继续只启用 password。启用 OIDC 时,通过 `methods`
显式打开。第一阶段允许同时启用 password 和 oidc,生产环境是否禁用 password 由部署方通过配置决定。
前端现有 `auth-state` 只应作为 UI 状态缓存,不能作为后端认证凭据。后续应逐步改为通过
`/api/v1/auth/userinfo` 同步当前真实登录态。
## 已确认决策
- OIDC Provider 第一阶段只支持 issuer discovery,不支持手动 endpoint override。
- 第一阶段保留用户名密码登录。
- 第一阶段允许 password 和 oidc 同时启用。
- OAuth2 授权码流程使用 `golang.org/x/oauth2`,不手写协议细节。
- OIDC discovery、ID Token 校验和 claims 解析使用成熟 OIDC 库。
## 开放问题
- `sessionSecret` 的默认值和生产环境校验策略如何定义。
- fake OIDC server 是否需要签发真实 JWT 并提供 JWKS。
- `/admin` 静态资源是否需要登录态保护,还是只保护 `/api/v1` 业务接口。
```
</details>
### Agent token
大致流程:用户登录 Dubbo Admin 后在页面创建一个可过期、可撤销、有 scope 的 MCP Access Token,Dubbo Admin
只保存这个 token 的 hash 和归属用户信息,Agent 调 POST /api/v1/mcp 时通过Authorization: Bearer
<token> 带上它,后端查库校验 token 后把请求映射回该用户并执行 MCP tool。
<details open>
<summary>spec</summary>
```md
# 引入 MCP Agent Token 身份标识
## 变更概述
为 Dubbo Admin 的 MCP 接口引入 Agent Token 认证能力。用户登录 Admin 控制台后,可以在个人页面创建自己的
MCP Access Token,并将 token 配置到 MCP Client 或 Agent 中。Agent 调用 `/api/v1/mcp` 时通过
`Authorization: Bearer <token>` 携带该 token,Dubbo Admin 校验后将请求映射为该 token 所属用户。
该方案的目标是解决 Agent 调用 MCP tools 时的用户身份标识和审计问题,而不是给所有普通 REST API 开放新的认证入口。
## 背景
当前 MCP 文档中,`/api/v1/mcp` 使用和 Admin Console API 相同的 session auth。测试流程是先调用
`/api/v1/auth/login` 获取 session cookie,再带 cookie 调用 MCP endpoint。
这种方式适合本地手动测试,但不适合真实 Agent 场景:
- Agent 不应该长期保存用户密码。
- Agent 不适合依赖浏览器 session cookie。
- Dubbo Admin 需要知道 MCP 调用代表哪个真实用户。
- 用户需要能撤销给 Agent 的访问能力。
- 后续审计需要记录用户、Agent token、MCP method 和 tool name。
因此需要一个面向 MCP endpoint 的用户级 token 机制。
## 目标
- 支持用户在 Admin 控制台创建 MCP Access Token。
- 支持 Agent 调用 `/api/v1/mcp` 时携带 bearer token。
- 服务端根据 token 识别真实用户并构造统一 Principal。
- token 支持过期、撤销、scope 和 last used 信息。
- token 明文只在创建时展示一次。
- 服务端只保存 token hash,不保存明文 token。
- token 数据支持持久化存储,生产环境可使用 MySQL 或 PostgreSQL。
- MCP endpoint 继续兼容现有 session cookie 认证,便于浏览器和本地测试。
## 非目标
- 不为所有 `/api/v1` REST API 开放 Agent Token 认证。
- 不让 Agent 使用用户名密码登录。
- 不实现 OAuth2 Token Exchange 或 On-Behalf-Of flow。
- 不实现 refresh token。
- 不实现完整 RBAC,只做 MCP token scope 校验。
- 不把 token 明文保存到数据库。
## 用户场景
### 创建 Token
1. 用户通过 password 或 OIDC 登录 Dubbo Admin。
2. 用户进入个人页面或安全设置页面。
3. 用户点击“创建 MCP Token”。
4. 用户填写 token 名称、有效期和权限范围。
5. 后端生成随机 token 明文。
6. 后端保存 token hash 和 metadata。
7. 前端只展示一次 token 明文。
8. 用户将 token 配置给 MCP Client 或 Agent。
### Agent 调用 MCP
1. Agent 读取用户配置的 MCP Access Token。
2. Agent 调用 `POST /api/v1/mcp`。
3. 请求头携带 `Authorization: Bearer <mcp-token>`。
4. Dubbo Admin 对 token 做 hash 后查询 token store。
5. 后端校验 token 是否存在、未过期、未撤销、scope 满足 MCP 调用。
6. 后端根据 token owner 构造 Principal。
7. MCP server 执行对应 JSON-RPC method 或 tool call。
8. 后端更新 token 的 last used 信息。
9. 审计日志记录用户身份、token id、MCP method 和 tool name。
示例请求:
```http
POST /api/v1/mcp
Authorization: Bearer dubbo_mcp_<random-secret>
Content-Type: application/json
Accept: application/json, text/event-stream
```
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "dubbo_get_application_detail",
"arguments": {
"mesh": "mock",
"appName": "demo-provider"
}
}
}
```
## Token 数据模型
建议新增 MCP token metadata:
```go
type MCPAgentToken struct {
ID string
TokenHash string
Name string
OwnerSubject string
OwnerName string
Scopes []string
ExpiresAt time.Time
RevokedAt *time.Time
CreatedAt time.Time
LastUsedAt *time.Time
LastUsedIP string
}
```
说明:
- `ID` 用于展示、撤销和审计。
- `TokenHash` 是 token 明文的 hash,不能存明文。
- `OwnerSubject` 对应 Principal 的 `Subject`。
- `OwnerName` 用于页面展示和审计辅助。
- `Scopes` 控制该 token 可访问的 MCP 能力。
- `RevokedAt` 非空时表示 token 已撤销。
- `LastUsedAt` 和 `LastUsedIP` 用于用户识别异常使用。
## Token 格式
token 明文建议带固定前缀:
```text
dubbo_mcp_<random-secret>
```
后端生成时要求:
- 使用加密安全随机数。
- 明文只返回给前端一次。
- 后端保存 `sha256(token)` 或更强 hash 结果。
- 日志中不得输出 token 明文。
## Scope 设计
第一阶段建议只支持少量 scope:
- `mcp:read`:允许调用只读 MCP tools。
- `mcp:write`:预留给未来写操作 MCP tools。
如果当前 MCP tools 都是查询类,创建 token 时默认只授予 `mcp:read`。
MCP tool 注册或调用时应能标识所需 scope。第一阶段如果没有写操作 tool,可以先统一要求 `mcp:read`。
## 存储设计
MCP Agent Token 需要服务端存储。原因是必须支持撤销、过期、owner 映射、scope 校验和 last used 记录。
生产环境应支持持久化存储:
- MySQL
- PostgreSQL
本地开发和测试可以支持 memory store:
- 适合单进程本地调试。
- 服务重启后 token 丢失。
- 不适合生产。
不建议把 token 强行建模为 mesh resource。MCP Agent Token
属于认证安全数据,不是服务发现或治理资源。建议新增独立 store 接口:
```go
type MCPAgentTokenStore interface {
Create(token *MCPAgentToken) error
GetByHash(hash string) (*MCPAgentToken, error)
ListByOwner(ownerSubject string) ([]*MCPAgentToken, error)
Revoke(tokenID string, ownerSubject string) error
UpdateLastUsed(tokenID string, usedAt time.Time, ip string) error
}
```
实现建议:
- `memoryMCPAgentTokenStore`:用于测试和本地开发。
- `dbMCPAgentTokenStore`:复用现有 MySQL/PostgreSQL 连接能力。
## Backend API 变更
新增面向用户的 token 管理接口:
- `POST /api/v1/auth/mcp-tokens`:创建 MCP token。
- `GET /api/v1/auth/mcp-tokens`:列出当前用户的 token metadata。
- `DELETE /api/v1/auth/mcp-tokens/:tokenID`:撤销当前用户的指定 token。
创建接口返回 token 明文一次:
```json
{
"id": "token-123",
"token": "dubbo_mcp_<random-secret>",
"name": "local-agent",
"scopes": ["mcp:read"],
"expiresAt": "2026-08-25T00:00:00Z"
}
```
列表接口不得返回 token 明文,只返回 metadata。
## MCP 认证变更
`/api/v1/mcp` 支持两种认证方式:
- session cookie:保留现有浏览器和本地测试体验。
- bearer token:供 Agent 和 MCP Client 使用。
认证优先级建议:
1. 如果请求带 `Authorization: Bearer`,优先按 MCP Agent Token 校验。
2. 如果没有 bearer token,再回退到现有 session cookie。
3. 两者都没有或都无效时返回 401。
bearer token 校验成功后,构造 Principal:
```go
type Principal struct {
Subject string
Username string
Email string
Groups []string
Roles []string
AuthType string
Provider string
Actor string
Scopes []string
}
```
字段含义:
- `Subject`:token owner 的用户 ID。
- `Username`:token owner 的展示名。
- `AuthType`:`mcp_token`。
- `Provider`:`dubbo-admin`。
- `Actor`:`mcp-token:<tokenID>`。
- `Scopes`:token 持有的 scope。
## 前端变更
新增一个用户级 token 管理入口,位置可以是:
- 顶部用户菜单中的“Agent Tokens”。
- 或个人设置页面中的“MCP Tokens”。
页面能力:
- 创建 token。
- 选择有效期。
- 选择 scope。
- 创建成功后只展示一次 token 明文。
- 列出现有 token metadata。
- 显示 token 是否过期、是否撤销、上次使用时间。
- 支持撤销 token。
前端不得保存 token 明文。用户关闭创建结果弹窗后,无法再次查看明文,只能重新创建。
## 配置变更
建议新增配置:
```yaml
console:
auth:
mcpToken:
enabled: true
defaultExpirationDays: 30
maxExpirationDays: 90
allowedScopes:
- mcp:read
- mcp:write
```
说明:
- `enabled` 控制是否允许用户创建 MCP token。
- `defaultExpirationDays` 是默认有效期。
- `maxExpirationDays` 限制用户可选择的最长有效期。
- `allowedScopes` 限制页面和后端可授予的 scope。
## 测试要求
后端测试应覆盖:
- 登录用户可以创建 MCP token。
- 创建 token 后只返回一次明文。
- token store 只保存 hash。
- 未过期 token 可以访问 `/api/v1/mcp`。
- 过期 token 访问 `/api/v1/mcp` 返回 401。
- 撤销 token 后访问 `/api/v1/mcp` 返回 401。
- 无所需 scope 的 token 访问对应 MCP tool 返回 403。
- session cookie 访问 `/api/v1/mcp` 仍然可用。
- token last used 信息会更新。
e2e 测试应覆盖:
- 用户登录 Admin。
- 用户创建 MCP token。
- 测试用 MCP client 带 token 调用 `tools/list`。
- 测试用 MCP client 带 token 调用只读 tool。
- 用户撤销 token。
- 撤销后的 token 调 MCP 失败。
## 安全要求
- token 明文只展示一次。
- 服务端只保存 hash。
- token 必须有过期时间。
- token 必须可撤销。
- token 不得写入日志。
- token 管理接口必须要求用户已登录。
- 用户只能查看和撤销自己的 token。
- MCP bearer token 只对 `/api/v1/mcp` 生效,不默认扩展到所有 REST API。
- 生产环境必须使用持久化 store,避免多副本和重启导致 token 状态不一致。
## 兼容性
现有 `/api/v1/mcp` session cookie 认证继续保留。现有 MCP 手动测试流程不应被破坏。
未启用 `console.auth.mcpToken.enabled` 时,Dubbo Admin 不展示 token 管理入口,也不接受
MCP Agent Token。
## 已确认决策
- Agent 用户身份标识第一阶段面向 MCP endpoint,不面向所有 REST API。
- 第一阶段采用用户创建 MCP Access Token 的方式。
- Agent 调用 MCP 时使用 `Authorization: Bearer <token>`。
- token 需要服务端存储。
- 生产环境可使用现有 MySQL/PostgreSQL 存储能力。
- 本地开发和测试可以使用 memory store。
- token 明文只展示一次,服务端只保存 hash。
## 开放问题
- MCP token metadata 是否应存入现有 dbcommon/gorm store,还是独立 auth 表。
- 默认 token 有效期和最大有效期分别是多少。
- 第一阶段是否只允许 `mcp:read`,还是同时开放 `mcp:write` 预留。
- MCP tool 与 scope 的映射关系放在 tool 注册处,还是放在独立策略表。
- token last used 更新是否同步写入,还是异步批量更新。
```
</details>
cc @robocanic
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]