Rewrite AndroidSyncSettings to be significantly simpler.
[chromium-blink-merge.git] / remoting / webapp / crd / js / oauth2.js
blob1074ed22e2ba47a6d3f34f41e8ee5f36209ce53d
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  * OAuth2 class that handles retrieval/storage of an OAuth2 token.
8  *
9  * Uses a content script to trampoline the OAuth redirect page back into the
10  * extension context.  This works around the lack of native support for
11  * chrome-extensions in OAuth2.
12  */
14 // TODO(jamiewalch): Delete this code once Chromoting is a v2 app and uses the
15 // identity API (http://crbug.com/ 134213).
17 'use strict';
19 /** @suppress {duplicate} */
20 var remoting = remoting || {};
22 /** @type {remoting.OAuth2} */
23 remoting.oauth2 = null;
26 /** @constructor */
27 remoting.OAuth2 = function() {
30 // Constants representing keys used for storing persistent state.
31 /** @private */
32 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token';
33 /** @private */
34 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token';
35 /** @private */
36 remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email';
37 /** @private */
38 remoting.OAuth2.prototype.KEY_FULLNAME_ = 'remoting-fullname';
40 // Constants for parameters used in retrieving the OAuth2 credentials.
41 /** @private */
42 remoting.OAuth2.prototype.SCOPE_ =
43       'https://www.googleapis.com/auth/chromoting ' +
44       'https://www.googleapis.com/auth/googletalk ' +
45       'https://www.googleapis.com/auth/userinfo#email';
47 // Configurable URLs/strings.
48 /** @private
49  *  @return {string} OAuth2 redirect URI.
50  */
51 remoting.OAuth2.prototype.getRedirectUri_ = function() {
52   return remoting.settings.OAUTH2_REDIRECT_URL();
55 /** @private
56  *  @return {string} API client ID.
57  */
58 remoting.OAuth2.prototype.getClientId_ = function() {
59   return remoting.settings.OAUTH2_CLIENT_ID;
62 /** @private
63  *  @return {string} API client secret.
64  */
65 remoting.OAuth2.prototype.getClientSecret_ = function() {
66   return remoting.settings.OAUTH2_CLIENT_SECRET;
69 /** @private
70  *  @return {string} OAuth2 authentication URL.
71  */
72 remoting.OAuth2.prototype.getOAuth2AuthEndpoint_ = function() {
73   return remoting.settings.OAUTH2_BASE_URL + '/auth';
76 /** @return {boolean} True if the app is already authenticated. */
77 remoting.OAuth2.prototype.isAuthenticated = function() {
78   if (this.getRefreshToken()) {
79     return true;
80   }
81   return false;
84 /**
85  * Remove the cached auth token, if any.
86  *
87  * @return {!Promise<null>} A promise resolved with the operation completes.
88  */
89 remoting.OAuth2.prototype.removeCachedAuthToken = function() {
90   window.localStorage.removeItem(this.KEY_EMAIL_);
91   window.localStorage.removeItem(this.KEY_FULLNAME_);
92   this.clearAccessToken_();
93   this.clearRefreshToken_();
94   return Promise.resolve(null);
97 /**
98  * Sets the refresh token.
99  *
100  * @param {string} token The new refresh token.
101  * @return {void} Nothing.
102  * @private
103  */
104 remoting.OAuth2.prototype.setRefreshToken_ = function(token) {
105   window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token));
106   window.localStorage.removeItem(this.KEY_EMAIL_);
107   window.localStorage.removeItem(this.KEY_FULLNAME_);
108   this.clearAccessToken_();
112  * @return {?string} The refresh token, if authenticated, or NULL.
113  */
114 remoting.OAuth2.prototype.getRefreshToken = function() {
115   var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_);
116   if (typeof value == 'string') {
117     return unescape(value);
118   }
119   return null;
123  * Clears the refresh token.
125  * @return {void} Nothing.
126  * @private
127  */
128 remoting.OAuth2.prototype.clearRefreshToken_ = function() {
129   window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_);
133  * @param {string} token The new access token.
134  * @param {number} expiration Expiration time in milliseconds since epoch.
135  * @return {void} Nothing.
136  * @private
137  */
138 remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) {
139   // Offset expiration by 120 seconds so that we can guarantee that the token
140   // we return will be valid for at least 2 minutes.
141   // If the access token is to be useful, this object must make some
142   // guarantee as to how long the token will be valid for.
143   // The choice of 2 minutes is arbitrary, but that length of time
144   // is part of the contract satisfied by callWithToken().
145   // Offset by a further 30 seconds to account for RTT issues.
146   var access_token = {
147     'token': token,
148     'expiration': (expiration - (120 + 30)) * 1000 + Date.now()
149   };
150   window.localStorage.setItem(this.KEY_ACCESS_TOKEN_,
151                               JSON.stringify(access_token));
155  * Returns the current access token, setting it to a invalid value if none
156  * existed before.
158  * @private
159  * @return {{token: string, expiration: number}} The current access token, or
160  *     an invalid token if not authenticated.
161  */
162 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() {
163   if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) {
164     // Always be able to return structured data.
165     this.setAccessToken_('', 0);
166   }
167   var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_);
168   if (typeof accessToken == 'string') {
169     var result = base.jsonParseSafe(accessToken);
170     if (result && 'token' in result && 'expiration' in result) {
171       return /** @type {{token: string, expiration: number}} */(result);
172     }
173   }
174   console.log('Invalid access token stored.');
175   return {'token': '', 'expiration': 0};
179  * Returns true if the access token is expired, or otherwise invalid.
181  * Will throw if !isAuthenticated().
183  * @return {boolean} True if a new access token is needed.
184  * @private
185  */
186 remoting.OAuth2.prototype.needsNewAccessToken_ = function() {
187   if (!this.isAuthenticated()) {
188     throw 'Not Authenticated.';
189   }
190   var access_token = this.getAccessTokenInternal_();
191   if (!access_token['token']) {
192     return true;
193   }
194   if (Date.now() > access_token['expiration']) {
195     return true;
196   }
197   return false;
201  * @return {void} Nothing.
202  * @private
203  */
204 remoting.OAuth2.prototype.clearAccessToken_ = function() {
205   window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_);
209  * Update state based on token response from the OAuth2 /token endpoint.
211  * @param {function(string):void} onOk Called with the new access token.
212  * @param {string} accessToken Access token.
213  * @param {number} expiresIn Expiration time for the access token.
214  * @return {void} Nothing.
215  * @private
216  */
217 remoting.OAuth2.prototype.onAccessToken_ =
218     function(onOk, accessToken, expiresIn) {
219   this.setAccessToken_(accessToken, expiresIn);
220   onOk(accessToken);
224  * Update state based on token response from the OAuth2 /token endpoint.
226  * @param {function():void} onOk Called after the new tokens are stored.
227  * @param {string} refreshToken Refresh token.
228  * @param {string} accessToken Access token.
229  * @param {number} expiresIn Expiration time for the access token.
230  * @return {void} Nothing.
231  * @private
232  */
233 remoting.OAuth2.prototype.onTokens_ =
234     function(onOk, refreshToken, accessToken, expiresIn) {
235   this.setAccessToken_(accessToken, expiresIn);
236   this.setRefreshToken_(refreshToken);
237   onOk();
241  * Redirect page to get a new OAuth2 authorization code
243  * @param {function(?string):void} onDone Completion callback to receive
244  *     the authorization code, or null on error.
245  * @return {void} Nothing.
246  */
247 remoting.OAuth2.prototype.getAuthorizationCode = function(onDone) {
248   var xsrf_token = base.generateXsrfToken();
249   var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' +
250     remoting.xhr.urlencodeParamHash({
251           'client_id': this.getClientId_(),
252           'redirect_uri': this.getRedirectUri_(),
253           'scope': this.SCOPE_,
254           'state': xsrf_token,
255           'response_type': 'code',
256           'access_type': 'offline',
257           'approval_prompt': 'force'
258         });
260   /**
261    * Processes the results of the oauth flow.
262    *
263    * @param {Object<string, string>} message Dictionary containing the parsed
264    *   OAuth redirect URL parameters.
265    * @param {function(*)} sendResponse Function to send response.
266    */
267   function oauth2MessageListener(message, sender, sendResponse) {
268     if ('code' in message && 'state' in message) {
269       if (message['state'] == xsrf_token) {
270         onDone(message['code']);
271       } else {
272         console.error('Invalid XSRF token.');
273         onDone(null);
274       }
275     } else {
276       if ('error' in message) {
277         console.error(
278             'Could not obtain authorization code: ' + message['error']);
279       } else {
280         // We intentionally don't log the response - since we don't understand
281         // it, we can't tell if it has sensitive data.
282         console.error('Invalid oauth2 response.');
283       }
284       onDone(null);
285     }
286     chrome.extension.onMessage.removeListener(oauth2MessageListener);
287     sendResponse(null);
288   }
289   chrome.extension.onMessage.addListener(oauth2MessageListener);
290   window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no');
294  * Redirect page to get a new OAuth Refresh Token.
296  * @param {function():void} onDone Completion callback.
297  * @return {void} Nothing.
298  */
299 remoting.OAuth2.prototype.doAuthRedirect = function(onDone) {
300   /** @type {remoting.OAuth2} */
301   var that = this;
302   /** @param {?string} code */
303   var onAuthorizationCode = function(code) {
304     if (code) {
305       that.exchangeCodeForToken(code, onDone);
306     } else {
307       onDone();
308     }
309   };
310   this.getAuthorizationCode(onAuthorizationCode);
314  * Asynchronously exchanges an authorization code for a refresh token.
316  * @param {string} code The OAuth2 authorization code.
317  * @param {function():void} onDone Callback to invoke on completion.
318  * @return {void} Nothing.
319  */
320 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) {
321   /** @param {remoting.Error} error */
322   var onError = function(error) {
323     console.error('Unable to exchange code for token: ', error);
324   };
326   remoting.oauth2Api.exchangeCodeForTokens(
327       this.onTokens_.bind(this, onDone), onError,
328       this.getClientId_(), this.getClientSecret_(), code,
329       this.getRedirectUri_());
333  * Print a command-line that can be used to register a host on Linux platforms.
334  */
335 remoting.OAuth2.prototype.printStartHostCommandLine = function() {
336   /** @type {string} */
337   var redirectUri = this.getRedirectUri_();
338   /** @param {?string} code */
339   var onAuthorizationCode = function(code) {
340     if (code) {
341       console.log('Run the following command to register a host:');
342       console.log(
343           '%c/opt/google/chrome-remote-desktop/start-host' +
344           ' --code=' + code +
345           ' --redirect-url=' + redirectUri +
346           ' --name=$HOSTNAME', 'font-weight: bold;');
347     }
348   };
349   this.getAuthorizationCode(onAuthorizationCode);
353  * Get an access token, refreshing it first if necessary.  The access
354  * token will remain valid for at least 2 minutes.
356  * @return {!Promise<string>} A promise resolved the an access token or
357  *     rejected with a remoting.Error.
358  */
359 remoting.OAuth2.prototype.getToken = function() {
360   /** @const */
361   var that = this;
363   return new Promise(function(resolve, reject) {
364     var refreshToken = that.getRefreshToken();
365     if (refreshToken) {
366       if (that.needsNewAccessToken_()) {
367         remoting.oauth2Api.refreshAccessToken(
368             that.onAccessToken_.bind(that, resolve), reject,
369             that.getClientId_(), that.getClientSecret_(),
370             refreshToken);
371       } else {
372         resolve(that.getAccessTokenInternal_()['token']);
373       }
374     } else {
375       reject(remoting.Error.NOT_AUTHENTICATED);
376     }
377   });
381  * Get the user's email address.
383  * @return {!Promise<string>} Promise resolved with the user's email
384  *     address or rejected with a remoting.Error.
385  */
386 remoting.OAuth2.prototype.getEmail = function() {
387   var cached = window.localStorage.getItem(this.KEY_EMAIL_);
388   if (typeof cached == 'string') {
389     return Promise.resolve(cached);
390   }
391   /** @type {remoting.OAuth2} */
392   var that = this;
394   return new Promise(function(resolve, reject) {
395     /** @param {string} email */
396     var onResponse = function(email) {
397       window.localStorage.setItem(that.KEY_EMAIL_, email);
398       window.localStorage.setItem(that.KEY_FULLNAME_, '');
399       resolve(email);
400     };
402     that.getToken().then(
403         remoting.oauth2Api.getEmail.bind(
404             remoting.oauth2Api, onResponse, reject),
405         reject);
406   });
410  * Get the user's email address and full name.
412  * @return {!Promise<{email: string, name: string}>} Promise
413  *     resolved with the user's email address and full name, or rejected
414  *     with a remoting.Error.
415  */
416 remoting.OAuth2.prototype.getUserInfo = function() {
417   var cachedEmail = window.localStorage.getItem(this.KEY_EMAIL_);
418   var cachedName = window.localStorage.getItem(this.KEY_FULLNAME_);
419   if (typeof cachedEmail == 'string' && typeof cachedName == 'string') {
420     /**
421      * The temp variable is needed to work around a compiler bug.
422      * @type {{email: string, name: string}}
423      */
424     var result = {email: cachedEmail, name: cachedName};
425     return Promise.resolve(result);
426   }
428   /** @type {remoting.OAuth2} */
429   var that = this;
431   return new Promise(function(resolve, reject) {
432     /**
433      * @param {string} email
434      * @param {string} name
435      */
436     var onResponse = function(email, name) {
437       window.localStorage.setItem(that.KEY_EMAIL_, email);
438       window.localStorage.setItem(that.KEY_FULLNAME_, name);
439       resolve({email: email, name: name});
440     };
442     that.getToken().then(
443         remoting.oauth2Api.getUserInfo.bind(
444             remoting.oauth2Api, onResponse, reject),
445         reject);
446   });
450  * If the user's email address is cached, return it, otherwise return null.
452  * @return {?string} The email address, if it has been cached by a previous call
453  *     to getEmail or getUserInfo, otherwise null.
454  */
455 remoting.OAuth2.prototype.getCachedEmail = function() {
456   var value = window.localStorage.getItem(this.KEY_EMAIL_);
457   if (typeof value == 'string') {
458     return value;
459   }
460   return null;
464  * If the user's full name is cached, return it, otherwise return null.
466  * @return {?string} The user's full name, if it has been cached by a previous
467  * call to getUserInfo, otherwise null.
468  */
469 remoting.OAuth2.prototype.getCachedUserFullName = function() {
470   var value = window.localStorage.getItem(this.KEY_FULLNAME_);
471   if (typeof value == 'string') {
472     return value;
473   }
474   return null;