OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview | 6 * @fileoverview |
7 * Wrapper class for Chrome's identity API. | 7 * Wrapper class for Chrome's identity API. |
8 */ | 8 */ |
9 | 9 |
10 'use strict'; | 10 'use strict'; |
(...skipping 13 matching lines...) Expand all Loading... |
24 * @param {remoting.Identity.ConsentDialog=} opt_consentDialog | 24 * @param {remoting.Identity.ConsentDialog=} opt_consentDialog |
25 * @constructor | 25 * @constructor |
26 */ | 26 */ |
27 remoting.Identity = function(opt_consentDialog) { | 27 remoting.Identity = function(opt_consentDialog) { |
28 /** @private */ | 28 /** @private */ |
29 this.consentDialog_ = opt_consentDialog; | 29 this.consentDialog_ = opt_consentDialog; |
30 /** @type {string} @private */ | 30 /** @type {string} @private */ |
31 this.email_ = ''; | 31 this.email_ = ''; |
32 /** @type {string} @private */ | 32 /** @type {string} @private */ |
33 this.fullName_ = ''; | 33 this.fullName_ = ''; |
34 /** @type {Array<remoting.Identity.Callbacks>} */ | 34 /** @type {base.Deferred<string>} */ |
35 this.pendingCallbacks_ = []; | 35 this.authTokenDeferred_ = null; |
36 }; | 36 }; |
37 | 37 |
38 /** | 38 /** |
39 * chrome.identity.getAuthToken should be initiated from user interactions if | 39 * chrome.identity.getAuthToken should be initiated from user interactions if |
40 * called with interactive equals true. This interface prompts a dialog for | 40 * called with interactive equals true. This interface prompts a dialog for |
41 * the user's consent. | 41 * the user's consent. |
42 * | 42 * |
43 * @interface | 43 * @interface |
44 */ | 44 */ |
45 remoting.Identity.ConsentDialog = function() {}; | 45 remoting.Identity.ConsentDialog = function() {}; |
46 | 46 |
47 /** | 47 /** |
48 * @return {Promise} A Promise that resolves when permission to start an | 48 * @return {Promise} A Promise that resolves when permission to start an |
49 * interactive flow is granted. | 49 * interactive flow is granted. |
50 */ | 50 */ |
51 remoting.Identity.ConsentDialog.prototype.show = function() {}; | 51 remoting.Identity.ConsentDialog.prototype.show = function() {}; |
52 | 52 |
53 /** | 53 /** |
54 * Call a function with an access token. | 54 * Gets an access token. |
55 * | 55 * |
56 * @param {function(string):void} onOk Function to invoke with access token if | 56 * @return {!Promise<string>} A promise resolved with an access token |
57 * an access token was successfully retrieved. | 57 * or rejected with a remoting.Error. |
58 * @param {function(remoting.Error):void} onError Function to invoke with an | |
59 * error code on failure. | |
60 * @return {void} Nothing. | |
61 */ | 58 */ |
62 remoting.Identity.prototype.callWithToken = function(onOk, onError) { | 59 remoting.Identity.prototype.getToken = function() { |
63 this.pendingCallbacks_.push(new remoting.Identity.Callbacks(onOk, onError)); | 60 /** @const */ |
64 if (this.pendingCallbacks_.length == 1) { | 61 var that = this; |
| 62 |
| 63 if (this.authTokenDeferred_ == null) { |
| 64 this.authTokenDeferred_ = new base.Deferred(); |
65 chrome.identity.getAuthToken( | 65 chrome.identity.getAuthToken( |
66 { 'interactive': false }, | 66 { 'interactive': false }, |
67 this.onAuthComplete_.bind(this, false)); | 67 that.onAuthComplete_.bind(that, false)); |
68 } | 68 } |
| 69 return this.authTokenDeferred_.promise(); |
69 }; | 70 }; |
70 | 71 |
71 /** | 72 /** |
72 * Call a function with a fresh access token. | 73 * Gets a fresh access token. |
73 * | 74 * |
74 * @param {function(string):void} onOk Function to invoke with access token if | 75 * @return {!Promise<string>} A promise resolved with an access token |
75 * an access token was successfully retrieved. | 76 * or rejected with a remoting.Error. |
76 * @param {function(remoting.Error):void} onError Function to invoke with an | |
77 * error code on failure. | |
78 * @return {void} Nothing. | |
79 */ | 77 */ |
80 remoting.Identity.prototype.callWithNewToken = function(onOk, onError) { | 78 remoting.Identity.prototype.getNewToken = function() { |
81 /** @type {remoting.Identity} */ | 79 /** @type {remoting.Identity} */ |
82 var that = this; | 80 var that = this; |
83 | 81 |
84 /** | 82 return this.getToken().then(function(/** string */ token) { |
85 * @param {string} token | 83 return new Promise(function(resolve, reject) { |
86 */ | 84 chrome.identity.removeCachedAuthToken({'token': token }, function() { |
87 function revokeToken(token) { | 85 resolve(that.getToken()); |
88 chrome.identity.removeCachedAuthToken( | 86 }); |
89 {'token': token }, | 87 }); |
90 that.callWithToken.bind(that, onOk, onError)); | 88 }); |
91 } | |
92 | |
93 this.callWithToken(revokeToken, onError); | |
94 }; | 89 }; |
95 | 90 |
96 /** | 91 /** |
97 * Remove the cached auth token, if any. | 92 * Removes the cached auth token, if any. |
98 * | 93 * |
99 * @param {function():void=} opt_onDone Completion callback. | 94 * @return {!Promise<null>} A promise resolved with the operation completes. |
100 * @return {void} Nothing. | |
101 */ | 95 */ |
102 remoting.Identity.prototype.removeCachedAuthToken = function(opt_onDone) { | 96 remoting.Identity.prototype.removeCachedAuthToken = function() { |
103 var onDone = (opt_onDone) ? opt_onDone : base.doNothing; | 97 return new Promise(function(resolve, reject) { |
104 | 98 /** @param {string} token */ |
105 /** @param {string} token */ | 99 var onToken = function(token) { |
106 var onToken = function(token) { | 100 if (token) { |
107 if (token) { | 101 chrome.identity.removeCachedAuthToken( |
108 chrome.identity.removeCachedAuthToken({'token': token}, onDone); | 102 {'token': token}, resolve.bind(null, null)); |
109 } else { | 103 } else { |
110 onDone(); | 104 resolve(null); |
111 } | 105 } |
112 }; | 106 }; |
113 chrome.identity.getAuthToken({'interactive': false}, onToken); | 107 chrome.identity.getAuthToken({'interactive': false}, onToken); |
| 108 }); |
114 }; | 109 }; |
115 | 110 |
116 /** | 111 /** |
117 * Get the user's email address and full name. | 112 * Gets the user's email address and full name. The full name will be |
118 * The full name will be null unless the webapp has requested and been | 113 * null unless the webapp has requested and been granted the |
119 * granted the userinfo.profile permission. | 114 * userinfo.profile permission. |
120 * | 115 * |
121 * @param {function(string,string):void} onOk Callback invoked when the user's | 116 * TODO(jrw): Type declarations say the name can't be null. Are the |
122 * email address and full name are available. | 117 * types wrong, or is the documentation wrong? |
123 * @param {function(remoting.Error):void} onError Callback invoked if an | 118 * |
124 * error occurs. | 119 * @return {!Promise<{email:string, name:string}>} Promise |
125 * @return {void} Nothing. | 120 * resolved with the user's email address and full name, or rejected |
| 121 * with a remoting.Error. |
126 */ | 122 */ |
127 remoting.Identity.prototype.getUserInfo = function(onOk, onError) { | 123 remoting.Identity.prototype.getUserInfo = function() { |
128 if (this.isAuthenticated()) { | 124 if (this.isAuthenticated()) { |
129 onOk(this.email_, this.fullName_); | 125 /** |
130 return; | 126 * The temp variable is needed to work around a compiler bug. |
| 127 * @type {{email: string, name: string}} |
| 128 */ |
| 129 var result = {email: this.email_, name: this.fullName_}; |
| 130 return Promise.resolve(result); |
131 } | 131 } |
132 | 132 |
133 /** @type {remoting.Identity} */ | 133 /** @type {remoting.Identity} */ |
134 var that = this; | 134 var that = this; |
135 | 135 |
136 /** | 136 return this.getToken().then(function(token) { |
137 * @param {string} email | 137 return new Promise(function(resolve, reject) { |
138 * @param {string} name | 138 /** |
139 */ | 139 * @param {string} email |
140 var onResponse = function(email, name) { | 140 * @param {string} name |
141 that.email_ = email; | 141 */ |
142 that.fullName_ = name; | 142 var onResponse = function(email, name) { |
143 onOk(email, name); | 143 that.email_ = email; |
144 }; | 144 that.fullName_ = name; |
| 145 resolve({email: email, name: name}); |
| 146 }; |
145 | 147 |
146 this.callWithToken( | 148 remoting.oauth2Api.getUserInfo(onResponse, reject, token); |
147 remoting.oauth2Api.getUserInfo.bind( | 149 }); |
148 remoting.oauth2Api, onResponse, onError), | 150 }); |
149 onError); | |
150 }; | 151 }; |
151 | 152 |
152 /** | 153 /** |
153 * Get the user's email address. | 154 * Gets the user's email address. |
154 * | 155 * |
155 * @param {function(string):void} onOk Callback invoked when the email | 156 * @return {!Promise<string>} Promise resolved with the user's email |
156 * address is available. | 157 * address or rejected with a remoting.Error. |
157 * @param {function(remoting.Error):void} onError Callback invoked if an | |
158 * error occurs. | |
159 * @return {void} Nothing. | |
160 */ | 158 */ |
161 remoting.Identity.prototype.getEmail = function(onOk, onError) { | 159 remoting.Identity.prototype.getEmail = function() { |
162 this.getUserInfo(function(email, name) { | 160 this.getUserInfo().then(function(userInfo) { |
163 onOk(email); | 161 return userInfo.email; |
164 }, onError); | 162 }); |
165 }; | 163 }; |
166 | 164 |
167 /** | 165 /** |
168 * Get the user's email address, or null if no successful call to getUserInfo | 166 * Gets the user's email address, or null if no successful call to |
169 * has been made. | 167 * getUserInfo has been made. |
170 * | 168 * |
171 * @return {?string} The cached email address, if available. | 169 * @return {?string} The cached email address, if available. |
172 */ | 170 */ |
173 remoting.Identity.prototype.getCachedEmail = function() { | 171 remoting.Identity.prototype.getCachedEmail = function() { |
174 return this.email_; | 172 return this.email_; |
175 }; | 173 }; |
176 | 174 |
177 /** | 175 /** |
178 * Get the user's full name. | 176 * Gets the user's full name. |
| 177 * |
179 * This will return null if either: | 178 * This will return null if either: |
180 * No successful call to getUserInfo has been made, or | 179 * No successful call to getUserInfo has been made, or |
181 * The webapp doesn't have permission to access this value. | 180 * The webapp doesn't have permission to access this value. |
182 * | 181 * |
183 * @return {?string} The cached user's full name, if available. | 182 * @return {?string} The cached user's full name, if available. |
184 */ | 183 */ |
185 remoting.Identity.prototype.getCachedUserFullName = function() { | 184 remoting.Identity.prototype.getCachedUserFullName = function() { |
186 return this.fullName_; | 185 return this.fullName_; |
187 }; | 186 }; |
188 | 187 |
189 /** | 188 /** |
190 * Callback for the getAuthToken API. | 189 * Callback for the getAuthToken API. |
191 * | 190 * |
192 * @param {boolean} interactive The value of the "interactive" parameter to | 191 * @param {boolean} interactive The value of the "interactive" parameter to |
193 * getAuthToken. | 192 * getAuthToken. |
194 * @param {?string} token The auth token, or null if the request failed. | 193 * @param {?string} token The auth token, or null if the request failed. |
195 * @private | 194 * @private |
196 */ | 195 */ |
197 remoting.Identity.prototype.onAuthComplete_ = function(interactive, token) { | 196 remoting.Identity.prototype.onAuthComplete_ = function(interactive, token) { |
| 197 var authTokenDeferred = this.authTokenDeferred_; |
| 198 if (authTokenDeferred == null) { |
| 199 return; |
| 200 } |
| 201 this.authTokenDeferred_ = null; |
| 202 |
198 // Pass the token to the callback(s) if it was retrieved successfully. | 203 // Pass the token to the callback(s) if it was retrieved successfully. |
199 if (token) { | 204 if (token) { |
200 while (this.pendingCallbacks_.length > 0) { | 205 authTokenDeferred.resolve(token); |
201 var callback = /** @type {remoting.Identity.Callbacks} */ | |
202 (this.pendingCallbacks_.shift()); | |
203 callback.onOk(token); | |
204 } | |
205 return; | 206 return; |
206 } | 207 } |
207 | 208 |
208 // If not, pass an error back to the callback(s) if we've already prompted the | 209 // If not, pass an error back to the callback(s) if we've already prompted the |
209 // user for permission. | 210 // user for permission. |
210 if (interactive) { | 211 if (interactive) { |
211 var error_message = | 212 var error_message = |
212 chrome.runtime.lastError ? chrome.runtime.lastError.message | 213 chrome.runtime.lastError ? chrome.runtime.lastError.message |
213 : 'Unknown error.'; | 214 : 'Unknown error.'; |
214 console.error(error_message); | 215 console.error(error_message); |
215 while (this.pendingCallbacks_.length > 0) { | 216 authTokenDeferred.reject(remoting.Error.NOT_AUTHENTICATED); |
216 var callback = /** @type {remoting.Identity.Callbacks} */ | |
217 (this.pendingCallbacks_.shift()); | |
218 callback.onError(remoting.Error.NOT_AUTHENTICATED); | |
219 } | |
220 return; | 217 return; |
221 } | 218 } |
222 | 219 |
223 // If there's no token, but we haven't yet prompted for permission, do so | 220 // If there's no token, but we haven't yet prompted for permission, do so |
224 // now. | 221 // now. |
225 var that = this; | 222 var that = this; |
226 var showConsentDialog = | 223 var showConsentDialog = |
227 (this.consentDialog_) ? this.consentDialog_.show() : Promise.resolve(); | 224 (this.consentDialog_) ? this.consentDialog_.show() : Promise.resolve(); |
228 showConsentDialog.then(function() { | 225 showConsentDialog.then(function() { |
229 chrome.identity.getAuthToken({'interactive': true}, | 226 chrome.identity.getAuthToken( |
230 that.onAuthComplete_.bind(that, true)); | 227 {'interactive': true}, that.onAuthComplete_.bind(that, true)); |
231 }); | 228 }); |
232 }; | 229 }; |
233 | 230 |
234 /** | 231 /** |
235 * Internal representation for pair of callWithToken callbacks. | |
236 * | |
237 * @param {function(string):void} onOk | |
238 * @param {function(remoting.Error):void} onError | |
239 * @constructor | |
240 * @private | |
241 */ | |
242 remoting.Identity.Callbacks = function(onOk, onError) { | |
243 /** @type {function(string):void} */ | |
244 this.onOk = onOk; | |
245 /** @type {function(remoting.Error):void} */ | |
246 this.onError = onError; | |
247 }; | |
248 | |
249 /** | |
250 * Returns whether the web app has authenticated with the Google services. | 232 * Returns whether the web app has authenticated with the Google services. |
251 * | 233 * |
252 * @return {boolean} | 234 * @return {boolean} |
253 */ | 235 */ |
254 remoting.Identity.prototype.isAuthenticated = function() { | 236 remoting.Identity.prototype.isAuthenticated = function() { |
255 return remoting.identity.email_ !== ''; | 237 return remoting.identity.email_ !== ''; |
256 }; | 238 }; |
OLD | NEW |