Skip to content

Commit

Permalink
Add support for custom domains (#609)
Browse files Browse the repository at this point in the history
- Add ability to pass in custom domains to Dropbox and DropboxAuth objects
- Refactor url generation to function based on hosts
  • Loading branch information
rogebrd authored Apr 12, 2021
1 parent b5631e4 commit 038089b
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 62 deletions.
4 changes: 4 additions & 0 deletions generator/typescript/index.d.tstemplate
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface DropboxAuthOptions {
clientSecret?: string;
// The fetch library for making requests.
fetch?: Function;
// A custom domain to use when making api requests. This should only be used for testing as scaffolding to avoid making network requests.
domain?: string;
}

export class DropboxAuth {
Expand Down Expand Up @@ -169,6 +171,8 @@ export interface DropboxOptions {
clientSecret?: string;
// The fetch library for making requests.
fetch?: Function;
// A custom domain to use when making api requests. This should only be used for testing as scaffolding to avoid making network requests.
domain?: string;
}

export class DropboxResponseError<T> {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dropbox",
"version": "9.4.0",
"version": "9.5.0",
"registry": "npm",
"description": "The Dropbox JavaScript SDK is a lightweight, promise based interface to the Dropbox v2 API that works in both nodejs and browser environments.",
"main": "cjs/index.js",
Expand Down Expand Up @@ -107,4 +107,4 @@
"dependencies": {
"node-fetch": "^2.6.1"
}
}
}
14 changes: 9 additions & 5 deletions src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
getTokenExpiresAtDate,
isBrowserEnv,
createBrowserSafeString,
OAuth2AuthorizationUrl,
OAuth2TokenUrl,
} from './utils.js';
import { parseResponse } from './response.js';

Expand Down Expand Up @@ -32,8 +34,6 @@ const PKCELength = 128;
const TokenAccessTypes = ['legacy', 'offline', 'online'];
const GrantTypes = ['code', 'token'];
const IncludeGrantedScopes = ['none', 'user', 'team'];
const BaseAuthorizeUrl = 'https://www.dropbox.com/oauth2/authorize';
const BaseTokenUrl = 'https://api.dropboxapi.com/oauth2/token';

/**
* @class DropboxAuth
Expand All @@ -49,6 +49,8 @@ const BaseTokenUrl = 'https://api.dropboxapi.com/oauth2/token';
* authentication URL.
* @arg {String} [options.clientSecret] - The client secret for your app. Used to create
* authentication URL and refresh access tokens.
* @arg {String} [options.domain] - A custom domain to use when making api requests. This
* should only be used for testing as scaffolding to avoid making network requests.
*/
export default class DropboxAuth {
constructor(options) {
Expand All @@ -60,6 +62,8 @@ export default class DropboxAuth {
this.refreshToken = options.refreshToken;
this.clientId = options.clientId;
this.clientSecret = options.clientSecret;

this.domain = options.domain;
}

/**
Expand Down Expand Up @@ -221,7 +225,7 @@ export default class DropboxAuth {
*/
getAuthenticationUrl(redirectUri, state, authType = 'token', tokenAccessType = null, scope = null, includeGrantedScopes = 'none', usePKCE = false) {
const clientId = this.getClientId();
const baseUrl = BaseAuthorizeUrl;
const baseUrl = OAuth2AuthorizationUrl(this.domain);

if (!clientId) {
throw new Error('A client id is required. You can set the client id using .setClientId().');
Expand Down Expand Up @@ -289,7 +293,7 @@ export default class DropboxAuth {
if (!clientId) {
throw new Error('A client id is required. You can set the client id using .setClientId().');
}
let path = BaseTokenUrl;
let path = OAuth2TokenUrl(this.domain);
path += '?grant_type=authorization_code';
path += `&code=${code}`;
path += `&client_id=${clientId}`;
Expand Down Expand Up @@ -339,7 +343,7 @@ export default class DropboxAuth {
* @returns {Promise<*>}
*/
refreshAccessToken(scope = null) {
let refreshUrl = BaseTokenUrl;
let refreshUrl = OAuth2TokenUrl(this.domain);
const clientId = this.getClientId();
const clientSecret = this.getClientSecret();

Expand Down
3 changes: 3 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export const APP_AUTH = 'app';
export const USER_AUTH = 'user';
export const TEAM_AUTH = 'team';
export const NO_AUTH = 'noauth';

export const DEFAULT_API_DOMAIN = 'dropboxapi.com';
export const DEFAULT_DOMAIN = 'dropbox.com';
12 changes: 8 additions & 4 deletions src/dropbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from './constants.js';
import { routes } from '../lib/routes.js';
import DropboxAuth from './auth.js';
import { getBaseURL, httpHeaderSafeJson } from './utils.js';
import { baseApiUrl, httpHeaderSafeJson } from './utils.js';
import { parseDownloadResponse, parseResponse } from './response.js';

let fetch;
Expand Down Expand Up @@ -46,6 +46,8 @@ const b64 = typeof btoa === 'undefined'
* authentication URL.
* @arg {String} [options.clientSecret] - The client secret for your app. Used to create
* authentication URL and refresh access tokens.
* @arg {String} [options.domain] - A custom domain to use when making api requests. This
* should only be used for testing as scaffolding to avoid making network requests.
*/
export default class Dropbox {
constructor(options) {
Expand All @@ -62,6 +64,8 @@ export default class Dropbox {
this.selectAdmin = options.selectAdmin;
this.pathRoot = options.pathRoot;

this.domain = options.domain;

Object.assign(this, routes);
}

Expand Down Expand Up @@ -125,7 +129,7 @@ export default class Dropbox {
this.setCommonHeaders(fetchOptions);
return fetchOptions;
})
.then((fetchOptions) => this.fetch(getBaseURL(host) + path, fetchOptions))
.then((fetchOptions) => this.fetch(baseApiUrl(host, this.domain) + path, fetchOptions))
.then((res) => parseResponse(res));
}

Expand All @@ -148,7 +152,7 @@ export default class Dropbox {

return fetchOptions;
})
.then((fetchOptions) => fetch(getBaseURL(host) + path, fetchOptions))
.then((fetchOptions) => fetch(baseApiUrl(host, this.domain) + path, fetchOptions))
.then((res) => parseDownloadResponse(res));
}

Expand Down Expand Up @@ -176,7 +180,7 @@ export default class Dropbox {

return fetchOptions;
})
.then((fetchOptions) => this.fetch(getBaseURL(host) + path, fetchOptions))
.then((fetchOptions) => this.fetch(baseApiUrl(host, this.domain) + path, fetchOptions))
.then((res) => parseResponse(res));
}

Expand Down
18 changes: 10 additions & 8 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { DEFAULT_API_DOMAIN, DEFAULT_DOMAIN } from './constants';

function getSafeUnicode(c) {
const unicode = `000${c.charCodeAt(0).toString(16)}`.slice(-4);
return `\\u${unicode}`;
}

export function getBaseURL(host) {
return `https://${host}.dropboxapi.com/2/`;
}
export const baseApiUrl = (subdomain, domain = DEFAULT_API_DOMAIN) => `https://${subdomain}.${domain}/2/`;
export const OAuth2AuthorizationUrl = (domain = DEFAULT_DOMAIN) => `https://${domain}/oauth2/authorize`;
export const OAuth2TokenUrl = (domain = DEFAULT_API_DOMAIN) => `https://api.${domain}/oauth2/token`;

// source https://www.dropboxforum.com/t5/API-support/HTTP-header-quot-Dropbox-API-Arg-quot-could-not-decode-input-as/m-p/173823/highlight/true#M6786
export function httpHeaderSafeJson(args) {
Expand All @@ -21,12 +23,12 @@ export function isWindowOrWorker() {
return (
(
typeof WorkerGlobalScope !== 'undefined'
&& self instanceof WorkerGlobalScope // eslint-disable-line no-restricted-globals
)
|| (
typeof module === 'undefined'
|| typeof window !== 'undefined'
&& self instanceof WorkerGlobalScope // eslint-disable-line no-restricted-globals
)
|| (
typeof module === 'undefined'
|| typeof window !== 'undefined'
)
);
}

Expand Down
82 changes: 43 additions & 39 deletions test/unit/auth.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,94 @@
import chai from 'chai';
import sinon from 'sinon';
import chaiAsPromised from 'chai-as-promised';
import { Dropbox, DropboxAuth } from '../../index.js';
import { DropboxAuth } from '../../index.js';

chai.use(chaiAsPromised);
describe('DropboxAuth', () => {
describe('accessToken', () => {
it('can be set in the constructor', () => {
const dbx = new Dropbox({ accessToken: 'foo' });
chai.assert.equal(dbx.auth.getAccessToken(), 'foo');
const dbx = new DropboxAuth({ accessToken: 'foo' });
chai.assert.equal(dbx.getAccessToken(), 'foo');
});

it('is undefined if not set in constructor', () => {
const dbx = new Dropbox();
chai.assert.equal(dbx.auth.getAccessToken(), undefined);
const dbx = new DropboxAuth();
chai.assert.equal(dbx.getAccessToken(), undefined);
});

it('can be set after being instantiated', () => {
const dbx = new Dropbox();
dbx.auth.setAccessToken('foo');
chai.assert.equal(dbx.auth.getAccessToken(), 'foo');
const dbx = new DropboxAuth();
dbx.setAccessToken('foo');
chai.assert.equal(dbx.getAccessToken(), 'foo');
});
});

describe('clientId', () => {
it('can be set in the constructor', () => {
const dbx = new Dropbox({ clientId: 'foo' });
chai.assert.equal(dbx.auth.getClientId(), 'foo');
const dbx = new DropboxAuth({ clientId: 'foo' });
chai.assert.equal(dbx.getClientId(), 'foo');
});

it('is undefined if not set in constructor', () => {
const dbx = new Dropbox();
chai.assert.equal(dbx.auth.getClientId(), undefined);
const dbx = new DropboxAuth();
chai.assert.equal(dbx.getClientId(), undefined);
});

it('can be set after being instantiated', () => {
const dbx = new Dropbox();
dbx.auth.setClientId('foo');
chai.assert.equal(dbx.auth.getClientId(), 'foo');
const dbx = new DropboxAuth();
dbx.setClientId('foo');
chai.assert.equal(dbx.getClientId(), 'foo');
});
});

describe('getAuthenticationUrl()', () => {
it('throws an error if the client id isn\'t set', () => {
const dbx = new Dropbox();
const dbx = new DropboxAuth();
chai.assert.throws(
DropboxAuth.prototype.getAuthenticationUrl.bind(dbx.auth, 'https://redirecturl.com'),
DropboxAuth.prototype.getAuthenticationUrl.bind(dbx, 'https://redirecturl.com'),
Error,
'A client id is required. You can set the client id using .setClientId().',
);
});

it('throws an error if the redirect url isn\'t set', () => {
const dbx = new Dropbox({ clientId: 'CLIENT_ID' });
const dbx = new DropboxAuth({ clientId: 'CLIENT_ID' });
chai.assert.throws(
DropboxAuth.prototype.getAuthenticationUrl.bind(dbx.auth),
DropboxAuth.prototype.getAuthenticationUrl.bind(dbx),
Error,
'A redirect uri is required.',
);
});

it('throws an error if the redirect url isn\'t set and type is code', () => {
const dbx = new Dropbox({ clientId: 'CLIENT_ID' });
const dbx = new DropboxAuth({ clientId: 'CLIENT_ID' });
return chai.expect(
dbx.auth.getAuthenticationUrl('', null, 'code'),
).to.eventually.deep.equal('https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=CLIENT_ID');
dbx.getAuthenticationUrl('', null, 'code'),
).to.eventually.deep.equal('https://dropbox.com/oauth2/authorize?response_type=code&client_id=CLIENT_ID');
});

const dbx = new Dropbox({ clientId: 'CLIENT_ID' });
it('changes the domain if a custom domain is set', () => {
const dbx = new DropboxAuth({
clientId: 'CLIENT_ID',
domain: 'mydomain.com',
});
dbx.getAuthenticationUrl('localhost', null, 'code')
.then((url) => {
chai.assertEqual(url, 'https://mydomain.com/oauth2/authorize?response_type=code&client_id=CLIENT_ID&redirect_uri=localhost');
});
});

const dbx = new DropboxAuth({ clientId: 'CLIENT_ID' });
for (const redirectUri of ['', 'localhost']) {
for (const state of ['', 'state']) {
for (const tokenAccessType of [null, 'legacy', 'offline', 'online']) {
for (const scope of [null, ['files.metadata.read', 'files.metadata.write']]) {
for (const includeGrantedScopes of ['none', 'user', 'team']) {
const input = {
redirectUri,
state,
tokenAccessType,
scope,
includeGrantedScopes,
};
it(`returns correct auth url with all combinations of valid input. redirectUri: ${redirectUri}, state: ${state},
tokenAccessType: ${tokenAccessType}, scope: ${scope}, includeGrantedScopes: ${includeGrantedScopes}`, (done) => {
dbx.auth.getAuthenticationUrl(redirectUri, state, 'code', tokenAccessType, scope, includeGrantedScopes) // eslint-disable-line no-await-in-loop
dbx.getAuthenticationUrl(redirectUri, state, 'code', tokenAccessType, scope, includeGrantedScopes) // eslint-disable-line no-await-in-loop
.then((url) => {
chai.assert(url.startsWith('https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=CLIENT_ID'));
chai.assert(url.startsWith('https://dropbox.com/oauth2/authorize?response_type=code&client_id=CLIENT_ID'));

if (redirectUri) {
chai.assert(url.includes(`&redirect_uri=${redirectUri}`));
Expand Down Expand Up @@ -128,19 +132,19 @@ describe('DropboxAuth', () => {

describe('clientSecret', () => {
it('can be set in the constructor', () => {
const dbx = new Dropbox({ clientSecret: 'foo' });
chai.assert.equal(dbx.auth.getClientSecret(), 'foo');
const dbx = new DropboxAuth({ clientSecret: 'foo' });
chai.assert.equal(dbx.getClientSecret(), 'foo');
});

it('is undefined if not set in constructor', () => {
const dbx = new Dropbox();
chai.assert.equal(dbx.auth.getClientSecret(), undefined);
const dbx = new DropboxAuth();
chai.assert.equal(dbx.getClientSecret(), undefined);
});

it('can be set after being instantiated', () => {
const dbx = new Dropbox();
dbx.auth.setClientSecret('foo');
chai.assert.equal(dbx.auth.getClientSecret(), 'foo');
const dbx = new DropboxAuth();
dbx.setClientSecret('foo');
chai.assert.equal(dbx.getClientSecret(), 'foo');
});
});

Expand Down
Loading

0 comments on commit 038089b

Please sign in to comment.