Updating trunk VERSION from 2139.0 to 2140.0
[chromium-blink-merge.git] / remoting / webapp / oauth2.js
blobc504a645d72d09208c141b94e18a59836b9a0334
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_XSRF_TOKEN_ = 'oauth2-xsrf-token';
37 /** @private */
38 remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email';
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   this.clearAccessToken_();
92   this.clearRefreshToken_();
95 /**
96  * Sets the refresh token.
97  *
98  * @param {string} token The new refresh token.
99  * @return {void} Nothing.
100  * @private
101  */
102 remoting.OAuth2.prototype.setRefreshToken_ = function(token) {
103   window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token));
104   window.localStorage.removeItem(this.KEY_EMAIL_);
105   this.clearAccessToken_();
109  * @return {?string} The refresh token, if authenticated, or NULL.
110  */
111 remoting.OAuth2.prototype.getRefreshToken = function() {
112   var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_);
113   if (typeof value == 'string') {
114     return unescape(value);
115   }
116   return null;
120  * Clears the refresh token.
122  * @return {void} Nothing.
123  * @private
124  */
125 remoting.OAuth2.prototype.clearRefreshToken_ = function() {
126   window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_);
130  * @param {string} token The new access token.
131  * @param {number} expiration Expiration time in milliseconds since epoch.
132  * @return {void} Nothing.
133  * @private
134  */
135 remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) {
136   // Offset expiration by 120 seconds so that we can guarantee that the token
137   // we return will be valid for at least 2 minutes.
138   // If the access token is to be useful, this object must make some
139   // guarantee as to how long the token will be valid for.
140   // The choice of 2 minutes is arbitrary, but that length of time
141   // is part of the contract satisfied by callWithToken().
142   // Offset by a further 30 seconds to account for RTT issues.
143   var access_token = {
144     'token': token,
145     'expiration': (expiration - (120 + 30)) * 1000 + Date.now()
146   };
147   window.localStorage.setItem(this.KEY_ACCESS_TOKEN_,
148                               JSON.stringify(access_token));
152  * Returns the current access token, setting it to a invalid value if none
153  * existed before.
155  * @private
156  * @return {{token: string, expiration: number}} The current access token, or
157  * an invalid token if not authenticated.
158  */
159 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() {
160   if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) {
161     // Always be able to return structured data.
162     this.setAccessToken_('', 0);
163   }
164   var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_);
165   if (typeof accessToken == 'string') {
166     var result = jsonParseSafe(accessToken);
167     if (result && 'token' in result && 'expiration' in result) {
168       return /** @type {{token: string, expiration: number}} */ result;
169     }
170   }
171   console.log('Invalid access token stored.');
172   return {'token': '', 'expiration': 0};
176  * Returns true if the access token is expired, or otherwise invalid.
178  * Will throw if !isAuthenticated().
180  * @return {boolean} True if a new access token is needed.
181  * @private
182  */
183 remoting.OAuth2.prototype.needsNewAccessToken_ = function() {
184   if (!this.isAuthenticated()) {
185     throw 'Not Authenticated.';
186   }
187   var access_token = this.getAccessTokenInternal_();
188   if (!access_token['token']) {
189     return true;
190   }
191   if (Date.now() > access_token['expiration']) {
192     return true;
193   }
194   return false;
198  * @return {void} Nothing.
199  * @private
200  */
201 remoting.OAuth2.prototype.clearAccessToken_ = function() {
202   window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_);
206  * Update state based on token response from the OAuth2 /token endpoint.
208  * @param {function(string):void} onOk Called with the new access token.
209  * @param {string} accessToken Access token.
210  * @param {number} expiresIn Expiration time for the access token.
211  * @return {void} Nothing.
212  * @private
213  */
214 remoting.OAuth2.prototype.onAccessToken_ =
215     function(onOk, accessToken, expiresIn) {
216   this.setAccessToken_(accessToken, expiresIn);
217   onOk(accessToken);
221  * Update state based on token response from the OAuth2 /token endpoint.
223  * @param {function():void} onOk Called after the new tokens are stored.
224  * @param {string} refreshToken Refresh token.
225  * @param {string} accessToken Access token.
226  * @param {number} expiresIn Expiration time for the access token.
227  * @return {void} Nothing.
228  * @private
229  */
230 remoting.OAuth2.prototype.onTokens_ =
231     function(onOk, refreshToken, accessToken, expiresIn) {
232   this.setAccessToken_(accessToken, expiresIn);
233   this.setRefreshToken_(refreshToken);
234   onOk();
238  * Redirect page to get a new OAuth2 Refresh Token.
240  * @return {void} Nothing.
241  */
242 remoting.OAuth2.prototype.doAuthRedirect = function() {
243   /** @type {remoting.OAuth2} */
244   var that = this;
245   var xsrf_token = remoting.generateXsrfToken();
246   window.localStorage.setItem(this.KEY_XSRF_TOKEN_, xsrf_token);
247   var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' +
248     remoting.xhr.urlencodeParamHash({
249           'client_id': this.getClientId_(),
250           'redirect_uri': this.getRedirectUri_(),
251           'scope': this.SCOPE_,
252           'state': xsrf_token,
253           'response_type': 'code',
254           'access_type': 'offline',
255           'approval_prompt': 'force'
256         });
258   /**
259    * Processes the results of the oauth flow.
260    *
261    * @param {Object.<string, string>} message Dictionary containing the parsed
262    *   OAuth redirect URL parameters.
263    */
264   function oauth2MessageListener(message) {
265     if ('code' in message && 'state' in message) {
266       var onDone = function() {
267         window.location.reload();
268       };
269       that.exchangeCodeForToken(
270           message['code'], message['state'], onDone);
271     } else {
272       if ('error' in message) {
273         console.error(
274             'Could not obtain authorization code: ' + message['error']);
275       } else {
276         // We intentionally don't log the response - since we don't understand
277         // it, we can't tell if it has sensitive data.
278         console.error('Invalid oauth2 response.');
279       }
280     }
281     chrome.extension.onMessage.removeListener(oauth2MessageListener);
282   }
283   chrome.extension.onMessage.addListener(oauth2MessageListener);
284   window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no');
288  * Asynchronously exchanges an authorization code for a refresh token.
290  * @param {string} code The OAuth2 authorization code.
291  * @param {string} state The state parameter received from the OAuth redirect.
292  * @param {function():void} onDone Callback to invoke on completion.
293  * @return {void} Nothing.
294  */
295 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, state, onDone) {
296   var xsrf_token = window.localStorage.getItem(this.KEY_XSRF_TOKEN_);
297   window.localStorage.removeItem(this.KEY_XSRF_TOKEN_);
298   if (xsrf_token == undefined || state != xsrf_token) {
299     // Invalid XSRF token, or unexpected OAuth2 redirect. Abort.
300     onDone();
301   }
302   /** @param {remoting.Error} error */
303   var onError = function(error) {
304     console.error('Unable to exchange code for token: ', error);
305   };
307   remoting.OAuth2Api.exchangeCodeForTokens(
308       this.onTokens_.bind(this, onDone), onError,
309       this.getClientId_(), this.getClientSecret_(), code,
310       this.getRedirectUri_());
314  * Call a function with an access token, refreshing it first if necessary.
315  * The access token will remain valid for at least 2 minutes.
317  * @param {function(string):void} onOk Function to invoke with access token if
318  *     an access token was successfully retrieved.
319  * @param {function(remoting.Error):void} onError Function to invoke with an
320  *     error code on failure.
321  * @return {void} Nothing.
322  */
323 remoting.OAuth2.prototype.callWithToken = function(onOk, onError) {
324   var refreshToken = this.getRefreshToken();
325   if (refreshToken) {
326     if (this.needsNewAccessToken_()) {
327       remoting.OAuth2Api.refreshAccessToken(
328           this.onAccessToken_.bind(this, onOk), onError,
329           this.getClientId_(), this.getClientSecret_(),
330           refreshToken);
331     } else {
332       onOk(this.getAccessTokenInternal_()['token']);
333     }
334   } else {
335     onError(remoting.Error.NOT_AUTHENTICATED);
336   }
340  * Get the user's email address.
342  * @param {function(string):void} onOk Callback invoked when the email
343  *     address is available.
344  * @param {function(remoting.Error):void} onError Callback invoked if an
345  *     error occurs.
346  * @return {void} Nothing.
347  */
348 remoting.OAuth2.prototype.getEmail = function(onOk, onError) {
349   var cached = window.localStorage.getItem(this.KEY_EMAIL_);
350   if (typeof cached == 'string') {
351     onOk(cached);
352     return;
353   }
354   /** @type {remoting.OAuth2} */
355   var that = this;
356   /** @param {string} email */
357   var onResponse = function(email) {
358     window.localStorage.setItem(that.KEY_EMAIL_, email);
359     onOk(email);
360   };
362   this.callWithToken(
363       remoting.OAuth2Api.getEmail.bind(null, onResponse, onError), onError);
367  * If the user's email address is cached, return it, otherwise return null.
369  * @return {?string} The email address, if it has been cached by a previous call
370  *     to getEmail, otherwise null.
371  */
372 remoting.OAuth2.prototype.getCachedEmail = function() {
373   var value = window.localStorage.getItem(this.KEY_EMAIL_);
374   if (typeof value == 'string') {
375     return value;
376   }
377   return null;