# 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 ``` 服务会将 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=" \ -d "aud=mcp:outlook" \ -d "scope=list_tools tool:mail_list_messages" ``` #### JSON 请求示例 ```json { "grant_type": "client_credentials", "client_id": "local-backend", "client_secret": "", "aud": "mcp:outlook", "scopes": [ "list_tools", "tool:mail_list_messages" ] } ``` #### 成功响应 ```json { "access_token": "", "token_type": "bearer", "expires_in": 3600 } ``` ### `aud` 和 `scope` 的校验规则 当前代码只支持两类 audience: #### 1. MCP audience 格式: ```text mcp: ``` 例如: ```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: ``` 例如: ```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 ``` 请求体: ```json { "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 ` | 查询完整 Outlook 配置,包含密码 | | `POST` | `/oauth/introspect` | `Authorization: Bearer ` | 内省 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 管理接口当前未做访问控制,调用方需自行在网关或网络层保护。