summaryrefslogtreecommitdiffstats
path: root/remoting
diff options
context:
space:
mode:
authorjamiewalch <jamiewalch@chromium.org>2015-03-18 14:38:25 -0700
committerCommit bot <commit-bot@chromium.org>2015-03-18 21:39:24 +0000
commite13a9d574fb9c9a4d0f36351e0e91e89b4e17928 (patch)
tree411f4772a963879748f1f7c02fc4ba796b44d24e /remoting
parenta0e8a714c577e98e26a1194c40b98ec0854c1c32 (diff)
downloadchromium_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.js66
-rw-r--r--remoting/webapp/crd/js/identity_unittest.js54
-rw-r--r--remoting/webapp/js_proto/chrome_mocks.js7
-rw-r--r--remoting/webapp/js_proto/sinon_proto.js3
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() {};