UNPKG

@uppy/companion

Version:

OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:

106 lines (105 loc) 4.58 kB
import * as oAuthState from '../helpers/oauth-state.js'; /** * 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} */ function isOriginAllowed(origin, allowedOrigins) { if (Array.isArray(allowedOrigins)) { return allowedOrigins.some((allowedOrigin) => isOriginAllowed(origin, allowedOrigin)); } 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) // this is needed for things like custom oauth domain (e.g. webdav) const grantDynamicConfig = Object.fromEntries(providerGrantConfig.dynamic?.flatMap((dynamicKey) => { const queryValue = req.query[dynamicKey]; // note: when using credentialsURL (dynamic oauth credentials), dynamic has ['key', 'secret', 'redirect_uri'] // but in that case, query string is empty, so we need to only fetch these parameters from QS if they exist. if (!queryValue) return []; return [[dynamicKey, queryValue]]; }) || []); const { oauthProvider } = providerClass; const qs = queryString({ ...grantDynamicConfig, state, }); // 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 */ export default 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); } if (!stateObj.origin && clientOrigin) { 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); } export { isOriginAllowed };