Skip to content

fix(cloud_function): return full base64 state for v0.0.9+ compatibility#325

Open
bs-nubank wants to merge 4 commits intogemini-cli-extensions:mainfrom
bs-nubank:fix/oauth-state-mismatch-v0.0.9
Open

fix(cloud_function): return full base64 state for v0.0.9+ compatibility#325
bs-nubank wants to merge 4 commits intogemini-cli-extensions:mainfrom
bs-nubank:fix/oauth-state-mismatch-v0.0.9

Conversation

@bs-nubank
Copy link
Copy Markdown

@bs-nubank bs-nubank commented Apr 7, 2026

Problem

Starting with workspace-server v0.0.9, the OAuth callback state validation was updated to decode the full base64 JSON payload returned by the cloud function, then extract the csrf field from it:

// v0.0.9 — extension/server/index.js
const i = searchParams.get('state');
const A = JSON.parse(Buffer.from(i, 'base64').toString('utf8'));
const g = A?.csrf ?? null;
if (!i || g !== localCsrfToken) {
  res.end('State mismatch. Possible CSRF attack.');
}

However, the cloud function still returns only the raw csrf hex string in the state parameter:

// cloud_function/index.js (before this fix)
if (payload.csrf) {
  finalUrl.searchParams.append('state', payload.csrf); // e.g. "daf5b690a71e..."
}

When the extension receives this raw hex value and tries to Buffer.from(hex, 'base64')JSON.parse, it fails silently, sets csrf to null, and the validation always returns:

State mismatch. Possible CSRF attack.

This makes every browser-based auth attempt on v0.0.9 fail. Users are stuck in a broken auth loop — the only workaround is the headless login (node index.js login).

Root cause

The extension code was updated in v0.0.9 to a new validation protocol (decode the full state, extract csrf) but the cloud function was not updated to match — it still uses the old protocol (return only the raw csrf hex).

Fix — 3 commits

1. fix(cloud_function): Return original state parameter unchanged

// After this fix
if (state) {
  finalUrl.searchParams.append('state', state); // full base64 JSON, as sent originally
}

2. fix(auth): Support both state formats in AuthManager for backward compatibility

⚠️ Backward compatibility issue discovered after the initial commit.

The cloud function fix changes the state value from raw hex to full base64 JSON. workspace-server ≤v0.0.7 compares the returned state directly against the local csrfToken (raw hex) — so after the cloud function is updated, those users would break.

AuthManager.authWithWeb now handles both formats:

// Try base64 JSON first (v0.0.9+ cloud function)
const decoded = JSON.parse(Buffer.from(returnedState, 'base64').toString('utf8'));
if (typeof decoded?.csrf === 'string') {
  csrfFromState = decoded.csrf;
}
// Fallback: treat as raw hex token (≤v0.0.7 cloud function)
if (!csrfFromState) csrfFromState = returnedState;

if (!csrfFromState || csrfFromState !== csrfToken) {
  res.end('State mismatch. Possible CSRF attack.');
}

This means both old and new cloud function versions work without a coordinated rollout.

3. Tests

  • cloud_function/index.test.js — 6 tests for state passthrough logic (verifies fix is correct, verifies old behaviour is gone)
  • workspace-server/src/__tests__/auth/AuthManager.test.ts — 6 tests for CSRF extraction covering both format variants, null/empty state, and edge cases

How to reproduce

  1. Install the Google Workspace extension v0.0.9 in Claude Desktop / Cowork
  2. Trigger any Google Workspace tool — the auth flow opens a browser
  3. Authorize in Google
  4. Observe: State mismatch. Possible CSRF attack. in the browser

Workaround (until this is deployed)

node "/path/to/Claude Extensions/ant.dir.ant.0x55979e1.google-workspace/server/index.js" login

The workspace-server v0.0.9 changed how the OAuth state parameter is
validated on callback. It now expects to receive the full base64-encoded
JSON state back from the cloud function, then decodes it to extract the
`csrf` field for comparison.

The cloud function was returning only `payload.csrf` (the raw hex token),
which worked for ≤ v0.0.7 but breaks v0.0.9+: `JSON.parse` fails on the
raw hex, csrf is set to null, and every auth attempt ends with:

  "State mismatch. Possible CSRF attack."

Fix: return the original `state` parameter unchanged so the client can
decode it regardless of version.
@google-cla
Copy link
Copy Markdown

google-cla bot commented Apr 7, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the handleCallback function in cloud_function/index.js to return the original base64-encoded state parameter instead of the extracted hex CSRF token, aiming to support newer workspace-server versions. Feedback indicates that this change is currently incompatible with the existing server-side validation logic in AuthManager.ts, which expects the raw hex token, and suggests that a coordinated update is necessary to avoid breaking the authentication flow.

Comment on lines +131 to 133
if (state) {
finalUrl.searchParams.append('state', state);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The change to return the full base64-encoded state string is incompatible with the current implementation of the callback validation in workspace-server/src/auth/AuthManager.ts.

In AuthManager.ts (lines 356-361), the server expects the state parameter to be the raw hex CSRF token and performs a direct comparison:

const returnedState = qs.get('state');
if (returnedState !== csrfToken) {
  res.end('State mismatch. Possible CSRF attack.');
  // ...
}

If the cloud function is updated to return the base64-encoded JSON string, this comparison will fail because returnedState (base64) will not match csrfToken (hex). This will break the authentication flow for all users of the current server implementation.

To resolve this, the server-side logic in AuthManager.ts must be updated to decode the base64 state and extract the csrf field before validation, as mentioned in the PR description. Please ensure that the server-side changes are included in this PR or a coordinated update.

…F validation

The cloud function fix (return full base64 state instead of raw payload.csrf)
is not backward-compatible with workspace-server ≤v0.0.7, which compared the
returned state directly against the raw hex csrfToken.

AuthManager.authWithWeb now tries to decode the returned state as base64 JSON
and extract the `csrf` field (v0.0.9+ cloud function format). If that fails
(parse error or missing field), it falls back to treating the value as a raw
hex token (≤v0.0.7 cloud function format).

This means the same AuthManager binary works regardless of which cloud function
version is deployed — no coordinated rollout required.
Adds the extractCsrfFromState helper (mirrors AuthManager logic) and a
dedicated describe block with 6 test cases covering:
- base64 JSON format (v0.0.9+ cloud function)
- raw hex format (≤v0.0.7 cloud function)
- null / empty state
- base64 JSON without csrf field (fallback)
- non-JSON base64 garbage (fallback)
Adds index.test.js with 6 test cases that verify the state parameter is
returned unchanged to the client (instead of just payload.csrf).

Key assertions:
- Returned value is the full base64 state (client can decode csrf from it)
- Returned value is NOT the raw hex csrf (old buggy behaviour is gone)
- manual=true flow returns null (no redirect)
- Oversized state (>4KB) throws
- Full roundtrip preserves uri, manual and csrf fields

Also adds jest as a devDependency and a test npm script to package.json.
@bs-nubank bs-nubank force-pushed the fix/oauth-state-mismatch-v0.0.9 branch 2 times, most recently from c074336 to 4469af1 Compare April 7, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant