Claude Code、OpenCode 和 Windsurf 等编码代理经常需要访问受 Cloudflare Access 保护的资源。当资源位于 Access 后面时,未通过身份验证的请求会收到重定向或 403 错误,而不是预期的响应。您的代理需要一种在访问资源之前进行身份验证的方法。
本页涵盖两种身份验证方法:
- cloudflared — 在您的用户身份下进行身份验证。用于可完成浏览器登录的交互式开发。
- Service tokens(服务令牌) — 使用静态凭据对进行身份验证。用于无浏览器可用的无头或自动化工作流。
使用 cloudflared,您的代理将在您的用户身份下进行身份验证。首次使用时,cloudflared 会打开一个浏览器窗口以进行交互式登录。之后,会话将持续为应用程序配置的会话持续时间。会话过期后,下一个请求需要重新进行浏览器登录。
对于对受保护资源的直接请求,请使用 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)是静态凭据对,无需浏览器登录即可对请求进行身份验证。在没有用户在场的情况下,将它们用于自动化工作流。
-
创建服务令牌并保存 Client ID(客户端 ID) 和 Client Secret(客户端密钥)。
-
在 Access 应用程序的策略配置中,添加 Service Auth 策略。此策略类型接受服务令牌凭据,而不是要求身份提供商登录。使用 Service Token(服务令牌) 选择器并选择您创建的令牌。
操作 规则类型 选择器 值 Service Auth(服务身份验证) Include(包含) Service Token(服务令牌) 您的代理令牌 -
将 Client ID 和 Client Secret 存储在您机器上代理可以读取的安全位置。
-
在发送到受保护资源的请求中,将这两个值作为请求头包含在内:
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) |