Bug Report: OAuth State Not Generated on Cloudflare Pages (Astro 5.x)
Summary
Keystatic's GitHub OAuth flow fails on Cloudflare Pages because no OAuth state parameter is generated - neither in the redirect URL to GitHub nor in a Set-Cookie header. This causes the callback to always return 401 Unauthorized.
Affected packages:
@keystatic/astro (v5.0.6)
@keystatic/core (v0.5.48)
Platform: Cloudflare Pages with @astrojs/cloudflare adapter
Environment
| Component |
Version |
| Astro |
5.13.8 |
| @astrojs/cloudflare |
12.6.12 |
| @keystatic/astro |
5.0.6 |
| @keystatic/core |
0.5.48 |
| Node.js |
18+ |
| Wrangler |
4.x |
Steps to Reproduce
- Create an Astro 5.x project with Cloudflare Pages adapter
- Install Keystatic (
@keystatic/astro, @keystatic/core)
- Configure
keystatic.config.ts with GitHub storage:
storage: {
kind: 'github',
repo: 'owner/repo',
}
- Set up GitHub OAuth App with correct Client ID/Secret
- Deploy to Cloudflare Pages
- Visit
/keystatic and click "Sign in with GitHub"
- After GitHub authorization, callback returns 401 Unauthorized
Expected Behavior
-
/api/keystatic/github/login should:
- Generate a random
state parameter
- Set a
Set-Cookie header containing the encrypted state
- Redirect to GitHub with
state in the URL query params
-
/api/keystatic/github/oauth/callback should:
- Receive
code AND state from GitHub
- Compare URL state with cookie state
- Exchange code for access token
- Set session cookie and redirect to
/keystatic
Actual Behavior
-
/api/keystatic/github/login:
- Returns 307 redirect to GitHub
- NO
state parameter in redirect URL
- NO
Set-Cookie header
-
GitHub redirects back to callback with only code (no state)
-
/api/keystatic/github/oauth/callback:
- Returns 401 Unauthorized
- Cannot validate state (none exists)
Evidence
Login Redirect URL (missing state)
https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=https://example.pages.dev/api/keystatic/github/oauth/callback
Expected: URL should include &state=<random_token>
Callback URL from GitHub (missing state)
/api/keystatic/github/oauth/callback?code=3c53071589e0a6cdab59
Expected: URL should include &state=<random_token>
Cloudflare Function Logs
Login endpoint:
{
"message": ["DEBUG: Status:", 307, "| Set-Cookie:", "NONE"],
"level": "log"
}
Callback endpoint:
{
"message": ["DEBUG: Status:", 401, "| Set-Cookie:", "NONE"],
"level": "log"
}
Generic Handler Response Object
The makeGenericAPIRouteHandler returns for /login:
{
"body": null,
"status": 307,
"headers": [
["Location", "https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=..."]
]
}
Note: Headers array contains only Location - no Set-Cookie header.
Handlers Tested
1. Built-in Keystatic Astro Integration
Using the default keystatic() integration with environment variables.
Result: 401 on callback, no cookies set
2. Custom API Route with @keystatic/astro/api
import { makeHandler } from '@keystatic/astro/api';
export const ALL: APIRoute = async (context) => {
const handler = makeHandler({
config: keystaticConfig,
clientId: '...',
clientSecret: '...',
secret: '...',
});
return handler(context);
};
Result: 401 on callback, no cookies set
3. Custom API Route with @keystatic/core/api/generic
import { makeGenericAPIRouteHandler } from '@keystatic/core/api/generic';
export const ALL: APIRoute = async (context) => {
const handler = makeGenericAPIRouteHandler({
config: keystaticConfig,
clientId: '...',
clientSecret: '...',
secret: '...',
});
const result = await handler(context.request);
// Convert { body, status, headers } to Response
const responseHeaders = new Headers();
for (const [key, value] of result.headers) {
responseHeaders.append(key, value);
}
return new Response(result.body, {
status: result.status,
headers: responseHeaders
});
};
Result: 401 on callback, no cookies set, no state in redirect URL
What We Verified Works
| Component |
Status |
Evidence |
| Environment variables |
Working |
Logs show all secrets present |
| GitHub Client ID |
Working |
Login redirects to GitHub successfully |
| GitHub Client Secret |
Working |
Regenerated and verified |
| Cloudflare runtime bindings |
Working |
context.locals.runtime.env populated |
| Request handling |
Working |
All endpoints receive requests correctly |
| Response conversion |
Working |
Generic handler responses properly converted |
Root Cause Analysis
The OAuth state generation appears to be failing silently. Possible causes:
-
Crypto API difference: Cloudflare Workers use Web Crypto API. State generation may rely on Node.js crypto that isn't available.
-
Cookie API abstraction: Keystatic's internal cookie-setting mechanism may not be compatible with the Cloudflare Workers environment.
-
Missing request context: The generic handler may require additional context (like a cookie jar or state store) that isn't being provided.
-
Silent failure: State generation may be throwing an error that's being caught and swallowed, resulting in a redirect without state.
Workarounds Attempted
- Middleware User-Agent patching - Added User-Agent header for GitHub API (not the cause)
- Environment variable injection - Multiple approaches, all working but not the issue
- Hardcoded secrets - Eliminated env var issues as the cause
- Both Astro and Generic handlers - Same result with both
Suggested Investigation Areas
packages/keystatic/src/api/github.ts - OAuth state generation logic
packages/keystatic/src/api/cookies.ts - Cookie setting abstraction
- Edge runtime compatibility - Web Crypto API usage for state generation
- Error handling - Check if errors in state generation are being silently caught
Minimal Reproduction Repository
A minimal reproduction can be created with:
npm create astro@latest -- --template minimal
npx astro add cloudflare react
npm install @keystatic/core @keystatic/astro
Then configure for GitHub storage and deploy to Cloudflare Pages.
Temporary Workaround
Keystatic Cloud bypasses GitHub OAuth entirely and works on Cloudflare Pages:
// keystatic.config.ts
storage: {
kind: 'cloud',
}
Related Issues
- This may be related to Astro 5.x changes in cookie handling
- May also affect other edge/serverless platforms (Deno Deploy, Bun, etc.)
System Information
OS: macOS (Darwin 24.6.0)
Platform: Cloudflare Pages (Workers runtime)
Region: Sydney (SYD)
Report generated: January 2025
Bug Report: OAuth State Not Generated on Cloudflare Pages (Astro 5.x)
Summary
Keystatic's GitHub OAuth flow fails on Cloudflare Pages because no OAuth state parameter is generated - neither in the redirect URL to GitHub nor in a Set-Cookie header. This causes the callback to always return 401 Unauthorized.
Affected packages:
@keystatic/astro(v5.0.6)@keystatic/core(v0.5.48)Platform: Cloudflare Pages with
@astrojs/cloudflareadapterEnvironment
Steps to Reproduce
@keystatic/astro,@keystatic/core)keystatic.config.tswith GitHub storage:/keystaticand click "Sign in with GitHub"Expected Behavior
/api/keystatic/github/loginshould:stateparameterSet-Cookieheader containing the encrypted statestatein the URL query params/api/keystatic/github/oauth/callbackshould:codeANDstatefrom GitHub/keystaticActual Behavior
/api/keystatic/github/login:stateparameter in redirect URLSet-CookieheaderGitHub redirects back to callback with only
code(no state)/api/keystatic/github/oauth/callback:Evidence
Login Redirect URL (missing state)
Expected: URL should include
&state=<random_token>Callback URL from GitHub (missing state)
Expected: URL should include
&state=<random_token>Cloudflare Function Logs
Login endpoint:
{ "message": ["DEBUG: Status:", 307, "| Set-Cookie:", "NONE"], "level": "log" }Callback endpoint:
{ "message": ["DEBUG: Status:", 401, "| Set-Cookie:", "NONE"], "level": "log" }Generic Handler Response Object
The
makeGenericAPIRouteHandlerreturns for/login:{ "body": null, "status": 307, "headers": [ ["Location", "https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=..."] ] }Note: Headers array contains only
Location- noSet-Cookieheader.Handlers Tested
1. Built-in Keystatic Astro Integration
Using the default
keystatic()integration with environment variables.Result: 401 on callback, no cookies set
2. Custom API Route with
@keystatic/astro/apiResult: 401 on callback, no cookies set
3. Custom API Route with
@keystatic/core/api/genericResult: 401 on callback, no cookies set, no state in redirect URL
What We Verified Works
context.locals.runtime.envpopulatedRoot Cause Analysis
The OAuth state generation appears to be failing silently. Possible causes:
Crypto API difference: Cloudflare Workers use Web Crypto API. State generation may rely on Node.js crypto that isn't available.
Cookie API abstraction: Keystatic's internal cookie-setting mechanism may not be compatible with the Cloudflare Workers environment.
Missing request context: The generic handler may require additional context (like a cookie jar or state store) that isn't being provided.
Silent failure: State generation may be throwing an error that's being caught and swallowed, resulting in a redirect without state.
Workarounds Attempted
Suggested Investigation Areas
packages/keystatic/src/api/github.ts- OAuth state generation logicpackages/keystatic/src/api/cookies.ts- Cookie setting abstractionMinimal Reproduction Repository
A minimal reproduction can be created with:
Then configure for GitHub storage and deploy to Cloudflare Pages.
Temporary Workaround
Keystatic Cloud bypasses GitHub OAuth entirely and works on Cloudflare Pages:
Related Issues
System Information
Report generated: January 2025