Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / remoting / webapp / base / js / identity.js
blob9229076d9fbc60d662a3acba3ce949c61e1db446
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 /**
6  * @fileoverview
7  * Wrapper class for Chrome's identity API.
8  */
9 /** @suppress {duplicate} */
10 var remoting = remoting || {};
12 (function(){
14 'use strict';
16 /**
17  * @type {remoting.Identity}
18  */
19 remoting.identity = null;
21 var USER_CANCELLED = 'The user did not approve access.';
23 /**
24  * @param {remoting.Identity.ConsentDialog=} opt_consentDialog
25  * @constructor
26  */
27 remoting.Identity = function(opt_consentDialog) {
28   /** @private */
29   this.consentDialog_ = opt_consentDialog;
30   /** @private {string} */
31   this.email_ = '';
32   /** @private {string} */
33   this.fullName_ = '';
34   /** @private {Object<base.Deferred<string>>} */
35   this.authTokensDeferred_ = {};
36   /** @private {boolean} */
37   this.interactive_ = false;
40 /**
41  * chrome.identity.getAuthToken should be initiated from user interactions if
42  * called with interactive equals true.  This interface prompts a dialog for
43  * the user's consent.
44  *
45  * @interface
46  */
47 remoting.Identity.ConsentDialog = function() {};
49 /**
50  * @return {Promise} A Promise that resolves when permission to start an
51  *   interactive flow is granted.
52  */
53 remoting.Identity.ConsentDialog.prototype.show = function() {};
55 /**
56  * Gets an access token.
57  *
58  * @param {Array<string>=} opt_scopes Optional OAuth2 scopes to request. If not
59  *     specified, the scopes specified in the manifest will be used. No consent
60  *     prompt will be needed as long as the requested scopes are a subset of
61  *     those already granted (in most cases, the remoting.Application framework
62  *     ensures that the scopes specified in the manifest are already authorized
63  *     before any application code is executed). Callers can request scopes not
64  *     specified in the manifest, but a consent prompt will be shown.
65  *
66  * @return {!Promise<string>} A promise resolved with an access token
67  *     or rejected with a remoting.Error.
68  */
69 remoting.Identity.prototype.getToken = function(opt_scopes) {
70   var key = getScopesKey(opt_scopes);
71   if (!this.authTokensDeferred_[key]) {
72     this.authTokensDeferred_[key] = new base.Deferred();
73     var options = {
74       'interactive': this.interactive_,
75       'scopes': opt_scopes
76     };
77     chrome.identity.getAuthToken(options,
78                                  this.onAuthComplete_.bind(this, opt_scopes));
79   }
80   return this.authTokensDeferred_[key].promise();
83 /**
84  * Gets a fresh access token.
85  *
86  * @param {Array<string>=} opt_scopes Optional OAuth2 scopes to request, as
87  *     documented in getToken().
88  * @return {!Promise<string>} A promise resolved with an access token
89  *     or rejected with a remoting.Error.
90  */
91 remoting.Identity.prototype.getNewToken = function(opt_scopes) {
92   /** @type {remoting.Identity} */
93   var that = this;
95   return this.getToken(opt_scopes).then(function(/** string */ token) {
96     return new Promise(function(resolve, reject) {
97       chrome.identity.removeCachedAuthToken({'token': token }, function() {
98         resolve(that.getToken(opt_scopes));
99       });
100     });
101   });
105  * Removes the cached auth token, if any.
107  * @return {!Promise<null>} A promise resolved with the operation completes.
108  */
109 remoting.Identity.prototype.removeCachedAuthToken = function() {
110   return new Promise(function(resolve, reject) {
111     /** @param {string=} token */
112     var onToken = function(token) {
113       if (token) {
114         chrome.identity.removeCachedAuthToken(
115             {'token': token}, resolve.bind(null, null));
116       } else {
117         resolve(null);
118       }
119     };
120     chrome.identity.getAuthToken({'interactive': false}, onToken);
121   });
125  * Gets the user's email address and full name.  The full name will be
126  * null unless the webapp has requested and been granted the
127  * userinfo.profile permission.
129  * TODO(jrw): Type declarations say the name can't be null.  Are the
130  * types wrong, or is the documentation wrong?
132  * @return {!Promise<{email:string, name:string}>} Promise
133  *     resolved with the user's email address and full name, or rejected
134  *     with a remoting.Error.
135  */
136 remoting.Identity.prototype.getUserInfo = function() {
137   if (this.isAuthenticated()) {
138     /**
139      * The temp variable is needed to work around a compiler bug.
140      * @type {{email: string, name: string}}
141      */
142     var result = {email: this.email_, name: this.fullName_};
143     return Promise.resolve(result);
144   }
146   /** @type {remoting.Identity} */
147   var that = this;
149   return this.getToken().then(function(token) {
150     return new Promise(function(resolve, reject) {
151       /**
152        * @param {string} email
153        * @param {string} name
154        */
155       var onResponse = function(email, name) {
156         that.email_ = email;
157         that.fullName_ = name;
158         resolve({email: email, name: name});
159       };
161       remoting.oauth2Api.getUserInfo(onResponse, reject, token);
162     });
163   });
167  * Gets the user's email address.
169  * @return {!Promise<string>} Promise resolved with the user's email
170  *     address or rejected with a remoting.Error.
171  */
172 remoting.Identity.prototype.getEmail = function() {
173   return this.getUserInfo().then(function(userInfo) {
174     return userInfo.email;
175   });
179  * Callback for the getAuthToken API.
181  * @param {Array<string>|undefined} scopes The explicit scopes passed to
182  *     getToken, or undefined if no scopes were specified.
183  * @param {string=} token The auth token, or null if the request failed.
184  * @private
185  */
186 remoting.Identity.prototype.onAuthComplete_ = function(scopes, token) {
187   var key = getScopesKey(scopes);
189   // Pass the token to the callback(s) if it was retrieved successfully.
190   if (token) {
191     var promise = this.authTokensDeferred_[key];
192     delete this.authTokensDeferred_[key];
193     promise.resolve(token);
194     return;
195   }
197   // If not, pass an error back to the callback(s) if we've already prompted the
198   // user for permission.
199   if (this.interactive_) {
200     var error_message =
201         chrome.runtime.lastError ? chrome.runtime.lastError.message
202                                  : 'Unknown error.';
203     console.error(error_message);
204     var error = (error_message == USER_CANCELLED) ?
205         new remoting.Error(remoting.Error.Tag.CANCELLED) :
206         new remoting.Error(remoting.Error.Tag.NOT_AUTHENTICATED);
207     this.authTokensDeferred_[key].reject(error);
208     delete this.authTokensDeferred_[key];
209     return;
210   }
212   // If there's no token, but we haven't yet prompted for permission, do so
213   // now.
214   var that = this;
215   var showConsentDialog =
216       (this.consentDialog_) ? this.consentDialog_.show() : Promise.resolve();
217   showConsentDialog.then(function() {
218     that.interactive_ = true;
219     var options = {
220       'interactive': that.interactive_,
221       'scopes': scopes
222     };
223     chrome.identity.getAuthToken(options,
224                                  that.onAuthComplete_.bind(that, scopes));
225   });
229  * Returns whether the web app has authenticated with the Google services.
231  * @return {boolean}
232  */
233 remoting.Identity.prototype.isAuthenticated = function() {
234   return remoting.identity.email_ !== '';
239  * @param {Array<string>=} opt_scopes
240  * @return {string}
241  */
242 function getScopesKey(opt_scopes) {
243   return opt_scopes ? JSON.stringify(opt_scopes) : '';
246 })();