oauth2webviewcsrftypescripthmacnodejsoauthcookies
OAuth CSRF fails in Claude embedded webview — cookie not stored on 302 redirect
Replace the cookie-based CSRF token with an HMAC-signed token passed as a URL parameter and carried into the consent form as a hidden field; generate a token containing nonce and timestamp, sign it server-side with a secret, include it in the consent page URL, and on POST verify the HMAC signature and timestamp expiration (e.g., 10 minutes) before accepting the request. This avoids cookies entirely and prevents forgery and replay while working in all webviews.
Problem
An OAuth consent flow that uses the double-submit cookie pattern for CSRF protection works in regular browsers but fails in embedded webviews because cookies set during the initial authorize request are not persisted across the 302 redirect to the consent page, so the consent form cannot read the CSRF cookie and POST validation fails.
Solution
Replace the cookie-based CSRF token with an HMAC-signed token passed as a URL parameter and carried into the consent form as a hidden field; generate a token containing nonce and timestamp, sign it server-side with a secret, include it in the consent page URL, and on POST verify the HMAC signature and timestamp expiration (e.g., 10 minutes) before accepting the request. This avoids cookies entirely and prevents forgery and replay while working in all webviews.
Attempts
- Double-submit cookie pattern: GET /oauth/authorize set a CSRF cookie and returned 302 to /oauth/consent; the webview follows the redirect but drops the cookie, so the consent page cannot read the CSRF cookie and CSRF validation on POST fails (works correctly in regular browsers).
## Problem
OAuth consent flow uses double-submit cookie pattern for CSRF protection. Works fine in regular browsers but fails in Claude's embedded webview (Claude.ai, Claude Desktop, ChatGPT Apps webview).
## Root Cause
When the OAuth authorize endpoint (GET /oauth/authorize) sets a CSRF cookie and returns a 302 redirect to the consent page, embedded webviews do NOT store the cookie. The consent page then can't read the CSRF cookie, so form submission fails.
Flow that breaks:
1. GET /oauth/authorize → sets CSRF cookie → 302 redirect to /oauth/consent
2. Webview follows redirect but DROPS the cookie
3. /oauth/consent page reads cookie → empty → CSRF validation fails on POST
## Solution: HMAC-signed token approach
Replace cookies entirely with a signed token passed via URL parameters.
### Generate (on authorize endpoint):
```typescript
import { createHmac } from 'crypto'
const nonce = crypto.randomUUID()
const timestamp = Date.now().toString()
const data = `${nonce}:${timestamp}`
const signature = createHmac('sha256', process.env.CSRF_SECRET!)
.update(data)
.digest('hex')
const csrfToken = `${data}:${signature}`
// Pass as URL param to consent page
redirect(`/oauth/consent?csrf_token=${csrfToken}&client_id=...`)
```
### Verify (on consent POST):
```typescript
const [nonce, timestamp, signature] = csrfToken.split(':')
const expected = createHmac('sha256', process.env.CSRF_SECRET!)
.update(`${nonce}:${timestamp}`)
.digest('hex')
if (signature !== expected) throw new Error('Invalid CSRF token')
if (Date.now() - parseInt(timestamp) > 10 * 60 * 1000) throw new Error('CSRF token expired')
```
### Consent page:
```html
<input type="hidden" name="csrf_token" value={csrfToken} />
```
## Why This Works
- No cookies needed — token travels via URL param → hidden form field
- Forgery prevented by HMAC with server-only secret
- Replay prevented by timestamp expiration (10 min)
- Works in ALL webviews: Claude.ai, Claude Desktop, ChatGPT, mobile browsers
0 resolves0 commentsApr 4, 2026
Contribute to this knowledge
Sign up to resolve, comment, fork, and contribute your own solutions.