Skip to content

Commit e733cd9

Browse files
authored
feat(auth): refresh access token request using body params (#890)
* feat(auth): refresh access token request using body params When refreshing authorization access token, send the end point parameters via body instead of URL. This prevents intermediate proxies or logs to accidentally (or not) expose secret values. Fix #813 [docs]: https://www.dropbox.com/developers/documentation/http/documentation#oauth2-token
1 parent 695271c commit e733cd9

File tree

4 files changed

+128
-29
lines changed

4 files changed

+128
-29
lines changed

generator/typescript/index.d.tstemplate

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface DropboxAuthOptions {
2222
domainDelimiter?: string;
2323
// An object (in the form of header: value) designed to set custom headers to use during a request.
2424
customHeaders?: object;
25+
// Whether request data is sent on body or as URL params. Defaults to false.
26+
dataOnBody?: boolean;
2527
}
2628

2729
export class DropboxAuth {

src/auth.js

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const IncludeGrantedScopes = ['none', 'user', 'team'];
5555
* subdomain. This should only be used for testing as scaffolding.
5656
* @arg {Object} [options.customHeaders] - An object (in the form of header: value) designed to set
5757
* custom headers to use during a request.
58+
* @arg {Boolean} [options.dataOnBody] - Whether request data is sent on body or as URL params.
59+
* Defaults to false.
5860
*/
5961
export default class DropboxAuth {
6062
constructor(options) {
@@ -70,6 +72,7 @@ export default class DropboxAuth {
7072
this.domain = options.domain;
7173
this.domainDelimiter = options.domainDelimiter;
7274
this.customHeaders = options.customHeaders;
75+
this.dataOnBody = options.dataOnBody;
7376
}
7477

7578
/**
@@ -349,7 +352,6 @@ export default class DropboxAuth {
349352
* @returns {Promise<*>}
350353
*/
351354
refreshAccessToken(scope = null) {
352-
let refreshUrl = OAuth2TokenUrl(this.domain, this.domainDelimiter);
353355
const clientId = this.getClientId();
354356
const clientSecret = this.getClientSecret();
355357

@@ -360,21 +362,33 @@ export default class DropboxAuth {
360362
throw new Error('Scope must be an array of strings');
361363
}
362364

363-
const headers = {};
364-
headers['Content-Type'] = 'application/json';
365-
refreshUrl += `?grant_type=refresh_token&refresh_token=${this.getRefreshToken()}`;
366-
refreshUrl += `&client_id=${clientId}`;
367-
if (clientSecret) {
368-
refreshUrl += `&client_secret=${clientSecret}`;
369-
}
370-
if (scope) {
371-
refreshUrl += `&scope=${scope.join(' ')}`;
372-
}
365+
let refreshUrl = OAuth2TokenUrl(this.domain, this.domainDelimiter);
373366
const fetchOptions = {
367+
headers: { 'Content-Type': 'application/json' },
374368
method: 'POST',
375369
};
376370

377-
fetchOptions.headers = headers;
371+
if (this.dataOnBody) {
372+
const body = { grant_type: 'refresh_token', client_id: clientId, refresh_token: this.getRefreshToken() };
373+
374+
if (clientSecret) {
375+
body.client_secret = clientSecret;
376+
}
377+
if (scope) {
378+
body.scope = scope.join(' ');
379+
}
380+
381+
fetchOptions.body = body;
382+
} else {
383+
refreshUrl += `?grant_type=refresh_token&refresh_token=${this.getRefreshToken()}`;
384+
refreshUrl += `&client_id=${clientId}`;
385+
if (clientSecret) {
386+
refreshUrl += `&client_secret=${clientSecret}`;
387+
}
388+
if (scope) {
389+
refreshUrl += `&scope=${scope.join(' ')}`;
390+
}
391+
}
378392

379393
return this.fetch(refreshUrl, fetchOptions)
380394
.then((res) => parseResponse(res))

test/unit/auth.js

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,18 @@ describe('DropboxAuth', () => {
160160
});
161161
});
162162

163+
describe('dataOnBody', () => {
164+
it('can be set in the constructor', () => {
165+
const dbx = new DropboxAuth({ dataOnBody: true });
166+
chai.assert.equal(dbx.dataOnBody, true);
167+
});
168+
169+
it('is undefined if not set in constructor', () => {
170+
const dbx = new DropboxAuth();
171+
chai.assert.equal(dbx.dataOnBody, undefined);
172+
});
173+
});
174+
163175
describe('refreshToken', () => {
164176
it('can be set in the constructor', () => {
165177
const dbxAuth = new DropboxAuth({ refreshToken: 'foo' });
@@ -366,9 +378,7 @@ describe('DropboxAuth', () => {
366378
);
367379
});
368380

369-
const testRefreshUrl = 'https://api.dropboxapi.com/oauth2/token?grant_type=refresh_token&refresh_token=undefined&client_id=foo&client_secret=bar';
370-
371-
it('sets the correct refresh url (no scope passed)', () => {
381+
it('sets request content type to json', () => {
372382
const dbxAuth = new DropboxAuth({
373383
clientId: 'foo',
374384
clientSecret: 'bar',
@@ -377,26 +387,97 @@ describe('DropboxAuth', () => {
377387
const fetchSpy = sinon.spy(dbxAuth, 'fetch');
378388
dbxAuth.refreshAccessToken();
379389
chai.assert.isTrue(fetchSpy.calledOnce);
380-
const refreshUrl = dbxAuth.fetch.getCall(0).args[0];
390+
381391
const { headers } = dbxAuth.fetch.getCall(0).args[1];
382-
chai.assert.equal(refreshUrl, testRefreshUrl);
383392
chai.assert.equal(headers['Content-Type'], 'application/json');
384393
});
385394

386-
it('sets the correct refresh url (scope passed)', () => {
387-
const dbxAuth = new DropboxAuth({
388-
clientId: 'foo',
389-
clientSecret: 'bar',
395+
describe('when dataOnBody flag is enabled', () => {
396+
const dataOnBody = true;
397+
398+
it('does the request without URL parameters', () => {
399+
const dbxAuth = new DropboxAuth({
400+
clientId: 'foo',
401+
clientSecret: 'bar',
402+
dataOnBody,
403+
});
404+
405+
const fetchSpy = sinon.spy(dbxAuth, 'fetch');
406+
dbxAuth.refreshAccessToken(['files.metadata.read']);
407+
chai.assert.isTrue(fetchSpy.calledOnce);
408+
const refreshUrl = dbxAuth.fetch.getCall(0).args[0];
409+
410+
chai.assert.equal(refreshUrl, 'https://api.dropboxapi.com/oauth2/token');
390411
});
391412

392-
const fetchSpy = sinon.spy(dbxAuth, 'fetch');
393-
dbxAuth.refreshAccessToken(['files.metadata.read']);
394-
chai.assert.isTrue(fetchSpy.calledOnce);
395-
const refreshUrl = dbxAuth.fetch.getCall(0).args[0];
396-
const { headers } = dbxAuth.fetch.getCall(0).args[1];
397-
const testScopeUrl = `${testRefreshUrl}&scope=files.metadata.read`;
398-
chai.assert.equal(refreshUrl, testScopeUrl);
399-
chai.assert.equal(headers['Content-Type'], 'application/json');
413+
it('sends the client id and secret in the body', () => {
414+
const dbxAuth = new DropboxAuth({
415+
clientId: 'foo',
416+
clientSecret: 'bar',
417+
dataOnBody,
418+
});
419+
420+
const fetchSpy = sinon.spy(dbxAuth, 'fetch');
421+
dbxAuth.refreshAccessToken();
422+
chai.assert.isTrue(fetchSpy.calledOnce);
423+
424+
const { body } = dbxAuth.fetch.getCall(0).args[1];
425+
chai.assert.equal(body.client_id, 'foo');
426+
chai.assert.equal(body.client_secret, 'bar');
427+
});
428+
429+
it('sends the scope in the body when passed', () => {
430+
const dbxAuth = new DropboxAuth({
431+
clientId: 'foo',
432+
clientSecret: 'bar',
433+
dataOnBody,
434+
});
435+
436+
const fetchSpy = sinon.spy(dbxAuth, 'fetch');
437+
dbxAuth.refreshAccessToken(['files.metadata.read']);
438+
chai.assert.isTrue(fetchSpy.calledOnce);
439+
440+
const { body } = dbxAuth.fetch.getCall(0).args[1];
441+
chai.assert.equal(body.scope, 'files.metadata.read');
442+
});
443+
});
444+
445+
describe('when dataOnBody flag is disabled', () => {
446+
const dataOnBody = false;
447+
const testRefreshUrl = 'https://api.dropboxapi.com/oauth2/token?grant_type=refresh_token&refresh_token=undefined&client_id=foo&client_secret=bar';
448+
449+
it('sets the correct refresh url (no scope passed)', () => {
450+
const dbxAuth = new DropboxAuth({
451+
clientId: 'foo',
452+
clientSecret: 'bar',
453+
dataOnBody,
454+
});
455+
456+
const fetchSpy = sinon.spy(dbxAuth, 'fetch');
457+
dbxAuth.refreshAccessToken();
458+
chai.assert.isTrue(fetchSpy.calledOnce);
459+
const refreshUrl = dbxAuth.fetch.getCall(0).args[0];
460+
const { headers } = dbxAuth.fetch.getCall(0).args[1];
461+
chai.assert.equal(refreshUrl, testRefreshUrl);
462+
chai.assert.equal(headers['Content-Type'], 'application/json');
463+
});
464+
465+
it('sets the correct refresh url (scope passed)', () => {
466+
const dbxAuth = new DropboxAuth({
467+
clientId: 'foo',
468+
clientSecret: 'bar',
469+
dataOnBody,
470+
});
471+
472+
const fetchSpy = sinon.spy(dbxAuth, 'fetch');
473+
dbxAuth.refreshAccessToken(['files.metadata.read']);
474+
chai.assert.isTrue(fetchSpy.calledOnce);
475+
const refreshUrl = dbxAuth.fetch.getCall(0).args[0];
476+
const { headers } = dbxAuth.fetch.getCall(0).args[1];
477+
const testScopeUrl = `${testRefreshUrl}&scope=files.metadata.read`;
478+
chai.assert.equal(refreshUrl, testScopeUrl);
479+
chai.assert.equal(headers['Content-Type'], 'application/json');
480+
});
400481
});
401482
});
402483
});

types/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface DropboxAuthOptions {
2323
domainDelimiter?: string;
2424
// An object (in the form of header: value) designed to set custom headers to use during a request.
2525
customHeaders?: object;
26+
// Whether request data is sent on body or as URL params. Defaults to false.
27+
dataOnBody?: boolean;
2628
}
2729

2830
export class DropboxAuth {

0 commit comments

Comments
 (0)