diff --git a/packages/@uppy/companion/src/companion.js b/packages/@uppy/companion/src/companion.js index fd53ebaaf2..9a4be5c37b 100644 --- a/packages/@uppy/companion/src/companion.js +++ b/packages/@uppy/companion/src/companion.js @@ -165,6 +165,7 @@ module.exports.app = (optionsArg = {}) => { key, secret, redirect_uri: getRedirectUri(), + origins: ['http://localhost:5173'], }, }) }) diff --git a/packages/@uppy/companion/src/server/controllers/connect.js b/packages/@uppy/companion/src/server/controllers/connect.js index f2f835e954..154ba67046 100644 --- a/packages/@uppy/companion/src/server/controllers/connect.js +++ b/packages/@uppy/companion/src/server/controllers/connect.js @@ -3,8 +3,8 @@ const oAuthState = require('../helpers/oauth-state') /** * Derived from `cors` npm package. * @see https://github.com/expressjs/cors/blob/791983ebc0407115bc8ae8e64830d440da995938/lib/index.js#L19-L34 - * @param {string} origin - * @param {*} allowedOrigins + * @param {string} origin + * @param {*} allowedOrigins * @returns {boolean} */ function isOriginAllowed(origin, allowedOrigins) { @@ -17,7 +17,6 @@ function isOriginAllowed(origin, allowedOrigins) { return allowedOrigins.test?.(origin) ?? !!allowedOrigins; } - const queryString = (params, prefix = '?') => { const str = new URLSearchParams(params).toString() return str ? `${prefix}${str}` : '' @@ -66,7 +65,7 @@ function getClientOrigin(base64EncodedState) { * * 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()`. + * 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 @@ -113,3 +112,4 @@ module.exports = function connect(req, res, next) { } encodeStateAndRedirect(req, res, stateObj) } +module.exports.isOriginAllowed = isOriginAllowed diff --git a/packages/@uppy/companion/src/server/controllers/send-token.js b/packages/@uppy/companion/src/server/controllers/send-token.js index 2ac5489691..290aaba4f0 100644 --- a/packages/@uppy/companion/src/server/controllers/send-token.js +++ b/packages/@uppy/companion/src/server/controllers/send-token.js @@ -1,4 +1,5 @@ const serialize = require('serialize-javascript') +const { isOriginAllowed } = require('./connect') const oAuthState = require('../helpers/oauth-state') @@ -46,18 +47,30 @@ const htmlContent = (token, origin) => { /** * - * @param {object} req - * @param {object} res - * @param {Function} next + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next */ -module.exports = function sendToken (req, res, next) { - const uppyAuthToken = req.companion.authToken +module.exports = function sendToken(req, res, next) { + // @ts-expect-error untyped + const { companion } = req + const uppyAuthToken = companion.authToken const { state } = oAuthState.getGrantDynamicFromRequest(req) - if (state) { - const origin = oAuthState.getFromState(state, 'origin', req.companion.options.secret) - res.send(htmlContent(uppyAuthToken, origin)) - return + + if (!state) { + return next() } - next() + + const clientOrigin = oAuthState.getFromState(state, 'origin', companion.options.secret) + const customerDefinedAllowedOrigins = oAuthState.getFromState(state, 'customerDefinedAllowedOrigins', companion.options.secret) + + if ( + customerDefinedAllowedOrigins && + !isOriginAllowed(clientOrigin, customerDefinedAllowedOrigins) + ) { + return next() + } + + return res.send(htmlContent(uppyAuthToken, clientOrigin)) } diff --git a/packages/@uppy/companion/src/server/provider/credentials.js b/packages/@uppy/companion/src/server/provider/credentials.js index e716e92251..d962849dd8 100644 --- a/packages/@uppy/companion/src/server/provider/credentials.js +++ b/packages/@uppy/companion/src/server/provider/credentials.js @@ -108,10 +108,32 @@ exports.getCredentialsOverrideMiddleware = (providers, companionOptions) => { const credentials = await fetchProviderKeys(providerName, companionOptions, payload) + // Besides the key and secret the fetched credentials can also contain `origins`, + // which is an array of strings of allowed origins to prevent any origin from getting the OAuth + // token through window.postMessage (see comment in connect.js). + // postMessage happens in send-token.js, which is a different request, so we need to put the allowed origins + // on the encrypted session state to access it later there. + if (Array.isArray(credentials.origins) && credentials.origins.length > 0) { + const decodedState = oAuthState.decodeState(state, companionOptions.secret) + decodedState.customerDefinedAllowedOrigins = credentials.origins + const newState = oAuthState.encodeState(decodedState, companionOptions.secret) + // @ts-expect-error untyped + req.session.grant = { + // @ts-expect-error untyped + ...req.session.grant, + dynamic: { + // @ts-expect-error untyped + ...req.session.grant?.dynamic, + state: newState, + }, + } + } + res.locals.grant = { dynamic: { key: credentials.key, secret: credentials.secret, + origins: credentials.origins, }, } diff --git a/packages/@uppy/companion/src/server/provider/index.js b/packages/@uppy/companion/src/server/provider/index.js index 64449d1968..746782b2a8 100644 --- a/packages/@uppy/companion/src/server/provider/index.js +++ b/packages/@uppy/companion/src/server/provider/index.js @@ -137,7 +137,7 @@ module.exports.addProviderOptions = (companionOptions, grantConfig, getOauthProv grantConfig[oauthProvider].secret = providerOptions[providerName].secret if (providerOptions[providerName].credentialsURL) { // eslint-disable-next-line no-param-reassign - grantConfig[oauthProvider].dynamic = ['key', 'secret', 'redirect_uri'] + grantConfig[oauthProvider].dynamic = ['key', 'secret', 'redirect_uri', 'origins'] } const provider = exports.getDefaultProviders()[providerName]