跳转到内容
搜索文档

对编码代理进行身份验证

最后更新 查看 MarkdownAgent 设置

Claude Code、OpenCode 和 Windsurf 等编码代理经常需要访问受 Cloudflare Access 保护的资源。当资源位于 Access 后面时,未通过身份验证的请求会收到重定向或 403 错误,而不是预期的响应。您的代理需要一种在访问资源之前进行身份验证的方法。

本页涵盖两种身份验证方法:

  • cloudflared — 在您的用户身份下进行身份验证。用于可完成浏览器登录的交互式开发。
  • Service tokens(服务令牌) — 使用静态凭据对进行身份验证。用于无浏览器可用的无头或自动化工作流。

使用 cloudflared

使用 cloudflared,您的代理将在您的用户身份下进行身份验证。首次使用时,cloudflared 会打开一个浏览器窗口以进行交互式登录。之后,会话将持续为应用程序配置的会话持续时间。会话过期后,下一个请求需要重新进行浏览器登录。

前提条件

下载并安装 cloudflared

使用 cloudflared access curl 进行请求

对于对受保护资源的直接请求,请使用 cloudflared access curl。这会自动处理身份验证,不需要令牌管理。

cloudflared access curl https://example.com/api/endpoint

如果这是会话中的第一个请求,cloudflared 会打开浏览器以供用户进行身份验证。如果需要,提示用户完成登录。

使用可重复使用的令牌

某些代理使用自己的客户端库进行 HTTP 请求,而不是直接调用 cloudflared。在这种情况下,请登录以获取令牌并将其作为请求头传递:

CF_TOKEN=$(cloudflared access login https://example.com)
curl --header "cf-access-token: $CF_TOKEN" https://example.com/api/endpoint

该令牌在为应用程序配置的会话持续时间内有效。

有关更多信息,请参阅通过 Access 使用 CLI 进行连接客户端 cloudflared

使用服务令牌

服务令牌(service tokens)是静态凭据对,无需浏览器登录即可对请求进行身份验证。在没有用户在场的情况下,将它们用于自动化工作流。

  1. 创建服务令牌并保存 Client ID(客户端 ID)Client Secret(客户端密钥)

  2. 在 Access 应用程序的策略配置中,添加 Service Auth 策略。此策略类型接受服务令牌凭据,而不是要求身份提供商登录。使用 Service Token(服务令牌) 选择器并选择您创建的令牌。

    操作 规则类型 选择器
    Service Auth(服务身份验证) Include(包含) Service Token(服务令牌) 您的代理令牌
  3. 将 Client ID 和 Client Secret 存储在您机器上代理可以读取的安全位置。

  4. 在发送到受保护资源的请求中,将这两个值作为请求头包含在内:

    curl --header "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
         --header "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
         https://example.com/api/endpoint

有关更多信息,请参阅服务令牌

配置您的代理

在项目根目录中添加一个包含以下技能定义的 AGENTS.md 文件。这会指示编码代理自动检测受 Cloudflare Access 保护的资源,并使用带有 PKCE 的标准 OAuth 2.0 流程(RFC 9728)进行身份验证。

---
name: access-oauth
description: "Detect Cloudflare Access-protected websites and authenticate via the standard OAuth 2.0 flow (RFC 9728 resource metadata, dynamic client registration, authorization code + PKCE)"
license: MIT
compatibility: opencode
metadata:
  category: authentication
  audience: developers
---

# Access OAuth 身份验证

使用标准 OAuth 2.0 协议(包含资源元数据检测、动态客户端注册以及带有 PKCE 的授权码流)对受 Cloudflare Access 保护的资源进行身份验证。

## 适用场景

当满足以下条件时,请使用此技能:

- 您需要访问一个返回 HTTP 401 状态码的 URL
- 响应中包含一个 `www-authenticate: Bearer` 标头,且该标头附带有 `resource_metadata` 的 URL
- 资源元数据指示这是一个受 Cloudflare Access 保护的资源
- 您希望通过用户的身份提供商 (IdP) 进行交互式身份验证

## 步骤 1:检测受保护的资源

发送请求并检查响应头:

```bash
curl -sI -L <URL> 2>&1
```

寻找带有 `www-authenticate` 标头的 **401** 响应,如:

```
www-authenticate: Bearer realm="OAuth", error="invalid_token",
  error_description="Missing or invalid access token",
  resource_metadata="https://<hostname>/.well-known/cloudflare-access-protected-resource/"
```

如果您看到此标头,则该网站支持 OAuth 流程。请继续执行步骤 2。

401 响应的 JSON 主体中也将包含:

```json
{
	"error": "invalid_token",
	"error_description": "Missing or invalid access token",
	"resource_metadata": "https://<hostname>/.well-known/cloudflare-access-protected-resource/"
}
```

### 如果没有 `www-authenticate` 标头

如果 401 响应中没有包含带有 `resource_metadata``www-authenticate` 标头,说明该网站可能不支持此 OAuth 流程。请回退至使用 `cloudflared access curl` 或基于浏览器的身份验证。

## 步骤 2:获取资源元数据

`www-authenticate` 标头中获取资源元数据 URL:

```bash
curl -s https://<hostname>/.well-known/cloudflare-access-protected-resource/
```

预期响应:

```json
{
	"resource": "https://<hostname>",
	"protected": true,
	"team_domain": "<team>.cloudflareaccess.com",
	"authorization_servers": ["https://<team>.cloudflareaccess.com"],
	"authentication_method": "cloudflared",
	"authentication_method_description": "Use `cloudflared access curl`...",
	"authentication_method_documentation": "https://developers.cloudflare.com/cloudflare-one/tutorials/cli/"
}
```

`authorization_servers[0]` 中提取**授权服务器 (authorization server)** URL(例如 `https://<team>.cloudflareaccess.com`)。

## 步骤 3:获取 OAuth 授权服务器元数据

```bash
curl -s https://<team>.cloudflareaccess.com/.well-known/oauth-authorization-server
```

预期响应:

```json
{
	"issuer": "<team>.cloudflareaccess.com",
	"authorization_endpoint": "https://<team>.cloudflareaccess.com/cdn-cgi/access/oauth/authorization",
	"token_endpoint": "https://<team>.cloudflareaccess.com/cdn-cgi/access/oauth/token",
	"response_types_supported": ["code"],
	"response_modes_supported": ["query"],
	"grant_types_supported": ["authorization_code", "refresh_token"],
	"token_endpoint_auth_methods_supported": [
		"client_secret_basic",
		"client_secret_post",
		"none"
	],
	"revocation_endpoint": "https://<team>.cloudflareaccess.com/cdn-cgi/access/oauth/revoke",
	"registration_endpoint": "https://<team>.cloudflareaccess.com/cdn-cgi/access/oauth/registration",
	"code_challenge_methods_supported": ["S256"]
}
```

验证以下内容:

- `"none"` 包含在 `token_endpoint_auth_methods_supported` 中(允许公共客户端)
- `"authorization_code"` 包含在 `grant_types_supported`
- `"S256"` 包含在 `code_challenge_methods_supported`
- 存在 `registration_endpoint`

提取 **registration_endpoint****authorization_endpoint****token_endpoint**

## 步骤 4:动态客户端注册

注册一个公共 OAuth 客户端:

```bash
curl -s -X POST <registration_endpoint> \
  -H "Content-Type: application/json" \
  -d '{
    "redirect_uris": ["http://localhost:8400/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code"],
    "response_types": ["code"],
    "resource": "https://<hostname>"
  }'
```

预期响应:

```json
{
	"client_id": "<uuid>",
	"redirect_uris": ["http://localhost:8400/callback"],
	"grant_types": ["authorization_code"],
	"response_types": ["code"],
	"token_endpoint_auth_method": "none",
	"registration_client_uri": "...",
	"client_id_issued_at": 1234567890
}
```

保存 **client_id**

## 步骤 5:生成 PKCE Challenge

生成 code verifier 和 S256 challenge。确保 challenge 以字母或数字字符开头,以避免 URL 解析问题:

```bash
while true; do
  CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '/+' '_-')
  CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | base64 | tr -d '=' | tr '/+' '_-')
  if [[ "$CODE_CHALLENGE" =~ ^[a-zA-Z0-9] ]]; then
    break
  fi
done
```

**重要**:Code challenge 必须以字母或数字字符 `[a-zA-Z0-9]` 开头。开头的 `-``_` 可能会导致授权服务器上的 URL 参数解析失败。

## 步骤 6:带有本地回调的授权码流程

启动本地 HTTP 服务器来捕获回调,然后引导用户打开授权 URL。

### 构建授权 URL

```
<authorization_endpoint>?
  client_id=<client_id>&
  redirect_uri=http%3A%2F%2Flocalhost%3A8400%2Fcallback&
  response_type=code&
  code_challenge=<CODE_CHALLENGE>&
  code_challenge_method=S256&
  resource=<URL-encoded target resource>
```

### 启动回调监听器并提示用户

运行端口 8400 上的 Python HTTP 服务器以捕获授权码:

```python
python3 -c '
import http.server, urllib.parse

class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urllib.parse.urlparse(self.path)
        params = urllib.parse.parse_qs(parsed.query)
        if "code" in params:
            code = params["code"][0]
            with open("/tmp/oauth_code.txt", "w") as f:
                f.write(code)
            self.send_response(200)
            self.send_header("Content-Type", "text/html")
            self.end_headers()
            self.wfile.write(b"<h1>Got it!</h1><p>Authorization code received. You can close this tab.</p>")
            print(f"CODE={code}", flush=True)
        elif "error" in params:
            err = params.get("error", [""])[0]
            desc = params.get("error_description", [""])[0]
            self.send_response(200)
            self.send_header("Content-Type", "text/html")
            self.end_headers()
            self.wfile.write(f"<h1>Error</h1><p>{err}: {desc}</p>".encode())
            print(f"ERROR: {err} - {desc}", flush=True)
        else:
            self.send_response(400)
            self.end_headers()
            self.wfile.write(b"Unexpected request")
            print(f"Unexpected: {self.path}", flush=True)
        import threading
        threading.Thread(target=self.server.shutdown).start()
    def log_message(self, format, *args):
        pass

print("Listening on http://localhost:8400 ...", flush=True)
print("Open the authorization URL in your browser.", flush=True)
http.server.HTTPServer(("", 8400), Handler).serve_forever()
'
```

**重要**:对于此 bash 命令,使用至少 120000ms 的超时时间,因为用户需要在浏览器中进行身份验证。

引导用户在浏览器中打开授权 URL。在他们通过 IdP 进行身份验证后,浏览器将重定向至 `http://localhost:8400/callback?code=<code>`,服务器将捕获该授权码并关闭。

## 步骤 7:使用授权码换取令牌

```bash
curl -s -X POST <token_endpoint> \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=<AUTH_CODE>" \
  -d "client_id=<CLIENT_ID>" \
  -d "redirect_uri=http://localhost:8400/callback" \
  -d "code_verifier=<CODE_VERIFIER>"
```

预期响应:

```json
{
	"access_token": "oauth:<token>",
	"token_type": "bearer",
	"expires_in": 900,
	"scope": "",
	"resource": "https://<hostname>/",
	"refresh_token": "oauth:<refresh_token>"
}
```

保存 **access_token****refresh_token**

## 步骤 8:访问受保护的资源

```bash
curl -s https://<hostname>/ \
  -H "Authorization: Bearer <access_token>"
```

此时应返回 Cloudflare Access 后端的实际内容。

## 步骤 9:刷新令牌(如果需要)

如果访问令牌过期(默认 900 秒),请使用刷新令牌:

```bash
curl -s -X POST <token_endpoint> \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=<REFRESH_TOKEN>" \
  -d "client_id=<CLIENT_ID>"
```

## 快速参考:完整流程总结

```
1. curl -sI <URL>                          # 检测 401 和 www-authenticate 标头
2. curl -s <resource_metadata_url>         # 获取授权服务器
3. curl -s <as>/.well-known/oauth-authorization-server  # 获取端点
4. POST <registration_endpoint>            # 注册公共客户端
5. 生成 PKCE code_verifier + challenge      # S256,字母/数字开头
6. 启动 localhost:8400 监听器               # 捕获回调
7. 用户打开授权 URL                        # 基于浏览器的 IdP 身份验证
8. POST <token_endpoint>                   # 使用授权码换取令牌
9. curl -H "Authorization: Bearer <token>" # 访问资源
```

## 故障排除

| 问题 | 原因 | 解决方法 |
| :--- | :--- | :--- |
| `code_challenge_method must be S256 for public clients` | Code challenge 以 `-``_` 开头,损坏了 URL 参数 | 重新生成,直到 challenge 以 `[a-zA-Z0-9]` 开头 |
| 令牌交换时出现 `invalid_grant` | Code 已过期或 verifier 不匹配 | 重新执行身份验证流程;授权码是单次使用且生命周期较短的 |
| 使用令牌后出现 401 错误 | 令牌已过期(默认 15 分钟) | 使用刷新令牌获取新的访问令牌 |
| 没有 `www-authenticate` 标头 | 网站不支持 OAuth 资源元数据 | 回退至使用 `cloudflared access curl` 或浏览器身份验证 |
| AS 元数据中没有 `registration_endpoint` | 未启用动态注册 | 必须使用预先注册的客户端或不同的身份验证方法 |
| 端口 8400 已被占用 | 上一个监听器未关闭 | 杀死该进程或使用不同的端口(相应地更新 redirect_uri) |

这篇文档对您有帮助吗?