Roll src/third_party/WebKit 3529d49:06e8485 (svn 202554:202555)
[chromium-blink-merge.git] / remoting / webapp / base / js / oauth2.js
blobd4b13bf3bd7a588672ed6a6eeafb25bb61247824
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 /**
27  * @constructor
28  * @extends {remoting.Identity}
29  */
30 remoting.OAuth2 = function() {
33 // Constants representing keys used for storing persistent state.
34 /** @private */
35 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token';
36 /** @private */
37 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token';
38 /** @private */
39 remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email';
40 /** @private */
41 remoting.OAuth2.prototype.KEY_FULLNAME_ = 'remoting-fullname';
43 // Constants for parameters used in retrieving the OAuth2 credentials.
44 /** @private */
45 remoting.OAuth2.prototype.SCOPE_ =
46       'https://www.googleapis.com/auth/chromoting ' +
47       'https://www.googleapis.com/auth/googletalk ' +
48       'https://www.googleapis.com/auth/userinfo#email';
50 // Configurable URLs/strings.
51 /** @private
52  *  @return {string} OAuth2 redirect URI.
53  */
54 remoting.OAuth2.prototype.getRedirectUri_ = function() {
55   return remoting.settings.OAUTH2_REDIRECT_URL();
58 /** @private
59  *  @return {string} API client ID.
60  */
61 remoting.OAuth2.prototype.getClientId_ = function() {
62   return remoting.settings.OAUTH2_CLIENT_ID;
65 /** @private
66  *  @return {string} API client secret.
67  */
68 remoting.OAuth2.prototype.getClientSecret_ = function() {
69   return remoting.settings.OAUTH2_CLIENT_SECRET;
72 /** @private
73  *  @return {string} OAuth2 authentication URL.
74  */
75 remoting.OAuth2.prototype.getOAuth2AuthEndpoint_ = function() {
76   return remoting.settings.OAUTH2_BASE_URL + '/auth';
79 /** @return {boolean} True if the app is already authenticated. */
80 remoting.OAuth2.prototype.isAuthenticated = function() {
81   if (this.getRefreshToken()) {
82     return true;
83   }
84   return false;
87 /**
88  * Remove the cached auth token, if any.
89  *
90  * @return {!Promise<null>} A promise resolved with the operation completes.
91  */
92 remoting.OAuth2.prototype.removeCachedAuthToken = function() {
93   window.localStorage.removeItem(this.KEY_EMAIL_);
94   window.localStorage.removeItem(this.KEY_FULLNAME_);
95   this.clearAccessToken_();
96   this.clearRefreshToken_();
97   return Promise.resolve(null);
101  * Sets the refresh token.
103  * @param {string} token The new refresh token.
104  * @return {void} Nothing.
105  * @private
106  */
107 remoting.OAuth2.prototype.setRefreshToken_ = function(token) {
108   window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token));
109   window.localStorage.removeItem(this.KEY_EMAIL_);
110   window.localStorage.removeItem(this.KEY_FULLNAME_);
111   this.clearAccessToken_();
115  * @return {?string} The refresh token, if authenticated, or NULL.
116  */
117 remoting.OAuth2.prototype.getRefreshToken = function() {
118   var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_);
119   if (typeof value == 'string') {
120     return unescape(value);
121   }
122   return null;
126  * Clears the refresh token.
128  * @return {void} Nothing.
129  * @private
130  */
131 remoting.OAuth2.prototype.clearRefreshToken_ = function() {
132   window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_);
136  * @param {string} token The new access token.
137  * @param {number} expiration Expiration time in milliseconds since epoch.
138  * @return {void} Nothing.
139  * @private
140  */
141 remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) {
142   // Offset expiration by 120 seconds so that we can guarantee that the token
143   // we return will be valid for at least 2 minutes.
144   // If the access token is to be useful, this object must make some
145   // guarantee as to how long the token will be valid for.
146   // The choice of 2 minutes is arbitrary, but that length of time
147   // is part of the contract satisfied by callWithToken().
148   // Offset by a further 30 seconds to account for RTT issues.
149   var access_token = {
150     'token': token,
151     'expiration': (expiration - (120 + 30)) * 1000 + Date.now()
152   };
153   window.localStorage.setItem(this.KEY_ACCESS_TOKEN_,
154                               JSON.stringify(access_token));
158  * Returns the current access token, setting it to a invalid value if none
159  * existed before.
161  * @private
162  * @return {{token: string, expiration: number}} The current access token, or
163  *     an invalid token if not authenticated.
164  */
165 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() {
166   if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) {
167     // Always be able to return structured data.
168     this.setAccessToken_('', 0);
169   }
170   var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_);
171   if (typeof accessToken == 'string') {
172     var result = base.jsonParseSafe(accessToken);
173     if (result && 'token' in result && 'expiration' in result) {
174       return /** @type {{token: string, expiration: number}} */(result);
175     }
176   }
177   console.log('Invalid access token stored.');
178   return {'token': '', 'expiration': 0};
182  * Returns true if the access token is expired, or otherwise invalid.
184  * Will throw if !isAuthenticated().
186  * @return {boolean} True if a new access token is needed.
187  * @private
188  */
189 remoting.OAuth2.prototype.needsNewAccessToken_ = function() {
190   if (!this.isAuthenticated()) {
191     throw 'Not Authenticated.';
192   }
193   var access_token = this.getAccessTokenInternal_();
194   if (!access_token['token']) {
195     return true;
196   }
197   if (Date.now() > access_token['expiration']) {
198     return true;
199   }
200   return false;
204  * @return {void} Nothing.
205  * @private
206  */
207 remoting.OAuth2.prototype.clearAccessToken_ = function() {
208   window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_);
212  * Update state based on token response from the OAuth2 /token endpoint.
214  * @param {function(string):void} onOk Called with the new access token.
215  * @param {string} accessToken Access token.
216  * @param {number} expiresIn Expiration time for the access token.
217  * @return {void} Nothing.
218  * @private
219  */
220 remoting.OAuth2.prototype.onAccessToken_ =
221     function(onOk, accessToken, expiresIn) {
222   this.setAccessToken_(accessToken, expiresIn);
223   onOk(accessToken);
227  * Update state based on token response from the OAuth2 /token endpoint.
229  * @param {function():void} onOk Called after the new tokens are stored.
230  * @param {string} refreshToken Refresh token.
231  * @param {string} accessToken Access token.
232  * @param {number} expiresIn Expiration time for the access token.
233  * @return {void} Nothing.
234  * @private
235  */
236 remoting.OAuth2.prototype.onTokens_ =
237     function(onOk, refreshToken, accessToken, expiresIn) {
238   this.setAccessToken_(accessToken, expiresIn);
239   this.setRefreshToken_(refreshToken);
240   onOk();
244  * Redirect page to get a new OAuth2 authorization code
246  * @param {function(?string):void} onDone Completion callback to receive
247  *     the authorization code, or null on error.
248  * @return {void} Nothing.
249  */
250 remoting.OAuth2.prototype.getAuthorizationCode = function(onDone) {
251   var xsrf_token = base.generateXsrfToken();
252   var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' +
253     remoting.Xhr.urlencodeParamHash({
254           'client_id': this.getClientId_(),
255           'redirect_uri': this.getRedirectUri_(),
256           'scope': this.SCOPE_,
257           'state': xsrf_token,
258           'response_type': 'code',
259           'access_type': 'offline',
260           'approval_prompt': 'force'
261         });
263   /**
264    * Processes the results of the oauth flow.
265    *
266    * @param {Object<string>} message Dictionary containing the parsed OAuth
267    *     redirect URL parameters.
268    * @param {function(*)} sendResponse Function to send response.
269    */
270   function oauth2MessageListener(message, sender, sendResponse) {
271     if ('code' in message && 'state' in message) {
272       if (message['state'] == xsrf_token) {
273         onDone(message['code']);
274       } else {
275         console.error('Invalid XSRF token.');
276         onDone(null);
277       }
278     } else {
279       if ('error' in message) {
280         console.error(
281             'Could not obtain authorization code: ' + message['error']);
282       } else {
283         // We intentionally don't log the response - since we don't understand
284         // it, we can't tell if it has sensitive data.
285         console.error('Invalid oauth2 response.');
286       }
287       onDone(null);
288     }
289     chrome.extension.onMessage.removeListener(oauth2MessageListener);
290     sendResponse(null);
291   }
292   chrome.extension.onMessage.addListener(oauth2MessageListener);
293   window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no');
297  * Redirect page to get a new OAuth Refresh Token.
299  * @param {function():void} onDone Completion callback.
300  * @return {void} Nothing.
301  */
302 remoting.OAuth2.prototype.doAuthRedirect = function(onDone) {
303   /** @type {remoting.OAuth2} */
304   var that = this;
305   /** @param {?string} code */
306   var onAuthorizationCode = function(code) {
307     if (code) {
308       that.exchangeCodeForToken(code, onDone);
309     } else {
310       onDone();
311     }
312   };
313   this.getAuthorizationCode(onAuthorizationCode);
317  * Asynchronously exchanges an authorization code for a refresh token.
319  * @param {string} code The OAuth2 authorization code.
320  * @param {function():void} onDone Callback to invoke on completion.
321  * @return {void} Nothing.
322  */
323 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) {
324   /** @param {!remoting.Error} error */
325   var onError = function(error) {
326     console.error('Unable to exchange code for token: ' + error.toString());
327   };
329   remoting.oauth2Api.exchangeCodeForTokens(
330       this.onTokens_.bind(this, onDone), onError,
331       this.getClientId_(), this.getClientSecret_(), code,
332       this.getRedirectUri_());
336  * Print a command-line that can be used to register a host on Linux platforms.
337  */
338 remoting.OAuth2.prototype.printStartHostCommandLine = function() {
339   /** @type {string} */
340   var redirectUri = this.getRedirectUri_();
341   /** @param {?string} code */
342   var onAuthorizationCode = function(code) {
343     if (code) {
344       console.log('Run the following command to register a host:');
345       console.log(
346           '%c/opt/google/chrome-remote-desktop/start-host' +
347           ' --code=' + code +
348           ' --redirect-url=' + redirectUri +
349           ' --name=$HOSTNAME', 'font-weight: bold;');
350     }
351   };
352   this.getAuthorizationCode(onAuthorizationCode);
356  * Get an access token, refreshing it first if necessary.  The access
357  * token will remain valid for at least 2 minutes.
359  * @return {!Promise<string>} A promise resolved the an access token or
360  *     rejected with a remoting.Error.
361  */
362 remoting.OAuth2.prototype.getToken = function() {
363   /** @const */
364   var that = this;
366   return new Promise(function(resolve, reject) {
367     var refreshToken = that.getRefreshToken();
368     if (refreshToken) {
369       if (that.needsNewAccessToken_()) {
370         remoting.oauth2Api.refreshAccessToken(
371             that.onAccessToken_.bind(that, resolve), reject,
372             that.getClientId_(), that.getClientSecret_(),
373             refreshToken);
374       } else {
375         resolve(that.getAccessTokenInternal_()['token']);
376       }
377     } else {
378       reject(new remoting.Error(remoting.Error.Tag.NOT_AUTHENTICATED));
379     }
380   });
384  * Get the user's email address.
386  * @return {!Promise<string>} Promise resolved with the user's email
387  *     address or rejected with a remoting.Error.
388  */
389 remoting.OAuth2.prototype.getEmail = function() {
390   var cached = window.localStorage.getItem(this.KEY_EMAIL_);
391   if (typeof cached == 'string') {
392     return Promise.resolve(cached);
393   }
394   /** @type {remoting.OAuth2} */
395   var that = this;
397   return new Promise(function(resolve, reject) {
398     /** @param {string} email */
399     var onResponse = function(email) {
400       window.localStorage.setItem(that.KEY_EMAIL_, email);
401       window.localStorage.setItem(that.KEY_FULLNAME_, '');
402       resolve(email);
403     };
405     that.getToken().then(
406         remoting.oauth2Api.getEmail.bind(
407             remoting.oauth2Api, onResponse, reject),
408         reject);
409   });
413  * Get the user's email address and full name.
415  * @return {!Promise<{email: string, name: string}>} Promise
416  *     resolved with the user's email address and full name, or rejected
417  *     with a remoting.Error.
418  */
419 remoting.OAuth2.prototype.getUserInfo = function() {
420   var cachedEmail = window.localStorage.getItem(this.KEY_EMAIL_);
421   var cachedName = window.localStorage.getItem(this.KEY_FULLNAME_);
422   if (typeof cachedEmail == 'string' && typeof cachedName == 'string') {
423     /**
424      * The temp variable is needed to work around a compiler bug.
425      * @type {{email: string, name: string}}
426      */
427     var result = {email: cachedEmail, name: cachedName};
428     return Promise.resolve(result);
429   }
431   /** @type {remoting.OAuth2} */
432   var that = this;
434   return new Promise(function(resolve, reject) {
435     /**
436      * @param {string} email
437      * @param {string} name
438      */
439     var onResponse = function(email, name) {
440       window.localStorage.setItem(that.KEY_EMAIL_, email);
441       window.localStorage.setItem(that.KEY_FULLNAME_, name);
442       resolve({email: email, name: name});
443     };
445     that.getToken().then(
446         remoting.oauth2Api.getUserInfo.bind(
447             remoting.oauth2Api, onResponse, reject),
448         reject);
449   });