# AuthMethod 配置指引

本文档面向集成 Turbo Agent Auth 服务的上游系统，讲解如何创建、更新与消费授权方式 (`AuthMethod`)，并落实到用户交互表单与两步授权流程的操作规范。

## 1. 授权方式创建 / 更新

创建接口：`POST /v1/auth-methods`

更新接口：`PATCH /v1/auth-methods/{auth_method_id}`

> 前置检查：先调用 `GET /v1/platforms` 或 `GET /v1/platforms/{platform_id}` 确认目标平台已在系统中创建，再进行授权方式创建或更新。

负载结构沿用 `AuthMethodCreate`/`AuthMethodUpdate` 模型，关键字段说明如下：

- `id`：唯一标识，建议使用业务端生成的可追溯 ID。更新时不需再次提交。
- `platformId`：所属平台 ID，需先通过 `/v1/platforms` 创建并在此引用。
- `name`：授权方式名称，前端列表展示与搜索使用。
- `type`：授权类型（枚举：`OAuth1`、`OAuth2`、`JWT`、`APIKey`、`Cookies`、`SMTP`、`POP3` 等）。驱动默认模板与后续解析逻辑。
- `description`：呈现给运营/实施侧的说明文字，引导如何准备凭证。
- `loginFlow`：定义“向平台发起登录”的请求模板。支持两种形态：
	1) 单步（如微信）：直接提供 `url/method/headers/query/body_template`；
	2) 多步（如 Superset）：提供 `steps: []`，每步可以定义自身请求模板、`responseMapping` 与 `requestPlacements`（把上一步解析结果注入下一步请求）。
	单步结构：
	```json
	{
		"url": "https://api.x.com/oauth/request_token",
		"method": "POST",
		"headers": {"Content-Type": "application/json"},
		"body_template": {
			"consumer_key": "{{consumer_key}}",
			"consumer_secret": "{{consumer_secret}}"
		}
	}
	```
	上例表示：用 `POST` 调用指定 URL，Body 中的 `consumer_key`、`consumer_secret` 来自用户在登录表单里填写的值。
	> **Content-Type 说明**：
	> - 若 `Content-Type` 为 `application/json`，`body_template`（字典）会被序列化为 JSON。
	> - 若 `Content-Type` 为 `application/x-www-form-urlencoded`，`body_template`（字典）会被序列化为表单数据。
	> - 若 `Content-Type` 包含 `multipart/form-data`，`body_template`（字典）会被序列化为 multipart 格式，且系统会自动处理 boundary（无需在 header 中硬编码 boundary）。
	多步结构（精简示意）：
	```json
	{
		"steps": [
			{
				"url": "http://<base>/login/",
				"method": "POST",
				"headers": {"Content-Type": "application/x-www-form-urlencoded"},
				"body_template": {"username": "{{username}}", "password": "{{password}}"},
				"responseMapping": {
					"cookies": {"collectAll": true, "targetField": "cookies", "fields": {"session": "session"}},
					"html": {"fields": {"csrf_token": {"selector": {"tag": "input", "attrs": {"id": "csrf_token"}}, "attribute": "value"}}}
				}
			},
			{
				"url": "http://<base>/login/",
				"method": "POST",
				"headers": {"Content-Type": "application/x-www-form-urlencoded"},
				"allowRedirects": false,
				"requestPlacements": {
					"cookies": {"session": "cookies.session"},
					"body": {"username": "username", "password": "password", "csrf_token": "csrf_token"}
				}
			}
		]
	}
	```
	- 第一步解析出 `cookies.session` 与 `csrf_token`；
	- 第二步用 `requestPlacements` 将这些值放进 Cookie 与表单字段。
- `loginFieldsSchema`：告诉前端“登录表单需要哪些字段”。例如：
	```json
	{
		"type": "object",
		"required": ["consumer_key", "consumer_secret"],
		"properties": {
			"consumer_key": {"type": "string", "title": "Consumer Key"},
			"consumer_secret": {"type": "string", "title": "Consumer Secret"}
		}
	}
	```
	前端读取到这份 Schema 后，就能渲染两个输入框，并在提交 Secret 时把用户输入放进 `loginPayload`。
	对于 OAuth1/OAuth2 类型，`callback_url`/`redirect_uri` 字段由服务端根据环境变量自动注入，请保持只读，不要让用户填写或上传自定义回调地址。
- `refreshFlow`：与 `loginFlow` 同理，只是用于“凭证快过期时自动刷新”。结构一致，往往会把 `refresh_token`、`client_secret` 等插入模板。
- `refreshFieldsSchema`：刷新流程要用到哪些字段。示例：
	```json
	{
		"type": "object",
		"required": ["refresh_token"],
		"properties": {
			"refresh_token": {"type": "string", "title": "Refresh Token"}
		}
	}
	```
	这些字段通常保存在 `Secret.data` 里，刷新时由服务端从中取出。
- `authFieldsSchema`：最终凭证的结构，决定 `Secret.data` 里必须有哪些键。例子：
	```json
	{
		"type": "object",
		"required": ["access_token", "access_token_secret"],
		"properties": {
			"access_token": {"type": "string", "title": "Access Token"},
			"access_token_secret": {"type": "string", "title": "Access Token Secret"},
			"expires_at": {"type": ["integer", "null"], "title": "过期时间戳"}
		}
	}
	```
	用户如果直接手动填写凭证，就按照这个 Schema 提交；自动登录得到的结果也会写成这种结构。
- `authFieldPlacements`：指明 Secret 中的字段（无论是人工录入还是登录阶段解析写入）在“业务请求”时应如何注入。这个映射只影响 Secret 已生效的最终请求，不参与登录/刷新阶段。支持的顶层键是固定枚举：
	- `headers`：HTTP 头，生成如 `Authorization: Bearer ...`；
	- `query`：URL 查询参数；
	- `cookies`：请求 Cookie；
	- `body`：请求体额外字段（表单或 JSON）；
	- `metadata`：非直接发送的附加信息（如默认过期字段、token 类型、OAuth1/OAuth2 所需秘钥信息）。
	典型配置如下：
	```json
	{
		"headers": {
			"Authorization": {
				"type": "Bearer",
				"tokenField": "access_token"
			}
		},
		"query": {
			"oauth_token": {
				"sourceField": "oauth_token"
			}
		}
	}
	```
	上例说明：发送请求时在 Header 加上 `Authorization: Bearer <access_token>`，同时把 `oauth_token` 放进查询字符串。这样 Secret 中的最终凭证（来自手动录入或 `responseMapping` 自动生成）可被直接注入后续业务调用。
	Superset Cookies 场景中，登录阶段由 `responseMapping.cookies` 写入 `Secret.data.cookies.session`，随后可在 `authFieldPlacements` 中声明：
	```json
	"cookies": {
	  "session": {
	    "sourceField": "cookies.session"
	  }
	}
	```
	这表示服务端在执行真实业务请求时，会带上 `session=<value>` 的 Cookie，实现“第一阶段登录 → 第二阶段调用”之间的衔接。

OAuth1 特殊说明：
- OAuth1 需要对每个请求进行签名，不能仅通过静态 header 注入完成。建议在 `authFieldPlacements.metadata.oauth1` 中声明客户端应如何从 `Secret.data` 取字段以构建会话：
```json
{
	"metadata": {
		"oauth1": {
			"consumerKeyField": "consumer_key",
			"consumerSecretField": "consumer_secret",
			"tokenField": "oauth_token",
			"tokenSecretField": "oauth_token_secret",
			"signatureMethod": "HMAC-SHA1"
		}
	}
}
```
客户端（Python 使用 `requests-oauthlib.OAuth1Session`，或 JS 使用 `oauth-1.0a`）在查询 Secret 时即可获得该 `metadata`，并据此从 `Secret.data` 取 `consumer_key/consumer_secret/oauth_token/oauth_token_secret` 组装会话并签名请求。详见 `docs/x_example.md`。
- `responseMapping`：定义“从平台响应里把哪些字段读出来”。自 v8 起统一采用分区配置，支持同时解析 JSON、Headers、Cookies、HTML 文本。常见结构：
	```json
	{
	  "body": {
	    "fields": {
	      "access_token": "data.access_token",
	      "refresh_token": {
	        "path": "data.refresh",
	        "source": "body.custom"
	      }
	    }
	  },
	  "headers": {
	    "fields": {
	      "trace_id": {
	        "name": "x-request-id",
	        "index": 0
	      }
	    }
	  },
	  "cookies": {
	    "collectAll": true,
	    "targetField": "cookies",
	    "fields": {
	      "session": "session"
	    }
	  },
	  "html": {
	    "fields": {
	      "csrf_token": {
	        "selector": {
	          "tag": "input",
	          "attrs": {"id": "csrf_token"}
	        },
	        "attribute": "value"
	      }
	    }
	  }
	}
	```
	说明：
	- `body.fields` 使用 JSON Path（`.` 分隔）读取响应体；也可改写为 `{ "path": "xxx", "source": "body.xxx" }` 自定义来源标记。
	- `headers.fields` 通过 `name`（大小写不敏感）获取响应头，可指定 `index` 选择同名多值中的第几项。
	- `cookies.collectAll`/`targetField` 会将响应 Cookie 与 `Set-Cookie` 合并后整体写入指定字段，同时保留来源；`fields` 可声明必须存在的 cookie。
	- `html.fields` 支持两类写法：`selector` + `attribute/text` 从 HTML 节点抓取，或 `{ "regex": "pattern", "group": 1 }` 用正则获取。
	- 顶层仍兼容旧版 `fields`/`source` 简写，但推荐统一迁移到上述分区写法，避免歧义。
  - 在多步登录中，也可以在“步骤级别”配置 `responseMapping`，其解析结果会作为上下文参与后续步骤的模板渲染与 `requestPlacements` 注入。

### 1.0 登录阶段请求注入（requestPlacements）

在多步登录（`loginFlow.steps`）中，使用 `requestPlacements` 指定“如何把上一步的解析结果或登录表单值，注入到当前步骤的请求”。支持位置：

- `headers`：注入到 HTTP Header；
- `query`：注入到 URL 查询参数；
- `cookies`：注入到请求 Cookie；
- `body`：注入到请求体（自动根据 `Content-Type` 写入 JSON 或表单）。

取值来源与写法：

- 直接路径（推荐）：`"session": "cookies.session"` 表示从上下文中读取 `cookies.session`；
- 对象写法：`{"sourceField": "path", "format": "prefix={{value}}"}` 用于简单格式化拼接；
- 也可 `{"value": "literal"}` 指定字面量。

上下文构成：

- 首次来自 `loginPayload`（前端按 `loginFieldsSchema` 采集）并注入默认 `runtime.callback_url`、`runtime.ip_allow_list`；
- 每一步执行后，如配置 `responseMapping`，解析结果会 merge 回上下文；
- 下一步的 `body_template`（或 `requestPlacements`）即可引用这些字段。
- `defaultValiditySeconds` / `refreshBeforeSeconds`：默认有效期及刷新提前量；在未从响应获取到到期时间时提供兜底逻辑。

### 1.1 前端引导要点

1. **基础信息收集**：`name`、`description` 直接呈现文本输入框，由管理员填写。
2. **流程配置**：若需要自动化登录/刷新，应提供可视化模板编辑（URL、HTTP 方法、Header/Query/Body 模板）。可以使用键值表格并支持 `{{placeholder}}` 占位符提示。
3. **Schema 设计**：
	- Schema 是 JSON Schema Draft 7 兼容结构，建议提供可视化字段编辑器（字段名、类型、提示、是否必填）。
	- `loginFieldsSchema` 决定登录界面字段；`authFieldsSchema` 决定直接提交凭证的字段；`refreshFieldsSchema` 决定后台刷新时需要保存的字段。
4. **字段作用提示**：
	- `loginFieldsSchema` 生成的字段在首次登录时提交给 `/secret` Upsert 接口中的 `loginPayload`；
	- `authFieldsSchema` 生成的字段直接保存到 `Secret.data`，随后根据 `authFieldPlacements` 被注入到请求；
	- `responseMapping` 指定自动登录或解析 API 在响应中把哪些值写回 `Secret.data`。
5. **默认模板引用**：若前端不确定如何填写，可调用 `/v1/auth-types/default-schemas/{type}` 获取参考样例并展示给用户（见“3. 参考样例”）。

### 1.2 更新策略

`PATCH /v1/auth-methods/{id}` 支持部分字段更新。注意：当 Schema 结构发生变更时，应同时评估已有 Secret 是否需要迁移或重新验证。

## 2. 授权方式检索

- 按平台列出：`GET /v1/platforms/{platform_id}/auth-methods`
- 获取详情：`GET /v1/auth-methods/{auth_method_id}`

返回模型为 `AuthMethodOut`，包含创建时提交的所有配置。上游服务可据此动态构建表单或配置页面。

## 2.1 平台管理快速参考

- 列出全部平台：`GET /v1/platforms`
- 创建平台：`POST /v1/platforms`
- 获取平台详情：`GET /v1/platforms/{platform_id}`
- 更新平台：`PATCH /v1/platforms/{platform_id}`

> 创建/更新 AuthMethod 前务必确认目标平台已存在；若平台尚未创建，请先调用平台接口完成创建。

## 3. 获取参考样例

为了减少重复配置，系统提供默认模板：

- 列出所有模板：`GET /v1/auth-types/default-schemas`
- 按类型获取：`GET /v1/auth-types/default-schemas/{auth_type}`

返回示例包含 `description`、`loginFlow`、`loginFieldsSchema`、`authFieldsSchema` 等字段。前端可将其作为初始值或对照文档展示给用户。

## 4. 基于 AuthMethod 创建表单

### 4.1 单步授权（直接提交凭证）

1. 调用 `GET /v1/auth-methods/{id}` 获取 `authFieldsSchema` 与 `authFieldPlacements`。
2. 根据 `authFieldsSchema` 渲染表单，提示用户准备所需字段。
3. 用户提交后，调用 `POST /v1/auth-methods/{id}/secret`，在 payload 的 `data` 中携带与 Schema 对应的键值。若不涉及自动登录，可省略 `loginPayload`。
4. 服务端会校验 Schema，保存 `Secret.data`，并记录 `authDataSources`（来源为 `manual-input`）。后续根据 `authFieldPlacements` 自动注入请求头/查询参数。

### 4.2 两步授权（先登录再保存凭证）

两步授权通常用于 OAuth1/OAuth2 等需要先换取临时 token、再获得最终凭证的流程。

1. 调用 `GET /v1/auth-methods/{id}`，读取 `loginFieldsSchema` 与 `authFieldsSchema`。
2. **第一步表单**：根据 `loginFieldsSchema` 渲染输入项（如 `consumer_key`、`consumer_secret`、`callback_url`）。
3. 用户提交后，可选择：
	- 直接保存到 `Secret.loginPayload`（`POST /secret` 时 `loginPayload` 字段），以备后台自动登录使用；
	- 或立即执行登录流程：调用 `POST /v1/auth-methods/{id}/secret` 同时启用 `autoLoginEnabled` 或随后调用 `/v1/secrets/{secret_id}/login`（如果实现了登录触发 API），服务器会使用 `loginFlow` 描述的模板去获取真实凭证。
4. 登录成功后，依据 `responseMapping` 和 `authFieldPlacements` 写回最终凭证。若流程需要人工补充凭证，可以再展示第二个表单：
	- 表单字段来自 `authFieldsSchema`，允许用户手动填写最终 token；
	- 提交时将数据放入 `payload.data`，系统校验通过后保存到 `Secret.data`。
5. 如需刷新凭证，根据 `refreshFieldsSchema` 再渲染刷新表单或在后台维护刷新数据。

## 5. Secret 提交示例

```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods/{auth_method_id}/secret \
  -H 'content-type: application/json' \
  -d '{
		  "id": "secret_001",
		  "name": "生产环境账号",
		  "tags": ["prod", "x-platform"],
		  "data": { ... 与 authFieldsSchema 对应 ... },
		  "loginPayload": { ... 与 loginFieldsSchema 对应，可选 ... },
		  "autoLoginEnabled": false
		}'
```

系统会：

- 校验 `data` 是否满足 `authFieldsSchema`，若缺少字段返回 400。
- 检查 `loginPayload` 是否满足 `loginFieldsSchema`（若提供）。
- 自动记录 `authDataSources`，用于追踪字段来源（手动输入 / 接口解析）。
- 根据 `authFieldPlacements` 提供给后续执行器所需的 Header/Cookie/Query 信息。

## 5.1 Secret 查询与更新

- 获取指定授权方式下的 Secret：`GET /v1/auth-methods/{auth_method_id}/secret?name={optional_name}`
- 列出授权方式下全部 Secret：`GET /v1/auth-methods/{auth_method_id}/secrets`
- 通过 ID 获取：`GET /v1/secrets/{secret_id}`
- 更新 Secret：`PATCH /v1/secrets/{secret_id}`（可更新 `data`、`loginPayload`、`autoLoginEnabled`、状态等字段）

使用建议：

1. 在展示 Secret 列表时，可根据 `tags`、`status`、`expiresAt` 等字段提供过滤。
2. 更新 `data` 或 `loginPayload` 时同样会触发 Schema 校验，可用来补齐字段或纠正错误。
3. 若需要提醒用户续期，可结合 `lastRefreshed`、`expiresAt` 与 `SecretRemindRequest` 接口（如已实现）发起通知。

提示：从本版本起，所有返回 `Secret` 的接口均会额外返回 `usageMapping` 字段（如果关联的 `AuthMethod.authFieldPlacements` 已配置）。`usageMapping` 等价于该授权方式的 `authFieldPlacements`，用于指导客户端在发起实际业务请求时，如何将 `Secret.data` 中的字段注入到 `headers/query/cookies/body`。

## 6. 追加注意事项

- 同一 `AuthMethod` 下，`Secret.name` 需唯一，重复提交会走更新逻辑。
- 若 AuthMethod Schema 更新，建议提示已有 Secret 重新校验或补齐字段。
- 上游服务在渲染表单时，应对 Schema 中的 `format`、`enum`、`title` 等元数据给予可视化提示，提升填写准确性。
- 如需国际化，可结合 Schema 中的 `title` 与自定义扩展字段控制展示。
- OAuth1/OAuth2 回调地址通过 `AUTH_CALLBACK_URL`（或 `AUTH_CALLBACK_BASE_URL` + `AUTH_CALLBACK_PATH`）从环境注入，前端、运营均无需也不得修改；若需要确认当前值，可调用 `GET /v1/runtime/callback-url`。
- 平台若需登记出口 IP 白名单，可调用 `GET /v1/runtime/ip-allowlist` 获取环境变量 `AUTH_IP_ALLOWLIST`（或历史 `AUTH_WHITELIST_IPS`）中配置的列表。

## 7. 示例：微信公众平台 OAuth1（稳定版接口）

微信公众平台提供的 `stable_token` 接口可视为 OAuth1 的第二阶段：

1. **配置阶段**：在 `loginFieldsSchema` 中要求运营录入 `AppID`（appid）、`AppSecret`（secret），`grant_type` 默认 `client_credential`，`force_refresh` 默认为 `false`；`callback_url` 字段只读用于展示由服务端注入的回调地址。
2. **登录阶段**：`loginFlow` 以 `POST` 请求 `https://api.weixin.qq.com/cgi-bin/stable_token`，Body 中包含 `grant_type`/`appid`/`secret`/`force_refresh`；
3. **结果保存**：`responseMapping` 把响应体里的 `access_token`、`expires_in` 写入 `Secret.data`，系统会据此计算过期时间。

示例 `curl` 片段可参考 `README.md` 中的“授权配置样例 - 微信公众平台”章节；提交 Secret 时只需补充账号描述与标签，`data` 字段会在自动登录后填充。需要对接微信侧 IP 白名单时，通过 `/v1/runtime/ip-allowlist` 获取配置即可。

通过上述流程，上游系统可以在前端清晰引导用户填写授权方式配置与凭证，并与 Turbo Agent Auth 服务保持一致的数据结构，从而支持自动化登录、凭证刷新与请求注入等功能。

## 8. 示例：Superset Cookies 两步登录（基于 steps + requestPlacements）

1) 创建平台（如已创建可跳过）

```bash
curl -X POST http://127.0.0.1:8000/v1/platforms \
	-H 'content-type: application/json' \
	-d '{
		"id":"plat_superset",
		"orgId":"org_demo",
		"nameId":"superset",
		"name":"Apache Superset",
		"code":"superset",
		"baseUrl":"http://<BASE_URL>:8088/"
	}'
```

2) 创建/更新 AuthMethod（关键在 `steps[*].responseMapping` + `steps[*].requestPlacements`）

```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods \
	-H 'content-type: application/json' \
	-d '{
		"id": "am_superset_cookies",
		"platformId": "plat_superset",
		"name": "superset_cookie_login",
		"type": "Cookies",
		"description": "Superset 两步登录：先取 csrf_token + session，再二次提交获取会话。",
		"loginFieldsSchema": {
			"type": "object",
			"required": ["username", "password"],
			"properties": {
				"username": {"type": "string"},
				"password": {"type": "string"}
			}
		},
		"loginFlow": {
			"steps": [
				{
					"url": "http://<BASE_URL>:8088/login/",
					"method": "POST",
					"headers": {"Content-Type": "application/x-www-form-urlencoded"},
					"body_template": {"username": "{{username}}", "password": "{{password}}"},
					"responseMapping": {
						"cookies": {"collectAll": true, "targetField": "cookies", "fields": {"session": "session"}},
						"html": {"fields": {"csrf_token": {"selector": {"tag": "input", "attrs": {"id": "csrf_token"}}, "attribute": "value"}}}
					}
				},
				{
					"url": "http://<BASE_URL>:8088/login/",
					"method": "POST",
					"headers": {"Content-Type": "application/x-www-form-urlencoded"},
					"allowRedirects": false,
					"requestPlacements": {
						"cookies": {"session": "cookies.session"},
						"body": {"username": "username", "password": "password", "csrf_token": "csrf_token"}
					}
				}
			]
		},
		"responseMapping": {
			"cookies": {"collectAll": true, "targetField": "cookies", "fields": {"session": "session"}}
		},
		"defaultValiditySeconds": 86400
	}'
```

3) 提交 Secret 并触发自动登录

```bash
curl -X POST http://127.0.0.1:8000/v1/auth-methods/am_superset_cookies/secret \
	-H 'content-type: application/json' \
	-d '{
		"id": "sec_superset_demo",
		"name": "superset_demo",
		"tags": ["superset"],
		"data": {},
		"loginPayload": {"username": "<user>", "password": "<pass>"},
		"autoLoginEnabled": true
	}'
```

结果：第一步解析 `csrf_token` 与 `cookies.session`，第二步通过 `requestPlacements` 注入 Cookie 与表单；最终 `responseMapping.cookies` 聚合会话 Cookie 并推导 `expiresAt`（若 `Set-Cookie` 含过期信息）。
