Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@uppy/companion: remove oauthOrigin #5311

Merged
merged 7 commits into from
Jul 9, 2024
Merged

@uppy/companion: remove oauthOrigin #5311

merged 7 commits into from
Jul 9, 2024

Conversation

aduh95
Copy link
Contributor

@aduh95 aduh95 commented Jul 3, 2024

No description provided.

@aduh95 aduh95 requested a review from Murderlon July 3, 2024 17:54
Copy link
Contributor

github-actions bot commented Jul 3, 2024

Diff output files
diff --git a/packages/@uppy/companion/lib/config/companion.js b/packages/@uppy/companion/lib/config/companion.js
index 5931656..1839910 100644
--- a/packages/@uppy/companion/lib/config/companion.js
+++ b/packages/@uppy/companion/lib/config/companion.js
@@ -107,8 +107,8 @@ const validateConfig = (companionOptions) => {
       "startup.uploadUrls",
     );
   }
-  if (!companionOptions.oauthOrigin) {
-    throw new TypeError("Option oauthOrigin is required. To disable security, pass \"*\"");
+  if (companionOptions.corsOrigins == null) {
+    throw new TypeError("Option corsOrigins is required. To disable security, pass true");
   }
   if (
     periodicPingUrls != null && (!Array.isArray(periodicPingUrls)
diff --git a/packages/@uppy/companion/lib/server/controllers/connect.d.ts b/packages/@uppy/companion/lib/server/controllers/connect.d.ts
index e2b73d9..668b7e4 100644
--- a/packages/@uppy/companion/lib/server/controllers/connect.d.ts
+++ b/packages/@uppy/companion/lib/server/controllers/connect.d.ts
@@ -1,2 +1,2 @@
-declare function _exports(req: object, res: object): void;
+declare function _exports(req: object, res: object, next: any): void;
 export = _exports;
diff --git a/packages/@uppy/companion/lib/server/controllers/connect.js b/packages/@uppy/companion/lib/server/controllers/connect.js
index 374cd76..7d7d616 100644
--- a/packages/@uppy/companion/lib/server/controllers/connect.js
+++ b/packages/@uppy/companion/lib/server/controllers/connect.js
@@ -1,42 +1,28 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
 const oAuthState = require("../helpers/oauth-state");
-const queryString = (params, prefix = "?") => {
-  const str = new URLSearchParams(params).toString();
-  return str ? `${prefix}${str}` : "";
-};
 /**
- * initializes the oAuth flow for a provider.
- *
- * @param {object} req
- * @param {object} res
+ * Derived from `cors` npm package.
+ * @see https://github.com/expressjs/cors/blob/791983ebc0407115bc8ae8e64830d440da995938/lib/index.js#L19-L34
+ * @param {string} origin
+ * @param {*} allowedOrigins
+ * @returns {boolean}
  */
-module.exports = function connect(req, res) {
-  const { secret, oauthOrigin } = req.companion.options;
-  const stateObj = oAuthState.generateState();
-  // not sure if we need to store origin in the session state (e.g. we could've just gotten it directly inside send-token)
-  // but we're afraid to change the logic there
-  if (!Array.isArray(oauthOrigin)) {
-    // If the server only allows a single origin, we ignore the client-supplied
-    // origin from query because we don't need it.
-    stateObj.origin = oauthOrigin;
-  } else if (oauthOrigin.length < 2) {
-    // eslint-disable-next-line prefer-destructuring
-    stateObj.origin = oauthOrigin[0];
-  } else {
-    // If we have multiple allowed origins, we need to check the client-supplied origin from query.
-    // If the client provides an untrusted origin,
-    // we want to send `undefined`. `undefined` means `/`, which is the same origin when passed to `postMessage`.
-    // https://html.spec.whatwg.org/multipage/web-messaging.html#dom-window-postmessage-options-dev
-    const { origin } = JSON.parse(atob(req.query.state));
-    stateObj.origin = oauthOrigin.find(o => o === origin);
+function isOriginAllowed(origin, allowedOrigins) {
+  if (Array.isArray(allowedOrigins)) {
+    return allowedOrigins.some(allowedOrigin => isOriginAllowed(origin, allowedOrigin));
   }
-  if (req.companion.options.server.oauthDomain) {
-    stateObj.companionInstance = req.companion.buildURL("", true);
-  }
-  if (req.query.uppyPreAuthToken) {
-    stateObj.preAuthToken = req.query.uppyPreAuthToken;
+  if (typeof allowedOrigins === "string") {
+    return origin === allowedOrigins;
   }
+  return allowedOrigins.test?.(origin) ?? !!allowedOrigins;
+}
+const queryString = (params, prefix = "?") => {
+  const str = new URLSearchParams(params).toString();
+  return str ? `${prefix}${str}` : "";
+};
+function encodeStateAndRedirect(req, res, stateObj) {
+  const { secret } = req.companion.options;
   const state = oAuthState.encodeState(stateObj, secret);
   const { providerClass, providerGrantConfig } = req.companion;
   // pass along grant's dynamic config (if specified for the provider in its grant config `dynamic` section)
@@ -62,4 +48,62 @@ module.exports = function connect(req, res) {
   });
   // Now we redirect to grant's /connect endpoint, see `app.use(Grant(grantConfig))`
   res.redirect(req.companion.buildURL(`/connect/${oauthProvider}${qs}`, true));
+}
+function getClientOrigin(base64EncodedState) {
+  try {
+    const { origin } = JSON.parse(atob(base64EncodedState));
+    return origin;
+  } catch {
+    return undefined;
+  }
+}
+/**
+ * Initializes the oAuth flow for a provider.
+ *
+ * The client has open a new tab and is about to be redirected to the auth
+ * provider. When the user will return to companion, we'll have to send the auth
+ * token back to Uppy with `window.postMessage()`.
+ * To prevent other tabs and unauthorized origins from accessing that token, we
+ * reuse origin(s) from `corsOrigins` to limit the scope of `postMessage()`, which
+ * has `targetOrigin` parameter, required for cross-origin messages (i.e. if Uppy
+ * and Companion are served from different origins).
+ * We support multiple origins in `corsOrigins`, we have to figure out which
+ * origin the current connect request is coming from. Because the OAuth window
+ * was opened with `window.open()`, starting a new browsing context, the request
+ * is not cross origin and we don't have a `Origin` header to work with.
+ * That's why we use the client-provided base64-encoded parameter, check if it
+ * matches origin(s) allowed in `corsOrigins` Companion option, and use that as
+ * our `targetOrigin` for the `window.postMessage()` call (see `send-token.js`).
+ *
+ * @param {object} req
+ * @param {object} res
+ */
+module.exports = function connect(req, res, next) {
+  const stateObj = oAuthState.generateState();
+  if (req.companion.options.server.oauthDomain) {
+    stateObj.companionInstance = req.companion.buildURL("", true);
+  }
+  if (req.query.uppyPreAuthToken) {
+    stateObj.preAuthToken = req.query.uppyPreAuthToken;
+  }
+  // Get the computed header generated by `cors` in a previous middleware.
+  stateObj.origin = res.getHeader("Access-Control-Allow-Origin");
+  let clientOrigin;
+  if (!stateObj.origin && (clientOrigin = getClientOrigin(req.query.state))) {
+    const { corsOrigins } = req.companion.options;
+    if (typeof corsOrigins === "function") {
+      corsOrigins(clientOrigin, (err, finalOrigin) => {
+        if (err) {
+          next(err);
+        }
+        stateObj.origin = finalOrigin;
+        encodeStateAndRedirect(req, res, stateObj);
+      });
+      return;
+    }
+    if (isOriginAllowed(clientOrigin, req.companion.options.corsOrigins)) {
+      stateObj.origin = clientOrigin;
+    }
+  }
+  encodeStateAndRedirect(req, res, stateObj);
 };
diff --git a/packages/@uppy/companion/lib/server/controllers/index.d.ts b/packages/@uppy/companion/lib/server/controllers/index.d.ts
index a782333..044ac31 100644
--- a/packages/@uppy/companion/lib/server/controllers/index.d.ts
+++ b/packages/@uppy/companion/lib/server/controllers/index.d.ts
@@ -6,7 +6,7 @@ export let thumbnail: typeof import("./thumbnail");
 export let list: typeof import("./list");
 export let simpleAuth: typeof import("./simple-auth");
 export let logout: typeof import("./logout");
-export let connect: (req: any, res: any) => void;
+export let connect: (req: any, res: any, next: any) => void;
 export let preauth: typeof import("./preauth");
 export let redirect: (req: any, res: any) => void;
 export let refreshToken: typeof import("./refresh-token");
diff --git a/packages/@uppy/companion/lib/standalone/helper.js b/packages/@uppy/companion/lib/standalone/helper.js
index fa5d55f..5c3e44d 100644
--- a/packages/@uppy/companion/lib/standalone/helper.js
+++ b/packages/@uppy/companion/lib/standalone/helper.js
@@ -40,9 +40,18 @@ const hasProtocol = (url) => {
 const companionProtocol = process.env.COMPANION_PROTOCOL || "http";
 function getCorsOrigins() {
   if (process.env.COMPANION_CLIENT_ORIGINS) {
-    return process.env.COMPANION_CLIENT_ORIGINS
-      .split(",")
-      .map((url) => (hasProtocol(url) ? url : `${companionProtocol}://${url}`));
+    switch (process.env.COMPANION_CLIENT_ORIGINS) {
+      case "true":
+        return true;
+      case "false":
+        return false;
+      case "*":
+        return "*";
+      default:
+        return process.env.COMPANION_CLIENT_ORIGINS
+          .split(",")
+          .map((url) => (hasProtocol(url) ? url : `${companionProtocol}://${url}`));
+    }
   }
   if (process.env.COMPANION_CLIENT_ORIGINS_REGEX) {
     return new RegExp(process.env.COMPANION_CLIENT_ORIGINS_REGEX);
@@ -179,9 +188,6 @@ const getConfigFromEnv = () => {
     corsOrigins: getCorsOrigins(),
     testDynamicOauthCredentials: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS === "true",
     testDynamicOauthCredentialsSecret: process.env.COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS_SECRET,
-    oauthOrigin: process.env.COMPANION_OAUTH_ORIGIN?.includes(",")
-      ? process.env.COMPANION_OAUTH_ORIGIN.split(",")
-      : process.env.COMPANION_OAUTH_ORIGIN,
   };
 };
 /**

docs/companion.md Show resolved Hide resolved
@@ -6,8 +6,10 @@ These cover all the major Uppy versions and how to migrate to them.

- End-of-Life versions of Node.js are no longer supported (use latest 18.x LTS,
20.x LTS, or 22.x current).
- Setting the `oauthOrigin` option is now required. To get back to the unsafe
behavior of the previous version, set it to `'*'`.
- Setting the `corsOrigin` option is now required. You should define the list of
Copy link
Member

Choose a reason for hiding this comment

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

Let's link to this option in the docs

docs/companion.md Show resolved Hide resolved
Copy link
Member

@Murderlon Murderlon left a comment

Choose a reason for hiding this comment

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

Code LGTM.

packages/@uppy/companion/src/server/controllers/connect.js Outdated Show resolved Hide resolved
docs/companion.md Outdated Show resolved Hide resolved
@aduh95 aduh95 requested review from Murderlon and mifi July 8, 2024 14:40
aduh95 and others added 2 commits July 9, 2024 11:33
Co-authored-by: Mikael Finstad <finstaden@gmail.com>
@aduh95 aduh95 merged commit 8b3f1ea into 4.x Jul 9, 2024
20 checks passed
@aduh95 aduh95 deleted the cors-ftw branch July 9, 2024 10:00
@github-actions github-actions bot mentioned this pull request Jul 10, 2024
github-actions bot added a commit that referenced this pull request Jul 10, 2024
| Package                   | Version | Package                   | Version |
| ------------------------- | ------- | ------------------------- | ------- |
| @uppy/angular             |   0.7.0 | @uppy/onedrive            |   4.0.0 |
| @uppy/audio               |   2.0.0 | @uppy/progress-bar        |   4.0.0 |
| @uppy/aws-s3              |   4.0.0 | @uppy/provider-views      |   4.0.0 |
| @uppy/aws-s3-multipart    |   4.0.0 | @uppy/react               |   4.0.0 |
| @uppy/box                 |   3.0.0 | @uppy/react-native        |   0.6.0 |
| @uppy/companion           |   5.0.0 | @uppy/redux-dev-tools     |   4.0.0 |
| @uppy/companion-client    |   4.0.0 | @uppy/remote-sources      |   2.0.0 |
| @uppy/compressor          |   2.0.0 | @uppy/screen-capture      |   4.0.0 |
| @uppy/core                |   4.0.0 | @uppy/status-bar          |   4.0.0 |
| @uppy/dashboard           |   4.0.0 | @uppy/store-default       |   4.0.0 |
| @uppy/drag-drop           |   4.0.0 | @uppy/store-redux         |   4.0.0 |
| @uppy/drop-target         |   3.0.0 | @uppy/svelte              |   4.0.0 |
| @uppy/dropbox             |   4.0.0 | @uppy/thumbnail-generator |   4.0.0 |
| @uppy/facebook            |   4.0.0 | @uppy/transloadit         |   4.0.0 |
| @uppy/file-input          |   4.0.0 | @uppy/tus                 |   4.0.0 |
| @uppy/form                |   4.0.0 | @uppy/unsplash            |   4.0.0 |
| @uppy/golden-retriever    |   4.0.0 | @uppy/url                 |   4.0.0 |
| @uppy/google-drive        |   4.0.0 | @uppy/utils               |   6.0.0 |
| @uppy/google-photos       |   0.2.0 | @uppy/vue                 |   2.0.0 |
| @uppy/image-editor        |   3.0.0 | @uppy/webcam              |   4.0.0 |
| @uppy/informer            |   4.0.0 | @uppy/xhr-upload          |   4.0.0 |
| @uppy/instagram           |   4.0.0 | @uppy/zoom                |   3.0.0 |
| @uppy/locales             |   4.0.0 | uppy                      |   4.0.0 |

- meta: Bump docker/setup-qemu-action from 3.0.0 to 3.1.0 (dependabot[bot] / #5314)
- meta: Bump docker/build-push-action from 6.2.0 to 6.3.0 (dependabot[bot] / #5313)
- @uppy/core: bring back resetProgress (Merlijn Vos / #5320)
- docs: add note regarding `COMPANION_CLIENT_ORIGINS_REGEX` (Antoine du Hamel / #5322)
- @uppy/companion: make streaming upload default to `true` (Mikael Finstad / #5315)
- docs: change slug for aws docs (Merlijn Vos / #5321)
- @uppy/core: export UppyOptions, UppyFile, Meta, Body (Merlijn Vos / #5319)
- meta: fix React linter rules (Antoine du Hamel / #5317)
- meta: enforce use of extension in import type declarations (Antoine du Hamel / #5316)
- @uppy/companion: remove `oauthOrigin` (Antoine du Hamel / #5311)
- docs,@uppy/companion-client: don't close socket when pausing (Mikael Finstad / #4821)
- @uppy/aws-s3: fix signing on client for bucket name with dots (Antoine du Hamel / #5312)
- @uppy/react: introduce useUppyEvent (Merlijn Vos / #5264)
- @uppy/companion: do not list Node.js 20.12 as compatible (Antoine du Hamel / #5309)
- @uppy/provider-views: `.openFolder()` - return progress indication (Evgenia Karunus / #5306)
- examples,@uppy/companion: Release: uppy@3.27.3 (github-actions[bot] / #5304)
- @uppy/companion: fix `TypeError` when parsing request (Antoine du Hamel / #5303)
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.

3 participants