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:

243 lines (222 loc) 7.79 kB
import { htmlEscape } from 'escape-goat' import got from 'got' import * as tokenService from '../helpers/jwt.js' import * as oAuthState from '../helpers/oauth-state.js' import { getRedirectPath, getURLBuilder } from '../helpers/utils.js' import logger from '../logger.js' // biome-ignore lint/correctness/noUnusedImports: It is used as a type import Provider from './Provider.js' /** * @param {string} url * @param {string} providerName * @param {object|null} credentialRequestParams - null asks for default credentials. */ async function fetchKeys(url, providerName, credentialRequestParams) { try { const { credentials } = await got .post(url, { json: { provider: providerName, parameters: credentialRequestParams }, }) .json() if (!credentials) throw new Error('Received no remote credentials') return credentials } catch (err) { logger.error(err, 'credentials.fetch.fail') throw err } } /** * Fetches for a providers OAuth credentials. If the config for that provider allows fetching * of the credentials via http, and the `credentialRequestParams` argument is provided, the oauth * credentials will be fetched via http. Otherwise, the credentials provided via companion options * will be used instead. * * @param {string} providerName the name of the provider whose oauth keys we want to fetch (e.g onedrive) * @param {object} companionOptions the companion options object * @param {object} credentialRequestParams the params that should be sent if an http request is required. */ async function fetchProviderKeys( providerName, companionOptions, credentialRequestParams, ) { let providerConfig = companionOptions.providerOptions[providerName] if (!providerConfig) { providerConfig = companionOptions.customProviders[providerName]?.config } if (!providerConfig) { return null } if (!providerConfig.credentialsURL) { return providerConfig } // If a default key is configured, do not ask the credentials endpoint for it. // In a future version we could make this an XOR thing, providing either an endpoint or global keys, // but not both. if (!credentialRequestParams && providerConfig.key) { return providerConfig } return fetchKeys( providerConfig.credentialsURL, providerName, credentialRequestParams || null, ) } /** * Returns a request middleware function that can be used to pre-fetch a provider's * Oauth credentials before the request is passed to the Oauth handler (https://github.com/simov/grant in this case). * * @param {Record<string, typeof Provider>} providers provider classes enabled for this server * @param {object} companionOptions companion options object * @returns {import('express').RequestHandler} */ export const getCredentialsOverrideMiddleware = ( providers, companionOptions, ) => { return async (req, res, next) => { try { const { oauthProvider, override } = req.params const [providerName] = Object.keys(providers).filter( (name) => providers[name].oauthProvider === oauthProvider, ) if (!providerName) { next() return } if (!companionOptions.providerOptions[providerName]?.credentialsURL) { next() return } const grantDynamic = oAuthState.getGrantDynamicFromRequest(req) // only use state via session object if user isn't making intial "connect" request. // override param indicates subsequent requests from the oauth flow const state = override ? grantDynamic.state : req.query.state if (!state) { next() return } const preAuthToken = oAuthState.getFromState( state, 'preAuthToken', companionOptions.secret, ) if (!preAuthToken) { next() return } let payload try { payload = tokenService.verifyEncryptedToken( preAuthToken, companionOptions.preAuthSecret, ) } catch (_err) { next() return } 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, }, } if (credentials.transloadit_gateway) { const redirectPath = getRedirectPath(providerName) const fullRedirectPath = getURLBuilder(companionOptions)( redirectPath, true, true, ) const redirectUri = new URL( fullRedirectPath, credentials.transloadit_gateway, ).toString() logger.info('Using redirect URI from transloadit_gateway', redirectUri) res.locals.grant.dynamic.redirect_uri = redirectUri } next() } catch (keyErr) { res.send(` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <h1>Could not fetch credentials</h1> <p> This is probably an Uppy configuration issue. Check that your Transloadit key is correct, and that the configured <code>credentialsName</code> for this remote provider matches the name you gave it in the Template Credentials setup on the Transloadit side. </p> <p>Internal error message: ${htmlEscape(keyErr.message)}</p> </body> </html> `) } } } /** * Returns a request scoped function that can be used to get a provider's oauth credentials * through out the lifetime of the request. * * @param {string} providerName the name of the provider attached to the scope of the request * @param {object} companionOptions the companion options object * @param {object} req the express request object for the said request * @returns {(providerName: string, companionOptions: object, credentialRequestParams?: object) => Promise} */ export const getCredentialsResolver = (providerName, companionOptions, req) => { const credentialsResolver = () => { const encodedCredentialsParams = req.header('uppy-credentials-params') let credentialRequestParams = null if (encodedCredentialsParams) { try { credentialRequestParams = JSON.parse( atob(encodedCredentialsParams), ).params } catch (error) { logger.error(error, 'credentials.resolve.fail', req.id) } } return fetchProviderKeys( providerName, companionOptions, credentialRequestParams, ) } return credentialsResolver }