Claude Code、OpenCode、Windsurf などのコーディングエージェントは、Cloudflare Access で保護されたリソースに到達する必要があることがよくあります。リソースが Access の背後にあると、未認証のリクエストは想定どおりのレスポンスではなく、リダイレクトまたは 403 エラーを受け取ります。エージェントは、リソースに到達する前に認証する必要があります。
このページでは、次の 2 つの認証方法を説明します。
- cloudflared — ユーザーのアイデンティティで認証します。ブラウザーログインを完了できる対話型の開発に使います。
- サービストークン — 静的な認証情報のペアで認証します。ブラウザーが使えないヘッドレスまたは自動化ワークフローに使います。
cloudflared を使うと、エージェントはユーザーのアイデンティティで認証します。初回利用時、cloudflared は対話型ログイン用のブラウザーウィンドウを開きます。その後、セッションは、アプリケーションに設定した セッション期間 のあいだ維持されます。セッションが期限切れになると、次のリクエストで再度ブラウザーログインが必要です。
cloudflared をダウンロードしてインストール します。
保護されたリソースへの直接リクエストには、cloudflared access curl を使います。認証は自動で処理され、トークン管理は不要です。
cloudflared access curl https://example.com/api/endpointセッション内の最初のリクエストの場合、cloudflared は認証用のブラウザーを開きます。必要に応じて、ユーザーにログインを完了するよう促してください。
一部のエージェントは、cloudflared を直接呼び出さず、独自のクライアントライブラリで HTTP リクエストを送ります。この場合は、ログインしてトークンを取得し、ヘッダーとして渡します。
CF_TOKEN=$(cloudflared access login https://example.com)
curl --header "cf-access-token: $CF_TOKEN" https://example.com/api/endpointトークンは、アプリケーションに設定したセッション期間のあいだ有効です。
詳細は、CLI で Access 経由で接続する と クライアント側の cloudflared を参照してください。
サービストークンは、ブラウザーログインなしでリクエストを認証する静的な認証情報のペアです。ユーザーがいない自動化ワークフローに使います。
-
サービストークンを作成 し、Client 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 Authentication
Authenticate to Cloudflare Access-protected resources using standard OAuth 2.0
(resource metadata discovery, dynamic client registration, authorization code with PKCE).
## When to Use
Use this skill when:
- You need to access a URL that returns HTTP 401
- The response contains a `www-authenticate: Bearer` header with a `resource_metadata` URL
- The resource metadata indicates it is a Cloudflare Access-protected resource
- You want to authenticate interactively through the user's IdP
## Step 1: Detect a Protected Resource
Make a request and inspect the response headers:
```bash
curl -sI -L <URL> 2>&1
```
Look for a **401** response with a `www-authenticate` header like:
```
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/"
```
If you see this header, the site supports the OAuth flow. Proceed to Step 2.
The JSON body of the 401 will also contain:
```json
{
"error": "invalid_token",
"error_description": "Missing or invalid access token",
"resource_metadata": "https://<hostname>/.well-known/cloudflare-access-protected-resource/"
}
```
### If No `www-authenticate` Header
If the 401 does not include `www-authenticate` with `resource_metadata`, the site may
not support this OAuth flow. Fall back to `cloudflared access curl` or browser-based
authentication.
## Step 2: Fetch Resource Metadata
Fetch the resource metadata URL from the `www-authenticate` header:
```bash
curl -s https://<hostname>/.well-known/cloudflare-access-protected-resource/
```
Expected response:
```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/"
}
```
Extract the **authorization server** URL from `authorization_servers[0]` (e.g. `https://<team>.cloudflareaccess.com`).
## Step 3: Fetch OAuth Authorization Server Metadata
```bash
curl -s https://<team>.cloudflareaccess.com/.well-known/oauth-authorization-server
```
Expected response:
```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"]
}
```
Verify that:
- `"none"` is in `token_endpoint_auth_methods_supported` (allows public clients)
- `"authorization_code"` is in `grant_types_supported`
- `"S256"` is in `code_challenge_methods_supported`
- A `registration_endpoint` is present
Extract the **registration_endpoint**, **authorization_endpoint**, and **token_endpoint**.
## Step 4: Dynamic Client Registration
Register a public OAuth client:
```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>"
}'
```
Expected response:
```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
}
```
Save the **client_id**.
## Step 5: Generate PKCE Challenge
Generate a code verifier and S256 challenge. Ensure the challenge starts with an
alphanumeric character to avoid URL parsing issues:
```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
```
**Important**: The code challenge MUST start with `[a-zA-Z0-9]`. A leading `-` or `_`
can cause URL parameter parsing failures on the authorization server.
## Step 6: Authorization Code Flow with Local Callback
Start a local HTTP server to catch the callback, then direct the user to the
authorization URL.
### Build the Authorization 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>
```
### Start the Callback Listener and Prompt the User
Run a Python HTTP server on port 8400 that captures the authorization code:
```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()
'
```
**Important**: Use a timeout of at least 120000ms for this bash command since the user
needs time to authenticate in the browser.
Tell the user to open the authorization URL in their browser. After they authenticate
with their IdP, the browser will redirect to `http://localhost:8400/callback?code=<code>`,
the server will capture it and shut down.
## Step 7: Exchange Code for Token
```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>"
```
Expected response:
```json
{
"access_token": "oauth:<token>",
"token_type": "bearer",
"expires_in": 900,
"scope": "",
"resource": "https://<hostname>/",
"refresh_token": "oauth:<refresh_token>"
}
```
Save the **access_token** and **refresh_token**.
## Step 8: Access the Protected Resource
```bash
curl -s https://<hostname>/ \
-H "Authorization: Bearer <access_token>"
```
This should now return the actual content behind Cloudflare Access.
## Step 9: Refresh the Token (if needed)
If the access token expires (default 900 seconds), use the refresh token:
```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>"
```
## Quick Reference: Full Flow Summary
```
1. curl -sI <URL> # Detect 401 + www-authenticate header
2. curl -s <resource_metadata_url> # Get authorization server
3. curl -s <as>/.well-known/oauth-authorization-server # Get endpoints
4. POST <registration_endpoint> # Register public client
5. Generate PKCE code_verifier + challenge # S256, alphanumeric start
6. Start localhost:8400 listener # Catch callback
7. User opens authorization URL # Browser-based IdP auth
8. POST <token_endpoint> # Exchange code for token
9. curl -H "Authorization: Bearer <token>" # Access resource
```
## Troubleshooting
| Problem | Cause | Fix |
| ------------------------------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `code_challenge_method must be S256 for public clients` | Code challenge starts with `-` or `_`, corrupting the URL parameter | Regenerate until challenge starts with `[a-zA-Z0-9]` |
| `invalid_grant` on token exchange | Code expired or verifier mismatch | Redo the auth flow; codes are single-use and short-lived |
| 401 after using token | Token expired (default 15 min) | Use refresh token to get a new access token |
| No `www-authenticate` header | Site doesn't support OAuth resource metadata | Fall back to `cloudflared access curl` or browser auth |
| No `registration_endpoint` in AS metadata | Dynamic registration not enabled | Must use a pre-registered client or different auth method |
| Port 8400 already in use | Previous listener didn't shut down | Kill the process or use a different port (update redirect_uri accordingly) |