From e845c68dc35cbb2ea829ca723520c4ba3b55ee9e Mon Sep 17 00:00:00 2001 From: Andy L WSL Date: Sat, 28 Feb 2026 12:58:52 +0000 Subject: [PATCH] auth details added --- docs/GCloudAuthenticationArchitecture.md | 54 ++++++++++++++++++++++++ src/core/operations/GoogleTranslate.mjs | 10 ++++- tests/browser/03_cloud_ops.js | 39 +++++++++++++++-- 3 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 docs/GCloudAuthenticationArchitecture.md diff --git a/docs/GCloudAuthenticationArchitecture.md b/docs/GCloudAuthenticationArchitecture.md new file mode 100644 index 00000000..a293402c --- /dev/null +++ b/docs/GCloudAuthenticationArchitecture.md @@ -0,0 +1,54 @@ +# Google Cloud Authentication Architecture + +While the [Google Cloud Setup Guide](GoogleCloudSetup.md) covers the step-by-step instructions for extracting your API credentials, this document outlines the architectural decisions, pitfalls, and lessons learned when authenticating from CyberChef's client-side environment to Google Cloud APIs. + +## The CyberChef Context +CyberChef operates entirely within the browser. There is no middle-tier backend server acting as a proxy. This means that any REST request to a Cloud API originates directly from the user's browser via `fetch()` from a Web Worker. + +This architecture has three major implications for authentication: + +### 1. Browser-Side Secret Exposure +Any secret embedded in a CyberChef recipe URL or baked into an operation is visible to the browser, the network layer, and anyone who receives the recipe link. + +* **API Keys**: An API Key is a static secret. If you use one, you **must** apply HTTP Referrer restrictions in the Google Cloud Console (as detailed in the Setup Guide) to ensure it can only be used from your specific CyberChef domain (e.g., `http://localhost:8080/`). +* **OAuth Tokens**: A temporary OAuth Token (from `gcloud auth print-access-token`) is generally preferred because it automatically expires (usually in 1 hour), mitigating the risk of credential leakage. + +### 2. The Application Default Credentials (ADC) Quota Project Pitfall +When you use a short-lived OAuth token generated by an End User account (e.g., via `gcloud auth login`), the token identifies *you*, but Google occasionally needs to know which project to bill for the specific API usage. + +If you attempt to call certain APIs (like `translate.googleapis.com`) using an End User OAuth Token, Google Cloud will throw a 403 error: +> *"Your application is authenticating by using local Application Default Credentials. The translate.googleapis.com API requires a quota project, which is not set by default."* + +#### The Solution: +CyberChef cloud operations that accept an OAuth Token must implement an optional **Quota Project** input argument. +Under the hood, this argument must be injected into the `fetch()` request as the `x-goog-user-project` HTTP header: +```javascript +headers.set("Authorization", `Bearer ${authString}`); +if (quotaProject) { + headers.set("x-goog-user-project", quotaProject); // Injects the billing bound project +} +``` +*Note: You configure your local `gcloud` environment's default quota project using `gcloud auth application-default set-quota-project `.* + +### 3. UI Argument Masking (The `toggleString` Pitfall) +To prevent your API Key or Token from being displayed in plain text within CyberChef's UI, Cloud operations should use the `toggleString` data type for the authentication argument. This renders the input cleanly and masks the text. + +#### The Pitfall: +When converting a CyberChef argument from a standard `string` to a `toggleString`, the underlying payload structure changes. The Web Worker will no longer receive a raw string; it will receive an object: +```javascript +// A normal string returns: "AIzaSy..." +// A toggleString returns: { "string": "AIzaSy...", "option": "UTF8" } +``` +If an operation's `run()` function expects a string and does not explicitly unpack `authStringObj.string`, the network request will silently fail, or the Web Worker will hang when attempting to serialize the object into an HTTP Header. + +Always ensure the runtime safely parses masked keys: +```javascript +const authString = typeof authStringObj === "string" ? authStringObj : (authStringObj.string || ""); +``` + +## Supported Methods Comparison + +| Authentication Method | Security Profile | Setup Complexity | Best For | +| :--- | :--- | :--- | :--- | +| **API Key (Restricted)** | Moderate (Long-lived secret, but restricted by referrer) | Low | Permanent embedded recipes, public web hosting, and sharing recipes internally. | +| **OAuth Bearer Token** | High (Expires in 1 hr) | Medium (Requires `gcloud` CLI) | Local development, automated E2E testing, and one-off secure executions. | diff --git a/src/core/operations/GoogleTranslate.mjs b/src/core/operations/GoogleTranslate.mjs index cea12d05..ce191910 100644 --- a/src/core/operations/GoogleTranslate.mjs +++ b/src/core/operations/GoogleTranslate.mjs @@ -51,6 +51,11 @@ class GoogleTranslate extends Operation { "type": "toggleString", "value": "", "toggleValues": ["UTF8", "Latin1", "Base64", "Hex"] + }, + { + "name": "Quota Project (ADC only)", + "type": "string", + "value": "" } ]; } @@ -61,7 +66,7 @@ class GoogleTranslate extends Operation { * @returns {string} */ async run(input, args) { - const [sourceLanguage, targetLanguage, authType, authStringObj] = args; + const [sourceLanguage, targetLanguage, authType, authStringObj, quotaProject] = args; const authString = typeof authStringObj === "string" ? authStringObj : (authStringObj.string || ""); if (input.length === 0) return ""; @@ -75,6 +80,9 @@ class GoogleTranslate extends Operation { url += `?key=${encodeURIComponent(authString)}`; } else if (authType === "OAuth Token") { headers.set("Authorization", `Bearer ${authString}`); + if (quotaProject) { + headers.set("x-goog-user-project", quotaProject); + } } const body = JSON.stringify({ diff --git a/tests/browser/03_cloud_ops.js b/tests/browser/03_cloud_ops.js index c8f3e5ee..93808dd4 100644 --- a/tests/browser/03_cloud_ops.js +++ b/tests/browser/03_cloud_ops.js @@ -28,7 +28,8 @@ module.exports = { "en", "es", "API Key", - { option: "UTF8", string: "" } + { option: "UTF8", string: "" }, + "" ]); browser.waitForElementNotVisible("#snackbar-container", 6000); @@ -41,6 +42,37 @@ module.exports = { }); }, + "Google Translate: Successful OAuth Token Translation": function (browser) { + let testToken = process.env.CYBERCHEF_GCP_TEST_TOKEN; + + if (!testToken || testToken === "YOUR_OAUTH_TOKEN_HERE") { + try { + testToken = require('child_process').execSync('gcloud auth print-access-token', { stdio: 'pipe', encoding: 'utf-8' }).trim(); + } catch (e) { + console.log("No valid CYBERCHEF_GCP_TEST_TOKEN found and gcloud failed. Skipping live API test."); + return; + } + } + + browserUtils.loadRecipe(browser, "Google Translate", "Hello", [ + "en", + "es", + "OAuth Token", + { option: "UTF8", string: testToken }, + "cyberchefcloud" + ]); + + browser.waitForElementNotVisible("#snackbar-container", 6000); + browserUtils.bake(browser); + browser.pause(2000); + browser.saveScreenshot("tests/browser/output/success_oauth_debug.png"); + browser.execute(function () { + return window.app.manager.output.outputEditorView.state.doc.toString(); + }, [], function ({ value }) { + browser.assert.equal(value, "Hola"); + }); + }, + "Google Translate: Successful API Key Translation": function (browser) { const testKey = process.env.CYBERCHEF_GCP_TEST_KEY; @@ -53,13 +85,14 @@ module.exports = { "en", "es", "API Key", - { option: "UTF8", string: testKey } + { option: "UTF8", string: testKey }, + "" ]); browser.waitForElementNotVisible("#snackbar-container", 6000); browserUtils.bake(browser); browser.pause(2000); - browser.saveScreenshot("tests/browser/output/success_debug.png"); + browser.saveScreenshot("tests/browser/output/success_apikey_debug.png"); browser.execute(function () { return window.app.manager.output.outputEditorView.state.doc.toString(); }, [], function ({ value }) {