blob: 9229076d9fbc60d662a3acba3ce949c61e1db446 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview
* Wrapper class for Chrome's identity API.
*/
/** @suppress {duplicate} */
var remoting = remoting || {};
(function(){
'use strict';
/**
* @type {remoting.Identity}
*/
remoting.identity = null;
var USER_CANCELLED = 'The user did not approve access.';
/**
* @param {remoting.Identity.ConsentDialog=} opt_consentDialog
* @constructor
*/
remoting.Identity = function(opt_consentDialog) {
/** @private */
this.consentDialog_ = opt_consentDialog;
/** @private {string} */
this.email_ = '';
/** @private {string} */
this.fullName_ = '';
/** @private {Object<base.Deferred<string>>} */
this.authTokensDeferred_ = {};
/** @private {boolean} */
this.interactive_ = false;
};
/**
* chrome.identity.getAuthToken should be initiated from user interactions if
* called with interactive equals true. This interface prompts a dialog for
* the user's consent.
*
* @interface
*/
remoting.Identity.ConsentDialog = function() {};
/**
* @return {Promise} A Promise that resolves when permission to start an
* interactive flow is granted.
*/
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(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.authTokensDeferred_[key].promise();
};
/**
* Gets a fresh access token.
*
* @param {Array<string>=} opt_scopes Optional OAuth2 scopes to request, as
* documented in getToken().
* @return {!Promise<string>} A promise resolved with an access token
* or rejected with a remoting.Error.
*/
remoting.Identity.prototype.getNewToken = function(opt_scopes) {
/** @type {remoting.Identity} */
var that = this;
return this.getToken(opt_scopes).then(function(/** string */ token) {
return new Promise(function(resolve, reject) {
chrome.identity.removeCachedAuthToken({'token': token }, function() {
resolve(that.getToken(opt_scopes));
});
});
});
};
/**
* Removes the cached auth token, if any.
*
* @return {!Promise<null>} A promise resolved with the operation completes.
*/
remoting.Identity.prototype.removeCachedAuthToken = function() {
return new Promise(function(resolve, reject) {
/** @param {string=} token */
var onToken = function(token) {
if (token) {
chrome.identity.removeCachedAuthToken(
{'token': token}, resolve.bind(null, null));
} else {
resolve(null);
}
};
chrome.identity.getAuthToken({'interactive': false}, onToken);
});
};
/**
* Gets the user's email address and full name. The full name will be
* null unless the webapp has requested and been granted the
* userinfo.profile permission.
*
* TODO(jrw): Type declarations say the name can't be null. Are the
* types wrong, or is the documentation wrong?
*
* @return {!Promise<{email:string, name:string}>} Promise
* resolved with the user's email address and full name, or rejected
* with a remoting.Error.
*/
remoting.Identity.prototype.getUserInfo = function() {
if (this.isAuthenticated()) {
/**
* The temp variable is needed to work around a compiler bug.
* @type {{email: string, name: string}}
*/
var result = {email: this.email_, name: this.fullName_};
return Promise.resolve(result);
}
/** @type {remoting.Identity} */
var that = this;
return this.getToken().then(function(token) {
return new Promise(function(resolve, reject) {
/**
* @param {string} email
* @param {string} name
*/
var onResponse = function(email, name) {
that.email_ = email;
that.fullName_ = name;
resolve({email: email, name: name});
};
remoting.oauth2Api.getUserInfo(onResponse, reject, token);
});
});
};
/**
* Gets the user's email address.
*
* @return {!Promise<string>} Promise resolved with the user's email
* address or rejected with a remoting.Error.
*/
remoting.Identity.prototype.getEmail = function() {
return this.getUserInfo().then(function(userInfo) {
return userInfo.email;
});
};
/**
* 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(scopes, token) {
var key = getScopesKey(scopes);
// Pass the token to the callback(s) if it was retrieved successfully.
if (token) {
var promise = this.authTokensDeferred_[key];
delete this.authTokensDeferred_[key];
promise.resolve(token);
return;
}
// If not, pass an error back to the callback(s) if we've already prompted the
// user for permission.
if (this.interactive_) {
var error_message =
chrome.runtime.lastError ? chrome.runtime.lastError.message
: 'Unknown error.';
console.error(error_message);
var error = (error_message == USER_CANCELLED) ?
new remoting.Error(remoting.Error.Tag.CANCELLED) :
new remoting.Error(remoting.Error.Tag.NOT_AUTHENTICATED);
this.authTokensDeferred_[key].reject(error);
delete this.authTokensDeferred_[key];
return;
}
// If there's no token, but we haven't yet prompted for permission, do so
// now.
var that = this;
var showConsentDialog =
(this.consentDialog_) ? this.consentDialog_.show() : Promise.resolve();
showConsentDialog.then(function() {
that.interactive_ = true;
var options = {
'interactive': that.interactive_,
'scopes': scopes
};
chrome.identity.getAuthToken(options,
that.onAuthComplete_.bind(that, scopes));
});
};
/**
* Returns whether the web app has authenticated with the Google services.
*
* @return {boolean}
*/
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) : '';
}
})();