Refactor doRedirect to expose access code.
[chromium-blink-merge.git] / remoting / webapp / crd / js / oauth2.js
blobfdef68b187672d09a5398b0ec4dddf09caa90656
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  * Removes all storage, and effectively unauthenticates the user.
86  *
87  * @return {void} Nothing.
88  */
89 remoting.OAuth2.prototype.clear = function() {
90   window.localStorage.removeItem(this.KEY_EMAIL_);
91   window.localStorage.removeItem(this.KEY_FULLNAME_);
92   this.clearAccessToken_();
93   this.clearRefreshToken_();
96 /**
97  * Sets the refresh token.
98  *
99  * @param {string} token The new refresh token.
100  * @return {void} Nothing.
101  * @private
102  */
103 remoting.OAuth2.prototype.setRefreshToken_ = function(token) {
104   window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token));
105   window.localStorage.removeItem(this.KEY_EMAIL_);
106   window.localStorage.removeItem(this.KEY_FULLNAME_);
107   this.clearAccessToken_();
111  * @return {?string} The refresh token, if authenticated, or NULL.
112  */
113 remoting.OAuth2.prototype.getRefreshToken = function() {
114   var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_);
115   if (typeof value == 'string') {
116     return unescape(value);
117   }
118   return null;
122  * Clears the refresh token.
124  * @return {void} Nothing.
125  * @private
126  */
127 remoting.OAuth2.prototype.clearRefreshToken_ = function() {
128   window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_);
132  * @param {string} token The new access token.
133  * @param {number} expiration Expiration time in milliseconds since epoch.
134  * @return {void} Nothing.
135  * @private
136  */
137 remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) {
138   // Offset expiration by 120 seconds so that we can guarantee that the token
139   // we return will be valid for at least 2 minutes.
140   // If the access token is to be useful, this object must make some
141   // guarantee as to how long the token will be valid for.
142   // The choice of 2 minutes is arbitrary, but that length of time
143   // is part of the contract satisfied by callWithToken().
144   // Offset by a further 30 seconds to account for RTT issues.
145   var access_token = {
146     'token': token,
147     'expiration': (expiration - (120 + 30)) * 1000 + Date.now()
148   };
149   window.localStorage.setItem(this.KEY_ACCESS_TOKEN_,
150                               JSON.stringify(access_token));
154  * Returns the current access token, setting it to a invalid value if none
155  * existed before.
157  * @private
158  * @return {{token: string, expiration: number}} The current access token, or
159  * an invalid token if not authenticated.
160  */
161 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() {
162   if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) {
163     // Always be able to return structured data.
164     this.setAccessToken_('', 0);
165   }
166   var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_);
167   if (typeof accessToken == 'string') {
168     var result = base.jsonParseSafe(accessToken);
169     if (result && 'token' in result && 'expiration' in result) {
170       return /** @type {{token: string, expiration: number}} */ result;
171     }
172   }
173   console.log('Invalid access token stored.');
174   return {'token': '', 'expiration': 0};
178  * Returns true if the access token is expired, or otherwise invalid.
180  * Will throw if !isAuthenticated().
182  * @return {boolean} True if a new access token is needed.
183  * @private
184  */
185 remoting.OAuth2.prototype.needsNewAccessToken_ = function() {
186   if (!this.isAuthenticated()) {
187     throw 'Not Authenticated.';
188   }
189   var access_token = this.getAccessTokenInternal_();
190   if (!access_token['token']) {
191     return true;
192   }
193   if (Date.now() > access_token['expiration']) {
194     return true;
195   }
196   return false;
200  * @return {void} Nothing.
201  * @private
202  */
203 remoting.OAuth2.prototype.clearAccessToken_ = function() {
204   window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_);
208  * Update state based on token response from the OAuth2 /token endpoint.
210  * @param {function(string):void} onOk Called with the new access token.
211  * @param {string} accessToken Access token.
212  * @param {number} expiresIn Expiration time for the access token.
213  * @return {void} Nothing.
214  * @private
215  */
216 remoting.OAuth2.prototype.onAccessToken_ =
217     function(onOk, accessToken, expiresIn) {
218   this.setAccessToken_(accessToken, expiresIn);
219   onOk(accessToken);
223  * Update state based on token response from the OAuth2 /token endpoint.
225  * @param {function():void} onOk Called after the new tokens are stored.
226  * @param {string} refreshToken Refresh token.
227  * @param {string} accessToken Access token.
228  * @param {number} expiresIn Expiration time for the access token.
229  * @return {void} Nothing.
230  * @private
231  */
232 remoting.OAuth2.prototype.onTokens_ =
233     function(onOk, refreshToken, accessToken, expiresIn) {
234   this.setAccessToken_(accessToken, expiresIn);
235   this.setRefreshToken_(refreshToken);
236   onOk();
240  * Redirect page to get a new OAuth2 authorization code
242  * @param {function(?string):void} onDone Completion callback to receive
243  *     the authorization code, or null on error.
244  * @return {void} Nothing.
245  */
246 remoting.OAuth2.prototype.getAuthorizationCode = function(onDone) {
247   var xsrf_token = base.generateXsrfToken();
248   var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' +
249     remoting.xhr.urlencodeParamHash({
250           'client_id': this.getClientId_(),
251           'redirect_uri': this.getRedirectUri_(),
252           'scope': this.SCOPE_,
253           'state': xsrf_token,
254           'response_type': 'code',
255           'access_type': 'offline',
256           'approval_prompt': 'force'
257         });
259   /**
260    * Processes the results of the oauth flow.
261    *
262    * @param {Object.<string, string>} message Dictionary containing the parsed
263    *   OAuth redirect URL parameters.
264    */
265   function oauth2MessageListener(message) {
266     if ('code' in message && 'state' in message) {
267       if (message['state'] == xsrf_token) {
268         onDone(message['code']);
269       } else {
270         console.error('Invalid XSRF token.');
271         onDone(null);
272       }
273     } else {
274       if ('error' in message) {
275         console.error(
276             'Could not obtain authorization code: ' + message['error']);
277       } else {
278         // We intentionally don't log the response - since we don't understand
279         // it, we can't tell if it has sensitive data.
280         console.error('Invalid oauth2 response.');
281       }
282       onDone(null);
283     }
284     chrome.extension.onMessage.removeListener(oauth2MessageListener);
285   }
286   chrome.extension.onMessage.addListener(oauth2MessageListener);
287   window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no');
291  * Redirect page to get a new OAuth Refresh Token.
293  * @param {function():void} onDone Completion callback.
294  * @return {void} Nothing.
295  */
296 remoting.OAuth2.prototype.doAuthRedirect = function(onDone) {
297   /** @type {remoting.OAuth2} */
298   var that = this;
299   /** @param {?string} code */
300   var onAuthorizationCode = function(code) {
301     if (code) {
302       that.exchangeCodeForToken(code, onDone);
303     } else {
304       onDone();
305     }
306   };
307   this.getAuthorizationCode(onAuthorizationCode);
311  * Asynchronously exchanges an authorization code for a refresh token.
313  * @param {string} code The OAuth2 authorization code.
314  * @param {function():void} onDone Callback to invoke on completion.
315  * @return {void} Nothing.
316  */
317 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) {
318   /** @param {remoting.Error} error */
319   var onError = function(error) {
320     console.error('Unable to exchange code for token: ', error);
321   };
323   remoting.OAuth2Api.exchangeCodeForTokens(
324       this.onTokens_.bind(this, onDone), onError,
325       this.getClientId_(), this.getClientSecret_(), code,
326       this.getRedirectUri_());
330  * Print a command-line that can be used to register a host on Linux platforms.
331  */
332 remoting.OAuth2.prototype.printStartHostCommandLine = function() {
333   /** @type {string} */
334   var redirectUri = this.getRedirectUri_();
335   /** @param {?string} code */
336   var onAuthorizationCode = function(code) {
337     if (code) {
338       console.log('Run the following command to register a host:');
339       console.log(
340           '%c/opt/google/chrome-remote-desktop/start-host' +
341           ' --code=' + code +
342           ' --redirect-url=' + redirectUri +
343           ' --name=$HOSTNAME', 'font-weight: bold;');
344     }
345   };
346   this.getAuthorizationCode(onAuthorizationCode);
350  * Call a function with an access token, refreshing it first if necessary.
351  * The access token will remain valid for at least 2 minutes.
353  * @param {function(string):void} onOk Function to invoke with access token if
354  *     an access token was successfully retrieved.
355  * @param {function(remoting.Error):void} onError Function to invoke with an
356  *     error code on failure.
357  * @return {void} Nothing.
358  */
359 remoting.OAuth2.prototype.callWithToken = function(onOk, onError) {
360   var refreshToken = this.getRefreshToken();
361   if (refreshToken) {
362     if (this.needsNewAccessToken_()) {
363       remoting.OAuth2Api.refreshAccessToken(
364           this.onAccessToken_.bind(this, onOk), onError,
365           this.getClientId_(), this.getClientSecret_(),
366           refreshToken);
367     } else {
368       onOk(this.getAccessTokenInternal_()['token']);
369     }
370   } else {
371     onError(remoting.Error.NOT_AUTHENTICATED);
372   }
376  * Get the user's email address.
378  * @param {function(string):void} onOk Callback invoked when the email
379  *     address is available.
380  * @param {function(remoting.Error):void} onError Callback invoked if an
381  *     error occurs.
382  * @return {void} Nothing.
383  */
384 remoting.OAuth2.prototype.getEmail = function(onOk, onError) {
385   var cached = window.localStorage.getItem(this.KEY_EMAIL_);
386   if (typeof cached == 'string') {
387     onOk(cached);
388     return;
389   }
390   /** @type {remoting.OAuth2} */
391   var that = this;
392   /** @param {string} email */
393   var onResponse = function(email) {
394     window.localStorage.setItem(that.KEY_EMAIL_, email);
395     window.localStorage.setItem(that.KEY_FULLNAME_, '');
396     onOk(email);
397   };
399   this.callWithToken(
400       remoting.OAuth2Api.getEmail.bind(null, onResponse, onError), onError);
404  * Get the user's email address and full name.
406  * @param {function(string,string):void} onOk Callback invoked when the user's
407  *     email address and full name are available.
408  * @param {function(remoting.Error):void} onError Callback invoked if an
409  *     error occurs.
410  * @return {void} Nothing.
411  */
412 remoting.OAuth2.prototype.getUserInfo = function(onOk, onError) {
413   var cachedEmail = window.localStorage.getItem(this.KEY_EMAIL_);
414   var cachedName = window.localStorage.getItem(this.KEY_FULLNAME_);
415   if (typeof cachedEmail == 'string' && typeof cachedName == 'string') {
416     onOk(cachedEmail, cachedName);
417     return;
418   }
419   /** @type {remoting.OAuth2} */
420   var that = this;
421   /**
422    * @param {string} email
423    * @param {string} name
424    */
425   var onResponse = function(email, name) {
426     window.localStorage.setItem(that.KEY_EMAIL_, email);
427     window.localStorage.setItem(that.KEY_FULLNAME_, name);
428     onOk(email, name);
429   };
431   this.callWithToken(
432       remoting.OAuth2Api.getUserInfo.bind(null, onResponse, onError), onError);
436  * If the user's email address is cached, return it, otherwise return null.
438  * @return {?string} The email address, if it has been cached by a previous call
439  *     to getEmail or getUserInfo, otherwise null.
440  */
441 remoting.OAuth2.prototype.getCachedEmail = function() {
442   var value = window.localStorage.getItem(this.KEY_EMAIL_);
443   if (typeof value == 'string') {
444     return value;
445   }
446   return null;
450  * If the user's full name is cached, return it, otherwise return null.
452  * @return {?string} The user's full name, if it has been cached by a previous
453  * call to getUserInfo, otherwise null.
454  */
455 remoting.OAuth2.prototype.getCachedUserFullName = function() {
456   var value = window.localStorage.getItem(this.KEY_FULLNAME_);
457   if (typeof value == 'string') {
458     return value;
459   }
460   return null;