From 09b7b33622a5311836e6c2fa90c54cd0597a1841 Mon Sep 17 00:00:00 2001 From: Caley Date: Wed, 8 Apr 2026 17:22:26 -0500 Subject: [PATCH 1/3] [Access] Fix OpenCode casing and use long-form --header flag in curl examples --- .../access-controls/authenticate-agents.mdx | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx diff --git a/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx b/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx new file mode 100644 index 000000000000000..5b3d6867c48ab17 --- /dev/null +++ b/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx @@ -0,0 +1,135 @@ +--- +pcx_content_type: how-to +title: Authenticate coding agents +description: Grant coding agents like Claude Code, OpenCode, and Windsurf access to resources protected by Cloudflare Access using cloudflared or service tokens. +products: + - access +tags: + - AI + - Authentication +sidebar: + order: 7 +--- + +Coding agents such as Claude Code, OpenCode, and Windsurf often need to reach resources protected by [Cloudflare Access](/cloudflare-one/access-controls/). When a resource is behind Access, unauthenticated requests receive a redirect or `403` error instead of the expected response. Your agent needs a way to authenticate before it can reach the resource. + +This page covers two authentication methods: + +- [**cloudflared**](#use-cloudflared) — authenticates under your user identity. Use for interactive development where you can complete a browser login. +- [**Service tokens**](#use-service-tokens) — authenticates with a static credential pair. Use for headless or automated workflows where no browser is available. + +:::note +Cloudflare Access also supports Managed OAuth to protected resources, which you can use to grant authorization to coding agents. +::: + +## Use cloudflared + +With `cloudflared`, your agent authenticates under your user identity. On first use, `cloudflared` opens a browser window for an interactive login. After that, the session persists for the [session duration](/cloudflare-one/access-controls/access-settings/session-management/) configured for the application. After the session expires, the next request requires a new browser login. + +:::caution +`cloudflared` authentication relies on WebSockets, which may close unexpectedly during long-running sessions. If your agent requires persistent, unattended access, [service tokens](#use-service-tokens) are more reliable. +::: + +### Prerequisites + +[Download and install cloudflared](/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/). + +### Make requests with cloudflared access curl + +For direct requests to a protected resource, use `cloudflared access curl`. This handles authentication automatically and does not require token management. + +```sh +cloudflared access curl https://example.com/api/endpoint +``` + +If this is the first request in a session, `cloudflared` opens a browser for the user to authenticate. Prompt the user to complete the login if needed. + +### Use a reusable token + +Some agents make HTTP requests using their own client libraries instead of calling `cloudflared` directly. In this case, fetch a reusable token and pass it as a header: + +```bash +CF_TOKEN=$(cloudflared access token -app=https://example.com) +curl --header "cf-access-token: $CF_TOKEN" https://example.com/api/endpoint +``` + +The token is valid for the session duration configured for the application. + +For more information, refer to [Connect through Access using a CLI](/cloudflare-one/tutorials/cli/) and [Client-side cloudflared](/cloudflare-one/access-controls/applications/non-http/cloudflared-authentication/). + +## Use service tokens + +Service tokens are static credential pairs that authenticate requests without a browser login. Use them for automated workflows where no user is present. + +1. [Create a service token](/cloudflare-one/access-controls/service-credentials/service-tokens/#create-a-service-token) and save the **Client ID** and **Client Secret**. + +2. In the Access application's policy configuration, add a [Service Auth policy](/cloudflare-one/access-controls/policies/#service-auth). This policy type accepts service token credentials instead of requiring an identity provider login. Use the **Service Token** selector and select the token you created. + + | Action | Rule type | Selector | Value | + | ------------ | --------- | ------------- | ---------------- | + | Service Auth | Include | Service Token | Your agent token | + +3. Store the Client ID and Client Secret in a secure location on your machine that your agent can read. + +4. Include both values as headers in requests to the protected resource: + + ```bash + curl --header "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \ + --header "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \ + https://example.com/api/endpoint + ``` + +For more information, refer to [Service tokens](/cloudflare-one/access-controls/service-credentials/service-tokens/). + +## Configure your agent + +Coding agents read project-level instruction files (commonly named `AGENTS.md`) to learn how to interact with tools and services in your development environment. Add the following snippet to your `AGENTS.md` file or agent skill configuration so that the agent can detect and authenticate to Access-protected resources: + +````markdown +### Interacting with protected resources + +A resource is protected by Cloudflare Access if any of the following are true: + +- The response is a 302 redirect to a URL containing `cloudflareaccess.com` +- The response is a 403 with a `Cf-Access-Error` header +- The response includes a `WWW-Authenticate: Bearer` challenge header referencing a Cloudflare Access policy + +Cloudflare Access is an identity-aware security tool that protects private +resources over the Internet. It requires an authentication event before +the target resource will respond. + +When you determine a resource is protected behind Access, authenticate +using one of the following methods: + +#### Method 1: cloudflared access curl (user identity) + +Use this when operating under a user's identity. Prefer `cloudflared access curl` +over managing tokens directly. + +1. Make requests directly: + ``` + cloudflared access curl https://example.com/api/endpoint + ``` + If this is the first request, cloudflared will open a browser for the user + to authenticate. Prompt the user to complete it. +2. If you are already running a long-lived program and need a reusable token + for a standard HTTP client: + ``` + CF_TOKEN=$(cloudflared access token -app=https://example.com) + curl --header "cf-access-token: $CF_TOKEN" https://example.com/api/endpoint + ``` + +#### Method 2: Service tokens (headless) + +Use this for non-interactive, machine-to-machine access. +No browser login required. + +Fetch the `CF-Access-Client-Id` and `CF-Access-Client-Secret` values from +[my secure credential store] and include them as headers: + +``` +curl --header "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \ + --header "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \ + https://example.com/api/endpoint +``` +```` From ea453049931c23aeeca7517546cbec28e9898aa2 Mon Sep 17 00:00:00 2001 From: Caley Date: Mon, 13 Apr 2026 15:57:51 -0500 Subject: [PATCH 2/3] [Access] Address PR review comments on authenticate-agents --- .../access-controls/authenticate-agents.mdx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx b/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx index 5b3d6867c48ab17..12dbad4fbd91106 100644 --- a/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx +++ b/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx @@ -18,18 +18,10 @@ This page covers two authentication methods: - [**cloudflared**](#use-cloudflared) — authenticates under your user identity. Use for interactive development where you can complete a browser login. - [**Service tokens**](#use-service-tokens) — authenticates with a static credential pair. Use for headless or automated workflows where no browser is available. -:::note -Cloudflare Access also supports Managed OAuth to protected resources, which you can use to grant authorization to coding agents. -::: - ## Use cloudflared With `cloudflared`, your agent authenticates under your user identity. On first use, `cloudflared` opens a browser window for an interactive login. After that, the session persists for the [session duration](/cloudflare-one/access-controls/access-settings/session-management/) configured for the application. After the session expires, the next request requires a new browser login. -:::caution -`cloudflared` authentication relies on WebSockets, which may close unexpectedly during long-running sessions. If your agent requires persistent, unattended access, [service tokens](#use-service-tokens) are more reliable. -::: - ### Prerequisites [Download and install cloudflared](/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/). @@ -46,10 +38,10 @@ If this is the first request in a session, `cloudflared` opens a browser for the ### Use a reusable token -Some agents make HTTP requests using their own client libraries instead of calling `cloudflared` directly. In this case, fetch a reusable token and pass it as a header: +Some agents make HTTP requests using their own client libraries instead of calling `cloudflared` directly. In this case, log in to get a token and pass it as a header: ```bash -CF_TOKEN=$(cloudflared access token -app=https://example.com) +CF_TOKEN=$(cloudflared access login https://example.com) curl --header "cf-access-token: $CF_TOKEN" https://example.com/api/endpoint ``` @@ -91,8 +83,7 @@ Coding agents read project-level instruction files (commonly named `AGENTS.md`) A resource is protected by Cloudflare Access if any of the following are true: - The response is a 302 redirect to a URL containing `cloudflareaccess.com` -- The response is a 403 with a `Cf-Access-Error` header -- The response includes a `WWW-Authenticate: Bearer` challenge header referencing a Cloudflare Access policy +- The response includes a `WWW-Authenticate: Cloudflare-Access resource_metadata="..."` header Cloudflare Access is an identity-aware security tool that protects private resources over the Internet. It requires an authentication event before @@ -115,7 +106,7 @@ over managing tokens directly. 2. If you are already running a long-lived program and need a reusable token for a standard HTTP client: ``` - CF_TOKEN=$(cloudflared access token -app=https://example.com) + CF_TOKEN=$(cloudflared access login https://example.com) curl --header "cf-access-token: $CF_TOKEN" https://example.com/api/endpoint ``` From f15e3367914819835af46a0acf361446d02f8bc9 Mon Sep 17 00:00:00 2001 From: Caley Date: Tue, 14 Apr 2026 15:58:37 -0500 Subject: [PATCH 3/3] swapped out original agents file with new one, per jroyal. addressed all other feedback. --- .../access-controls/authenticate-agents.mdx | 338 ++++++++++++++++-- 1 file changed, 303 insertions(+), 35 deletions(-) diff --git a/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx b/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx index 12dbad4fbd91106..b7cf6ec9024560a 100644 --- a/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx +++ b/src/content/docs/cloudflare-one/access-controls/authenticate-agents.mdx @@ -18,6 +18,10 @@ This page covers two authentication methods: - [**cloudflared**](#use-cloudflared) — authenticates under your user identity. Use for interactive development where you can complete a browser login. - [**Service tokens**](#use-service-tokens) — authenticates with a static credential pair. Use for headless or automated workflows where no browser is available. +:::note +Cloudflare Access also supports Managed OAuth for protected resources, which you can use to grant authorization to coding agents. +::: + ## Use cloudflared With `cloudflared`, your agent authenticates under your user identity. On first use, `cloudflared` opens a browser window for an interactive login. After that, the session persists for the [session duration](/cloudflare-one/access-controls/access-settings/session-management/) configured for the application. After the session expires, the next request requires a new browser login. @@ -40,7 +44,7 @@ If this is the first request in a session, `cloudflared` opens a browser for the Some agents make HTTP requests using their own client libraries instead of calling `cloudflared` directly. In this case, log in to get a token and pass it as a header: -```bash +```sh CF_TOKEN=$(cloudflared access login https://example.com) curl --header "cf-access-token: $CF_TOKEN" https://example.com/api/endpoint ``` @@ -65,7 +69,7 @@ Service tokens are static credential pairs that authenticate requests without a 4. Include both values as headers in requests to the protected resource: - ```bash + ```sh curl --header "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \ --header "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \ https://example.com/api/endpoint @@ -75,52 +79,316 @@ For more information, refer to [Service tokens](/cloudflare-one/access-controls/ ## Configure your agent -Coding agents read project-level instruction files (commonly named `AGENTS.md`) to learn how to interact with tools and services in your development environment. Add the following snippet to your `AGENTS.md` file or agent skill configuration so that the agent can detect and authenticate to Access-protected resources: +Add an `AGENTS.md` file to your project root with the following skill definition. This instructs coding agents to automatically detect Cloudflare Access-protected resources and authenticate using the standard OAuth 2.0 flow with PKCE (RFC 9728). ````markdown -### Interacting with protected resources +--- +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 +--- -A resource is protected by Cloudflare Access if any of the following are true: +# Access OAuth Authentication -- The response is a 302 redirect to a URL containing `cloudflareaccess.com` -- The response includes a `WWW-Authenticate: Cloudflare-Access resource_metadata="..."` header +Authenticate to Cloudflare Access-protected resources using standard OAuth 2.0 +(resource metadata discovery, dynamic client registration, authorization code with PKCE). -Cloudflare Access is an identity-aware security tool that protects private -resources over the Internet. It requires an authentication event before -the target resource will respond. +## When to Use -When you determine a resource is protected behind Access, authenticate -using one of the following methods: +Use this skill when: -#### Method 1: cloudflared access curl (user identity) +- 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 -Use this when operating under a user's identity. Prefer `cloudflared access curl` -over managing tokens directly. +## Step 1: Detect a Protected Resource -1. Make requests directly: - ``` - cloudflared access curl https://example.com/api/endpoint - ``` - If this is the first request, cloudflared will open a browser for the user - to authenticate. Prompt the user to complete it. -2. If you are already running a long-lived program and need a reusable token - for a standard HTTP client: - ``` - CF_TOKEN=$(cloudflared access login https://example.com) - curl --header "cf-access-token: $CF_TOKEN" https://example.com/api/endpoint - ``` +Make a request and inspect the response headers: + +```bash +curl -sI -L 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:///.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:///.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:///.well-known/cloudflare-access-protected-resource/ +``` + +Expected response: + +```json +{ + "resource": "https://", + "protected": true, + "team_domain": ".cloudflareaccess.com", + "authorization_servers": ["https://.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://.cloudflareaccess.com`). + +## Step 3: Fetch OAuth Authorization Server Metadata + +```bash +curl -s https://.cloudflareaccess.com/.well-known/oauth-authorization-server +``` + +Expected response: + +```json +{ + "issuer": ".cloudflareaccess.com", + "authorization_endpoint": "https://.cloudflareaccess.com/cdn-cgi/access/oauth/authorization", + "token_endpoint": "https://.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://.cloudflareaccess.com/cdn-cgi/access/oauth/revoke", + "registration_endpoint": "https://.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 -#### Method 2: Service tokens (headless) +Register a public OAuth client: -Use this for non-interactive, machine-to-machine access. -No browser login required. +```bash +curl -s -X POST \ + -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://" + }' +``` + +Expected response: + +```json +{ + "client_id": "", + "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 + +``` +? + client_id=& + redirect_uri=http%3A%2F%2Flocalhost%3A8400%2Fcallback& + response_type=code& + code_challenge=& + code_challenge_method=S256& + 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"

Got it!

Authorization code received. You can close this tab.

") + 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"

Error

{err}: {desc}

".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. -Fetch the `CF-Access-Client-Id` and `CF-Access-Client-Secret` values from -[my secure credential store] and include them as headers: +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=`, +the server will capture it and shut down. +## Step 7: Exchange Code for Token + +```bash +curl -s -X POST \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code" \ + -d "code=" \ + -d "client_id=" \ + -d "redirect_uri=http://localhost:8400/callback" \ + -d "code_verifier=" +``` + +Expected response: + +```json +{ + "access_token": "oauth:", + "token_type": "bearer", + "expires_in": 900, + "scope": "", + "resource": "https:///", + "refresh_token": "oauth:" +} +``` + +Save the **access_token** and **refresh_token**. + +## Step 8: Access the Protected Resource + +```bash +curl -s https:/// \ + -H "Authorization: Bearer " +``` + +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 \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=refresh_token" \ + -d "refresh_token=" \ + -d "client_id=" ``` -curl --header "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \ - --header "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \ - https://example.com/api/endpoint + +## Quick Reference: Full Flow Summary + +``` +1. curl -sI # Detect 401 + www-authenticate header +2. curl -s # Get authorization server +3. curl -s /.well-known/oauth-authorization-server # Get endpoints +4. POST # 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 # Exchange code for token +9. curl -H "Authorization: Bearer " # 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) | ````