diff options
author | jamiewalch <jamiewalch@chromium.org> | 2015-03-18 14:38:25 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2015-03-18 21:39:24 +0000 |
commit | e13a9d574fb9c9a4d0f36351e0e91e89b4e17928 (patch) | |
tree | 411f4772a963879748f1f7c02fc4ba796b44d24e /remoting | |
parent | a0e8a714c577e98e26a1194c40b98ec0854c1c32 (diff) | |
download | chromium_src-e13a9d574fb9c9a4d0f36351e0e91e89b4e17928.zip chromium_src-e13a9d574fb9c9a4d0f36351e0e91e89b4e17928.tar.gz chromium_src-e13a9d574fb9c9a4d0f36351e0e91e89b4e17928.tar.bz2 |
Add optional scopes parameter to Identity.getToken().
BUG=b/13306957
Review URL: https://codereview.chromium.org/1015043002
Cr-Commit-Position: refs/heads/master@{#321211}
Diffstat (limited to 'remoting')
-rw-r--r-- | remoting/webapp/crd/js/identity.js | 66 | ||||
-rw-r--r-- | remoting/webapp/crd/js/identity_unittest.js | 54 | ||||
-rw-r--r-- | remoting/webapp/js_proto/chrome_mocks.js | 7 | ||||
-rw-r--r-- | remoting/webapp/js_proto/sinon_proto.js | 3 |
4 files changed, 104 insertions, 26 deletions
diff --git a/remoting/webapp/crd/js/identity.js b/remoting/webapp/crd/js/identity.js index e940dac..742a552 100644 --- a/remoting/webapp/crd/js/identity.js +++ b/remoting/webapp/crd/js/identity.js @@ -31,8 +31,8 @@ remoting.Identity = function(opt_consentDialog) { this.email_ = ''; /** @private {string} */ this.fullName_ = ''; - /** @type {base.Deferred<string>} */ - this.authTokenDeferred_ = null; + /** @private {Object<base.Deferred<string>>} */ + this.authTokensDeferred_ = {}; /** @private {boolean} */ this.interactive_ = false; }; @@ -55,20 +55,29 @@ remoting.Identity.ConsentDialog.prototype.show = function() {}; /** * Gets an access token. * + * @param {Array<string>=} opt_scopes Optional OAuth2 scopes to request. If not + * specified, the scopes specified in the manifest will be used. No consent + * prompt will be needed as long as the requested scopes are a subset of + * those already granted (in most cases, the remoting.Application framework + * ensures that the scopes specified in the manifest are already authorized + * before any application code is executed). Callers can request scopes not + * specified in the manifest, but a consent prompt will be shown. + * * @return {!Promise<string>} A promise resolved with an access token * or rejected with a remoting.Error. */ -remoting.Identity.prototype.getToken = function() { - /** @const */ - var that = this; - - if (this.authTokenDeferred_ == null) { - this.authTokenDeferred_ = new base.Deferred(); - chrome.identity.getAuthToken( - { 'interactive': this.interactive_ }, - this.onAuthComplete_.bind(this)); +remoting.Identity.prototype.getToken = function(opt_scopes) { + var key = getScopesKey(opt_scopes); + if (!this.authTokensDeferred_[key]) { + this.authTokensDeferred_[key] = new base.Deferred(); + var options = { + 'interactive': this.interactive_, + 'scopes': opt_scopes + }; + chrome.identity.getAuthToken(options, + this.onAuthComplete_.bind(this, opt_scopes)); } - return this.authTokenDeferred_.promise(); + return this.authTokensDeferred_[key].promise(); }; /** @@ -167,16 +176,20 @@ remoting.Identity.prototype.getEmail = function() { /** * Callback for the getAuthToken API. * + * @param {Array<string>|undefined} scopes The explicit scopes passed to + * getToken, or undefined if no scopes were specified. * @param {?string} token The auth token, or null if the request failed. * @private */ -remoting.Identity.prototype.onAuthComplete_ = function(token) { - var authTokenDeferred = this.authTokenDeferred_; +remoting.Identity.prototype.onAuthComplete_ = function(scopes, token) { + var key = getScopesKey(scopes); + var authTokenDeferred = this.authTokensDeferred_[key]; // Pass the token to the callback(s) if it was retrieved successfully. if (token) { - authTokenDeferred.resolve(token); - this.authTokenDeferred_ = null; + var promise = this.authTokensDeferred_[key]; + delete this.authTokensDeferred_[key]; + promise.resolve(token); return; } @@ -190,8 +203,8 @@ remoting.Identity.prototype.onAuthComplete_ = function(token) { var error = (error_message == USER_CANCELLED) ? new remoting.Error(remoting.Error.Tag.CANCELLED) : new remoting.Error(remoting.Error.Tag.NOT_AUTHENTICATED); - authTokenDeferred.reject(error); - this.authTokenDeferred_ = null; + this.authTokensDeferred_[key].reject(error); + delete this.authTokensDeferred_[key]; return; } @@ -202,8 +215,12 @@ remoting.Identity.prototype.onAuthComplete_ = function(token) { (this.consentDialog_) ? this.consentDialog_.show() : Promise.resolve(); showConsentDialog.then(function() { that.interactive_ = true; - chrome.identity.getAuthToken({'interactive': that.interactive_}, - that.onAuthComplete_.bind(that)); + var options = { + 'interactive': that.interactive_, + 'scopes': scopes + }; + chrome.identity.getAuthToken(options, + that.onAuthComplete_.bind(that, scopes)); }); }; @@ -216,4 +233,13 @@ remoting.Identity.prototype.isAuthenticated = function() { return remoting.identity.email_ !== ''; }; + +/** + * @param {Array<string>=} opt_scopes + * @return {string} + */ +function getScopesKey(opt_scopes) { + return opt_scopes ? JSON.stringify(opt_scopes) : ''; +} + })(); diff --git a/remoting/webapp/crd/js/identity_unittest.js b/remoting/webapp/crd/js/identity_unittest.js index 3971e61..a187129 100644 --- a/remoting/webapp/crd/js/identity_unittest.js +++ b/remoting/webapp/crd/js/identity_unittest.js @@ -23,8 +23,6 @@ var identity = null; var MockConsent = function(assert) { /** @type {boolean} */ this.grantConsent = true; - /** @type {Array<string> | undefined} */ - this.scopes = undefined; /** @private {QUnit.Assert} */ this.assert_ = assert; }; @@ -34,7 +32,8 @@ MockConsent.prototype.show = function() { // with {interactive: false} failed, and it should occur before any call with // {interactive: true}. this.assert_.ok(getAuthToken.calledOnce); - this.assert_.ok(getAuthToken.calledWith({'interactive': false})); + this.assert_.ok(getAuthToken.calledWith( + {'interactive': false, scopes: undefined})); getAuthToken.reset(); if (this.grantConsent) { @@ -64,7 +63,8 @@ QUnit.test('consent is requested only on first invocation', function(assert) { function(/** string */ token) { assert.ok(promptForConsent.called); assert.ok(getAuthToken.calledOnce); - assert.ok(getAuthToken.calledWith({'interactive': true})); + assert.ok(getAuthToken.calledWith( + {'interactive': true, 'scopes': undefined})); // Request another token. promptForConsent.reset(); @@ -74,11 +74,55 @@ QUnit.test('consent is requested only on first invocation', function(assert) { }).then(function(/** string */ token) { assert.ok(!promptForConsent.called); assert.ok(getAuthToken.calledOnce); - assert.ok(getAuthToken.calledWith({'interactive': true})); + assert.ok(getAuthToken.calledWith({ + 'interactive': true, 'scopes': undefined})); assert.equal(token, 'token'); }); }); +QUnit.test('requesting an explicit scope works', function(assert) { + assert.ok(!promptForConsent.called); + return identity.getToken().then( + function() { + // Request a token with an explicit scope. + promptForConsent.reset(); + getAuthToken.reset(); + return identity.getToken(['scope']); + + }).then(function(/** string */ token) { + assert.ok(!promptForConsent.called); + assert.ok(getAuthToken.calledOnce); + assert.ok(getAuthToken.calledWith({ + 'interactive': true, 'scopes': ['scope']})); + assert.equal(token, 'token["scope"]'); + }); +}); + +QUnit.test('multiple concurrent outstanding requests are handled correctly', + function(assert) { + assert.ok(!promptForConsent.called); + return identity.getToken().then( + function() { + // Request a token with an explicit scope and another without. + promptForConsent.reset(); + getAuthToken.reset(); + var withScope = identity.getToken(['scope']); + var withoutScope = identity.getToken(); + return Promise.all([withScope, withoutScope]); + + }).then(function(/** Array<string> */ tokens) { + assert.ok(!promptForConsent.called); + assert.ok(getAuthToken.calledTwice); + assert.ok(getAuthToken.calledWith({ + 'interactive': true, 'scopes': ['scope']})); + assert.ok(getAuthToken.calledWith({ + 'interactive': true, 'scopes': undefined})); + assert.equal(tokens.length, 2); + assert.equal(tokens[0], 'token["scope"]'); + assert.equal(tokens[1], 'token'); + }); +}); + QUnit.test('cancellations are reported correctly', function(assert) { consentDialog.grantConsent = false; chromeMocks.runtime.lastError.message = 'The user did not approve access.'; diff --git a/remoting/webapp/js_proto/chrome_mocks.js b/remoting/webapp/js_proto/chrome_mocks.js index 3b404b6..e64421d 100644 --- a/remoting/webapp/js_proto/chrome_mocks.js +++ b/remoting/webapp/js_proto/chrome_mocks.js @@ -192,8 +192,13 @@ chromeMocks.Identity = function() { * @param {function(string=):void} callback */ chromeMocks.Identity.prototype.getAuthToken = function(options, callback) { + // Append the 'scopes' array, if present, to the dummy token. + var token = this.token_; + if (token !== undefined && options['scopes'] !== undefined) { + token += JSON.stringify(options['scopes']); + } // Don't use setTimeout because sinon mocks it. - window.requestAnimationFrame(callback.bind(null, this.token_)); + window.requestAnimationFrame(callback.bind(null, token)); }; /** @param {string} token */ diff --git a/remoting/webapp/js_proto/sinon_proto.js b/remoting/webapp/js_proto/sinon_proto.js index d7e1a2f..ff7eb96 100644 --- a/remoting/webapp/js_proto/sinon_proto.js +++ b/remoting/webapp/js_proto/sinon_proto.js @@ -86,6 +86,9 @@ sinon.Spy.prototype.called = false; /** @type {boolean} */ sinon.Spy.prototype.calledOnce = false; +/** @type {boolean} */ +sinon.Spy.prototype.calledTwice = false; + /** @type {function(...):boolean} */ sinon.Spy.prototype.calledWith = function() {}; |