@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:
186 lines (185 loc) • 8.38 kB
JavaScript
;
const { htmlEscape } = require('escape-goat');
const logger = require('../logger');
const oAuthState = require('../helpers/oauth-state');
const tokenService = require('../helpers/jwt');
const { getURLBuilder, getRedirectPath } = require('../helpers/utils');
// biome-ignore lint/correctness/noUnusedVariables: used in types
const Provider = require('./Provider');
const got = require('../got');
/**
* @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 (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}
*/
exports.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) {
// TODO we should return an html page here that can communicate the error
// back to the Uppy client, just like /send-token does
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}
*/
module.exports.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;
};