|
@@ -1,46 +1,30 @@
|
|
|
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 (typeof allowedOrigins === 'string'){
|
|
|
+ return origin === allowedOrigins;
|
|
|
}
|
|
|
+ return allowedOrigins.test?.(origin) ?? !!allowedOrigins;
|
|
|
+}
|
|
|
|
|
|
- if (req.query.uppyPreAuthToken) {
|
|
|
- stateObj.preAuthToken = req.query.uppyPreAuthToken
|
|
|
- }
|
|
|
|
|
|
+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
|
|
|
|
|
@@ -66,3 +50,66 @@ 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)
|
|
|
+}
|