# 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 管理接口当前未做访问控制，调用方需自行在网关或网络层保护。
