- 新增 NANO_OUTLOOK_MCP_URL 和 NANO_OUTLOOK_MCP_SERVER_ID 环境变量配置 - 实现 Outlook 邮件和日历的分页查询功能,添加安全参数验证 - 为 app-instance 创建脚本添加 Outlook MCP 服务器 ID 参数 - 更新前端 Outlook 页面实现邮件列表和日历事件的分页浏览 - 添加 Git 忽略文件配置和 Docker 挂载路径修复 BREAKING CHANGE: Outlook 集成现在需要配置 MCP URL 和服务器 ID 环境变量
640 lines
16 KiB
Markdown
640 lines
16 KiB
Markdown
# Authz-service
|
||
|
||
## 概览
|
||
|
||
这是一个基于 FastAPI 的认证授权服务,当前实现主要提供 4 类能力:
|
||
|
||
1. 后端实例注册与管理
|
||
2. 用户注册时自动绑定/创建后端
|
||
3. 基于 OAuth 2.0 `client_credentials` 的 access token 签发
|
||
4. token 内省、权限配置、Outlook 配置管理
|
||
|
||
文档内容按当前代码实现整理,接口定义来源于 [app/main.py](/home/ivan/xuan/steven_project/Authz-service/app/main.py)。
|
||
|
||
## 鉴权模型
|
||
|
||
### 1. 当前真正支持的登录方式
|
||
|
||
当前服务没有“用户名 + 密码登录”接口。
|
||
|
||
真正用于获取 access token 的方式只有一种:
|
||
|
||
- OAuth 2.0 `client_credentials`
|
||
- 调用接口:`POST /oauth/token`
|
||
- 需要提供:`client_id`、`client_secret`、`aud`
|
||
- 可选提供:`scope`
|
||
|
||
也就是说:
|
||
|
||
- `username` / `password` 只在 `POST /oauth/register` 注册阶段出现
|
||
- 其中 `password` 当前仅做“必填校验”,不会被保存,也不会参与后续登录或鉴权
|
||
|
||
### 2. 内部接口鉴权
|
||
|
||
以下接口要求请求头带内部 Bearer Token:
|
||
|
||
- `GET /internal/backends/{backend_id}/settings/outlook`
|
||
- `POST /oauth/introspect`
|
||
|
||
请求头格式:
|
||
|
||
```http
|
||
Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>
|
||
```
|
||
|
||
服务会将 Bearer Token 与环境变量 `AUTHZ_INTERNAL_TOKEN` 做精确匹配。
|
||
|
||
### 3. Token 签名与校验
|
||
|
||
- 签名算法:`RS256`
|
||
- JWKS 地址:`GET /.well-known/jwks.json`
|
||
- OAuth 元数据地址:`GET /.well-known/oauth-authorization-server`
|
||
- access token 默认有效期:`3600` 秒,可通过环境变量 `AUTHZ_ACCESS_TOKEN_TTL_SECONDS` 调整
|
||
|
||
## 基础信息
|
||
|
||
- 默认 `issuer`:`http://127.0.0.1:19090`
|
||
- 默认 `token_endpoint`:`/oauth/token`
|
||
- 默认 `introspection_endpoint`:`/oauth/introspect`
|
||
- 默认只声明支持 `client_secret_post`
|
||
|
||
如果服务已启动,FastAPI 默认也会提供:
|
||
|
||
- `GET /docs`
|
||
- `GET /openapi.json`
|
||
|
||
## 典型流程
|
||
|
||
### 流程 A:注册用户并自动创建后端
|
||
|
||
1. 调用 `POST /oauth/register`
|
||
2. 服务创建或更新 backend
|
||
3. 服务返回 `backend_id`、`client_id`
|
||
4. 如果 backend 是首次创建,还会返回一次性的 `client_secret`
|
||
5. 客户端使用 `client_id + client_secret + aud` 调用 `POST /oauth/token` 换取 access token
|
||
|
||
### 流程 B:单独注册 backend
|
||
|
||
1. 调用 `POST /backends/register`
|
||
2. 记录返回的 `client_id` 和 `client_secret`
|
||
3. 配置 `POST /backends/{backend_id}/permissions`
|
||
4. 用 `POST /oauth/token` 获取 token
|
||
|
||
### 流程 C:由 Auth Portal 发起的一站式注册
|
||
|
||
1. Auth Portal 调用 `POST /portal/register`
|
||
2. AuthZ 先调用 deploy-control 创建或解析实例
|
||
3. AuthZ 再调用实例自己的 `POST /api/auth/register`
|
||
4. 实例在注册过程中回调 AuthZ 的 `/oauth/register` / `/backends/register`
|
||
5. AuthZ 将最终 token 和 backend 连接信息回传给 Auth Portal
|
||
|
||
## 注册时需要提供什么信息
|
||
|
||
### 用户注册接口:`POST /oauth/register`
|
||
|
||
请求体字段如下:
|
||
|
||
| 字段 | 是否必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `username` | 是 | `string` | 用户名,去掉首尾空格后不能为空 |
|
||
| `password` | 是 | `string` | 当前仅用于必填校验,不会被持久化,也不会用于后续登录 |
|
||
| `email` | 否 | `string` | 用户邮箱 |
|
||
| `name` | 否 | `string` | backend 名称的候选值之一 |
|
||
| `backend_name` | 否 | `string` | backend 名称 |
|
||
| `backend_id` | 否 | `string` | 指定 backend 唯一标识;不传则自动生成 |
|
||
| `base_url` | 否 | `string` | backend 服务地址 |
|
||
| `public_base_url` | 否 | `string` | `base_url` 的候选字段之一 |
|
||
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
|
||
| `backend` | 否 | `object` | 嵌套 backend 配置,优先级高于同名顶层字段 |
|
||
|
||
其中真正影响 backend 创建/更新的有效字段有:
|
||
|
||
- backend 名称:按以下优先级取值
|
||
`backend.name` -> `backend_name` -> `name` -> `username`
|
||
- backend id:按以下优先级取值
|
||
`backend.backend_id` -> `backend_id` -> 自动生成
|
||
- backend 服务地址:按以下优先级取值
|
||
`backend.base_url` -> `public_base_url` -> `base_url`
|
||
- backend 前端地址:按以下优先级取值
|
||
`backend.frontend_base_url` -> `frontend_base_url`
|
||
|
||
`base_url` 最终必须能解析出有效值,否则会返回 `400 base_url is required`。
|
||
|
||
#### 推荐最小请求体
|
||
|
||
```json
|
||
{
|
||
"username": "alice",
|
||
"password": "any-non-empty-value",
|
||
"email": "alice@example.com",
|
||
"backend_name": "Alice Workspace",
|
||
"base_url": "https://api.example.com",
|
||
"frontend_base_url": "https://app.example.com"
|
||
}
|
||
```
|
||
|
||
#### 使用嵌套 backend 的请求体
|
||
|
||
```json
|
||
{
|
||
"username": "alice",
|
||
"password": "any-non-empty-value",
|
||
"email": "alice@example.com",
|
||
"backend": {
|
||
"name": "Alice Workspace",
|
||
"backend_id": "alice-workspace",
|
||
"base_url": "https://api.example.com",
|
||
"frontend_base_url": "https://app.example.com"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 成功响应
|
||
|
||
```json
|
||
{
|
||
"user": {
|
||
"username": "alice",
|
||
"email": "alice@example.com",
|
||
"default_backend_id": "alice-workspace",
|
||
"created_at": "2026-03-13T08:00:00+00:00",
|
||
"updated_at": "2026-03-13T08:00:00+00:00"
|
||
},
|
||
"backend": {
|
||
"backend_id": "alice-workspace",
|
||
"client_id": "alice-workspace",
|
||
"client_secret": "generated-secret-only-when-created",
|
||
"name": "Alice Workspace",
|
||
"base_url": "https://api.example.com",
|
||
"frontend_base_url": "https://app.example.com",
|
||
"status": "active",
|
||
"created_at": "2026-03-13T08:00:00+00:00"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 特别说明
|
||
|
||
- 如果 `backend_id` 不存在,服务会新建 backend,并返回新的 `client_secret`
|
||
- 如果 `backend_id` 已存在,服务会更新该 backend 信息,但 `client_secret` 会返回 `null`
|
||
- 用户数据当前只保存:`username`、`email`、`default_backend_id`
|
||
- 当前实现没有用户密码校验、密码存储、密码登录、刷新 token
|
||
|
||
### 后端注册接口:`POST /backends/register`
|
||
|
||
请求体字段如下:
|
||
|
||
| 字段 | 是否必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `name` | 是 | `string` | backend 名称 |
|
||
| `base_url` | 是 | `string` | backend 服务地址 |
|
||
| `backend_id` | 否 | `string` | 指定 backend id;不传则按 `name` 自动生成 |
|
||
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
|
||
|
||
示例:
|
||
|
||
```json
|
||
{
|
||
"name": "Local Backend",
|
||
"base_url": "https://api.example.com",
|
||
"backend_id": "local-backend",
|
||
"frontend_base_url": "https://app.example.com"
|
||
}
|
||
```
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"backend_id": "local-backend",
|
||
"client_id": "local-backend",
|
||
"client_secret": "generated-secret",
|
||
"created_at": "2026-03-13T08:00:00+00:00",
|
||
"frontend_base_url": "https://app.example.com"
|
||
}
|
||
```
|
||
|
||
## 登录/鉴权时需要什么信息
|
||
|
||
### 换取 access token:`POST /oauth/token`
|
||
|
||
这是当前服务唯一的“登录/鉴权入口”。
|
||
|
||
支持两种提交方式:
|
||
|
||
- `application/x-www-form-urlencoded`
|
||
- `application/json`
|
||
|
||
请求字段如下:
|
||
|
||
| 字段 | 是否必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `grant_type` | 否 | `string` | 仅支持 `client_credentials`,默认值也是这个 |
|
||
| `client_id` | 是 | `string` | 注册 backend 后得到,默认等于 `backend_id` |
|
||
| `client_secret` | 是 | `string` | 注册或轮转密钥时得到 |
|
||
| `aud` | 是 | `string` | 目标 audience,决定可申请哪些 scope |
|
||
| `scope` / `scopes` | 否 | `string` / `string[]` | 需要的权限范围;不传则自动下发该 audience 下允许的全部 scope |
|
||
|
||
说明:
|
||
|
||
- 表单模式下使用字段 `scope`,多个 scope 用空格分隔
|
||
- JSON 模式下使用字段 `scopes`,类型为字符串数组
|
||
- JSON 模式下也可以把 `aud` 换成 `resource` 吗:不可以。`resource` 只在表单解析分支里作为 `aud` 的兜底字段
|
||
|
||
#### 表单请求示例
|
||
|
||
```bash
|
||
curl -X POST http://127.0.0.1:19090/oauth/token \
|
||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||
-d "grant_type=client_credentials" \
|
||
-d "client_id=local-backend" \
|
||
-d "client_secret=<client-secret>" \
|
||
-d "aud=mcp:outlook" \
|
||
-d "scope=list_tools tool:mail_list_messages"
|
||
```
|
||
|
||
#### JSON 请求示例
|
||
|
||
```json
|
||
{
|
||
"grant_type": "client_credentials",
|
||
"client_id": "local-backend",
|
||
"client_secret": "<client-secret>",
|
||
"aud": "mcp:outlook",
|
||
"scopes": [
|
||
"list_tools",
|
||
"tool:mail_list_messages"
|
||
]
|
||
}
|
||
```
|
||
|
||
#### 成功响应
|
||
|
||
```json
|
||
{
|
||
"access_token": "<jwt>",
|
||
"token_type": "bearer",
|
||
"expires_in": 3600
|
||
}
|
||
```
|
||
|
||
### `aud` 和 `scope` 的校验规则
|
||
|
||
当前代码只支持两类 audience:
|
||
|
||
#### 1. MCP audience
|
||
|
||
格式:
|
||
|
||
```text
|
||
mcp:<server_id>
|
||
```
|
||
|
||
例如:
|
||
|
||
```text
|
||
mcp:outlook
|
||
```
|
||
|
||
只有当权限配置满足以下条件时才能签发 token:
|
||
|
||
```json
|
||
{
|
||
"mcp": {
|
||
"outlook": {
|
||
"enabled": true,
|
||
"tools": [
|
||
"mail_list_messages",
|
||
"mail_send_email"
|
||
]
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
这时允许的 scope 会自动变成:
|
||
|
||
- `list_tools`
|
||
- `tool:mail_list_messages`
|
||
- `tool:mail_send_email`
|
||
|
||
#### 2. A2A audience
|
||
|
||
格式:
|
||
|
||
```text
|
||
a2a:<agent_id>
|
||
```
|
||
|
||
例如:
|
||
|
||
```text
|
||
a2a:planner
|
||
```
|
||
|
||
只有当权限配置满足以下条件时才能签发 token:
|
||
|
||
```json
|
||
{
|
||
"a2a": {
|
||
"enabled": true,
|
||
"agents": [
|
||
"planner"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
这时允许的 scope 只有:
|
||
|
||
- `run_task`
|
||
|
||
#### 3. 请求 scope 的规则
|
||
|
||
- 如果不传 `scope/scopes`,服务会返回该 audience 下允许的全部 scope
|
||
- 如果传了 `scope/scopes`,必须是允许 scope 的子集,否则返回 `403 Requested scopes exceed backend permissions`
|
||
- 如果 audience 未启用,返回 `403 Audience is not enabled for this backend`
|
||
- 当前默认开启 `AUTHZ_MCP_PERMISSIVE_DEFAULT=1`
|
||
- 对 `mcp:*` audience,会优先放行本次请求里声明的 scopes,并自动补 `list_tools`
|
||
- 如果你后面要改回严格模式,把这个环境变量设成 `0`
|
||
|
||
### Token 内省:`POST /oauth/introspect`
|
||
|
||
此接口是内部接口,必须带内部 Bearer Token。
|
||
|
||
请求头:
|
||
|
||
```http
|
||
Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>
|
||
```
|
||
|
||
请求体:
|
||
|
||
```json
|
||
{
|
||
"token": "<access-token>"
|
||
}
|
||
```
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"active": true,
|
||
"client_id": "local-backend",
|
||
"backend_id": "local-backend",
|
||
"aud": "mcp:outlook",
|
||
"scp": [
|
||
"list_tools",
|
||
"tool:mail_list_messages"
|
||
],
|
||
"exp": 1773392400
|
||
}
|
||
```
|
||
|
||
token 无效时返回:
|
||
|
||
```json
|
||
{
|
||
"active": false
|
||
}
|
||
```
|
||
|
||
## 接口清单
|
||
|
||
### 公开接口
|
||
|
||
| 方法 | 路径 | 鉴权 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `GET` | `/healthz` | 无 | 健康检查 |
|
||
| `GET` | `/.well-known/oauth-authorization-server` | 无 | OAuth 元数据 |
|
||
| `GET` | `/.well-known/jwks.json` | 无 | JWKS 公钥 |
|
||
| `POST` | `/backends/register` | 无 | 注册 backend |
|
||
| `GET` | `/backends` | 无 | 查询全部 backend |
|
||
| `GET` | `/backends/{backend_id}` | 无 | 查询单个 backend |
|
||
| `PUT` | `/backends/{backend_id}` | 无 | 更新 backend |
|
||
| `POST` | `/backends/{backend_id}/disable` | 无 | 禁用 backend |
|
||
| `POST` | `/backends/{backend_id}/enable` | 无 | 启用 backend |
|
||
| `POST` | `/backends/{backend_id}/rotate-secret` | 无 | 轮转 client secret |
|
||
| `GET` | `/backends/{backend_id}/permissions` | 无 | 查询权限配置 |
|
||
| `POST` | `/backends/{backend_id}/permissions` | 无 | 保存权限配置 |
|
||
| `GET` | `/backends/{backend_id}/settings/outlook` | 无 | 查询 Outlook 配置,密码会被隐藏 |
|
||
| `POST` | `/backends/{backend_id}/settings/outlook` | 无 | 保存 Outlook 配置 |
|
||
| `DELETE` | `/backends/{backend_id}/settings/outlook` | 无 | 删除 Outlook 配置 |
|
||
| `POST` | `/oauth/register` | 无 | 用户注册并绑定 backend |
|
||
| `POST` | `/oauth/token` | 无 | 使用 client credentials 获取 token |
|
||
|
||
### 内部接口
|
||
|
||
| 方法 | 路径 | 鉴权 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `GET` | `/internal/backends/{backend_id}/settings/outlook` | `Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>` | 查询完整 Outlook 配置,包含密码 |
|
||
| `POST` | `/oauth/introspect` | `Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>` | 内省 token |
|
||
|
||
## 详细接口说明
|
||
|
||
### 1. `GET /healthz`
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"status": "ok"
|
||
}
|
||
```
|
||
|
||
### 2. `GET /.well-known/oauth-authorization-server`
|
||
|
||
响应示例:
|
||
|
||
```json
|
||
{
|
||
"issuer": "http://127.0.0.1:19090",
|
||
"token_endpoint": "http://127.0.0.1:19090/oauth/token",
|
||
"introspection_endpoint": "http://127.0.0.1:19090/oauth/introspect",
|
||
"jwks_uri": "http://127.0.0.1:19090/.well-known/jwks.json",
|
||
"grant_types_supported": [
|
||
"client_credentials"
|
||
],
|
||
"token_endpoint_auth_methods_supported": [
|
||
"client_secret_post"
|
||
]
|
||
}
|
||
```
|
||
|
||
### 3. `GET /.well-known/jwks.json`
|
||
|
||
返回 RSA 公钥集合,用于校验 `RS256` access token。
|
||
|
||
### 4. `GET /backends`
|
||
|
||
返回全部 backend 列表。
|
||
|
||
### 5. `GET /backends/{backend_id}`
|
||
|
||
- backend 不存在时返回 `404 Backend not found`
|
||
|
||
### 6. `PUT /backends/{backend_id}`
|
||
|
||
请求体字段:
|
||
|
||
| 字段 | 是否必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `name` | 否 | `string` | backend 名称 |
|
||
| `base_url` | 否 | `string` | backend 服务地址 |
|
||
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
|
||
|
||
说明:
|
||
|
||
- 只有非空字段才会更新
|
||
- 当前实现不支持把已有字段显式清空为 `null`
|
||
|
||
### 7. `POST /backends/{backend_id}/disable`
|
||
|
||
将 backend 状态改为 `disabled`。
|
||
|
||
### 8. `POST /backends/{backend_id}/enable`
|
||
|
||
将 backend 状态改为 `active`。
|
||
|
||
### 9. `POST /backends/{backend_id}/rotate-secret`
|
||
|
||
返回新的 `client_secret`,并覆盖旧的凭证。
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"backend_id": "local-backend",
|
||
"client_id": "local-backend",
|
||
"client_secret": "new-generated-secret",
|
||
"rotated_at": "2026-03-13T08:00:00+00:00"
|
||
}
|
||
```
|
||
|
||
### 10. `GET /backends/{backend_id}/permissions`
|
||
|
||
返回指定 backend 的权限配置;若从未配置,返回空对象 `{}`。
|
||
|
||
### 11. `POST /backends/{backend_id}/permissions`
|
||
|
||
请求体是任意 JSON 对象,当前实际生效的结构建议如下:
|
||
|
||
```json
|
||
{
|
||
"mcp": {
|
||
"outlook": {
|
||
"enabled": true,
|
||
"tools": [
|
||
"mail_list_messages",
|
||
"mail_send_email"
|
||
]
|
||
}
|
||
},
|
||
"a2a": {
|
||
"enabled": true,
|
||
"agents": [
|
||
"planner"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
服务会原样保存该 JSON。
|
||
|
||
### 12. `GET /backends/{backend_id}/settings/outlook`
|
||
|
||
返回 Outlook 配置,但不会暴露明文密码。
|
||
|
||
未配置时响应:
|
||
|
||
```json
|
||
{
|
||
"configured": false
|
||
}
|
||
```
|
||
|
||
已配置时响应示例:
|
||
|
||
```json
|
||
{
|
||
"configured": true,
|
||
"email": "user@example.com",
|
||
"username": "user",
|
||
"domain": "example.com",
|
||
"service_endpoint": null,
|
||
"server": "mail.example.com",
|
||
"autodiscover": false,
|
||
"default_timezone": "Asia/Shanghai",
|
||
"updated_at": "2026-03-13T08:00:00+00:00",
|
||
"password_masked": true
|
||
}
|
||
```
|
||
|
||
### 13. `POST /backends/{backend_id}/settings/outlook`
|
||
|
||
请求体字段:
|
||
|
||
| 字段 | 是否必填 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `configured` | 否 | `boolean` | 默认 `true`,一般传 `true` |
|
||
| `email` | 是 | `string` | 邮箱地址 |
|
||
| `username` | 是 | `string` | Outlook 用户名 |
|
||
| `domain` | 否 | `string` | 域名 |
|
||
| `service_endpoint` | 否 | `string` | 服务地址 |
|
||
| `server` | 否 | `string` | 邮件服务器 |
|
||
| `autodiscover` | 否 | `boolean` | 是否自动发现 |
|
||
| `default_timezone` | 否 | `string` | 默认时区,默认 `Asia/Shanghai` |
|
||
| `password` | 是,但更新时可空 | `string` | 初次配置必须传;后续更新可传空字符串表示沿用旧密码 |
|
||
|
||
初次配置时如果没有密码,会返回:
|
||
|
||
```json
|
||
{
|
||
"detail": "Password is required for initial Outlook setup"
|
||
}
|
||
```
|
||
|
||
### 14. `DELETE /backends/{backend_id}/settings/outlook`
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"ok": true
|
||
}
|
||
```
|
||
|
||
### 15. `GET /internal/backends/{backend_id}/settings/outlook`
|
||
|
||
内部接口,返回完整 Outlook 配置,包含明文 `password`。
|
||
|
||
### 16. `POST /oauth/register`
|
||
|
||
见上文“注册时需要提供什么信息”。
|
||
|
||
### 17. `POST /oauth/token`
|
||
|
||
见上文“登录/鉴权时需要什么信息”。
|
||
|
||
### 18. `POST /oauth/introspect`
|
||
|
||
见上文“Token 内省”。
|
||
|
||
## 常见错误码
|
||
|
||
| HTTP 状态码 | 场景 |
|
||
| --- | --- |
|
||
| `400` | 缺少 `username`、`password`、`base_url`,或 `grant_type` 不支持 |
|
||
| `401` | `client_id/client_secret` 无效,或内部 Bearer Token 无效 |
|
||
| `403` | backend 被禁用、audience 未启用、请求 scope 超出权限 |
|
||
| `404` | backend 不存在,或内部 Outlook 配置不存在 |
|
||
| `409` | 注册 backend 时 `backend_id` 已存在 |
|
||
|
||
## 当前实现限制
|
||
|
||
1. `/oauth/register` 的 `password` 目前不会被保存,也不会用于认证。
|
||
2. 没有用户名密码登录接口,只有 `client_credentials`。
|
||
3. 没有 refresh token。
|
||
4. 公开的 backend 管理接口当前未做访问控制,调用方需自行在网关或网络层保护。
|