- add sources.
[platform/framework/web/crosswalk.git] / src / remoting / webapp / oauth2.js
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.
4
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  */
13
14 // TODO(jamiewalch): Delete this code once Chromoting is a v2 app and uses the
15 // identity API (http://crbug.com/ 134213).
16
17 'use strict';
18
19 /** @suppress {duplicate} */
20 var remoting = remoting || {};
21
22 /** @type {remoting.OAuth2} */
23 remoting.oauth2 = null;
24
25
26 /** @constructor */
27 remoting.OAuth2 = function() {
28 };
29
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_REFRESH_TOKEN_REVOKABLE_ =
35     'oauth2-refresh-token-revokable';
36 /** @private */
37 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token';
38 /** @private */
39 remoting.OAuth2.prototype.KEY_XSRF_TOKEN_ = 'oauth2-xsrf-token';
40 /** @private */
41 remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email';
42
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';
49
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;
56 };
57
58 /** @private
59  *  @return {string} API client ID.
60  */
61 remoting.OAuth2.prototype.getClientId_ = function() {
62   return remoting.settings.OAUTH2_CLIENT_ID;
63 };
64
65 /** @private
66  *  @return {string} API client secret.
67  */
68 remoting.OAuth2.prototype.getClientSecret_ = function() {
69   return remoting.settings.OAUTH2_CLIENT_SECRET;
70 };
71
72 /** @private
73  *  @return {string} OAuth2 authentication URL.
74  */
75 remoting.OAuth2.prototype.getOAuth2AuthEndpoint_ = function() {
76   return remoting.settings.OAUTH2_BASE_URL + '/auth';
77 };
78
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;
85 };
86
87 /**
88  * Removes all storage, and effectively unauthenticates the user.
89  *
90  * @return {void} Nothing.
91  */
92 remoting.OAuth2.prototype.clear = function() {
93   window.localStorage.removeItem(this.KEY_EMAIL_);
94   this.clearAccessToken_();
95   this.clearRefreshToken_();
96 };
97
98 /**
99  * Sets the refresh token.
100  *
101  * This method also marks the token as revokable, so that this object will
102  * revoke the token when it no longer needs it.
103  *
104  * @param {string} token The new refresh token.
105  * @return {void} Nothing.
106  * @private
107  */
108 remoting.OAuth2.prototype.setRefreshToken_ = function(token) {
109   window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token));
110   window.localStorage.setItem(this.KEY_REFRESH_TOKEN_REVOKABLE_, true);
111   window.localStorage.removeItem(this.KEY_EMAIL_);
112   this.clearAccessToken_();
113 };
114
115 /**
116  * Gets the refresh token.
117  *
118  * This method also marks the refresh token as not revokable, so that this
119  * object will not revoke the token when it no longer needs it. After this
120  * object has exported the token, it cannot know whether it is still in use
121  * when this object no longer needs it.
122  *
123  * @return {?string} The refresh token, if authenticated, or NULL.
124  */
125 remoting.OAuth2.prototype.exportRefreshToken = function() {
126   window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_REVOKABLE_);
127   return this.getRefreshToken_();
128 };
129
130 /**
131  * @return {?string} The refresh token, if authenticated, or NULL.
132  * @private
133  */
134 remoting.OAuth2.prototype.getRefreshToken_ = function() {
135   var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_);
136   if (typeof value == 'string') {
137     return unescape(value);
138   }
139   return null;
140 };
141
142 /**
143  * Clears the refresh token.
144  *
145  * @return {void} Nothing.
146  * @private
147  */
148 remoting.OAuth2.prototype.clearRefreshToken_ = function() {
149   if (window.localStorage.getItem(this.KEY_REFRESH_TOKEN_REVOKABLE_)) {
150     this.revokeToken_(this.getRefreshToken_());
151   }
152   window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_);
153   window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_REVOKABLE_);
154 };
155
156 /**
157  * @param {string} token The new access token.
158  * @param {number} expiration Expiration time in milliseconds since epoch.
159  * @return {void} Nothing.
160  * @private
161  */
162 remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) {
163   // Offset expiration by 120 seconds so that we can guarantee that the token
164   // we return will be valid for at least 2 minutes.
165   // If the access token is to be useful, this object must make some
166   // guarantee as to how long the token will be valid for.
167   // The choice of 2 minutes is arbitrary, but that length of time
168   // is part of the contract satisfied by callWithToken().
169   // Offset by a further 30 seconds to account for RTT issues.
170   var access_token = {
171     'token': token,
172     'expiration': (expiration - (120 + 30)) * 1000 + Date.now()
173   };
174   window.localStorage.setItem(this.KEY_ACCESS_TOKEN_,
175                               JSON.stringify(access_token));
176 };
177
178 /**
179  * Returns the current access token, setting it to a invalid value if none
180  * existed before.
181  *
182  * @private
183  * @return {{token: string, expiration: number}} The current access token, or
184  * an invalid token if not authenticated.
185  */
186 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() {
187   if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) {
188     // Always be able to return structured data.
189     this.setAccessToken_('', 0);
190   }
191   var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_);
192   if (typeof accessToken == 'string') {
193     var result = jsonParseSafe(accessToken);
194     if (result && 'token' in result && 'expiration' in result) {
195       return /** @type {{token: string, expiration: number}} */ result;
196     }
197   }
198   console.log('Invalid access token stored.');
199   return {'token': '', 'expiration': 0};
200 };
201
202 /**
203  * Returns true if the access token is expired, or otherwise invalid.
204  *
205  * Will throw if !isAuthenticated().
206  *
207  * @return {boolean} True if a new access token is needed.
208  * @private
209  */
210 remoting.OAuth2.prototype.needsNewAccessToken_ = function() {
211   if (!this.isAuthenticated()) {
212     throw 'Not Authenticated.';
213   }
214   var access_token = this.getAccessTokenInternal_();
215   if (!access_token['token']) {
216     return true;
217   }
218   if (Date.now() > access_token['expiration']) {
219     return true;
220   }
221   return false;
222 };
223
224 /**
225  * @return {void} Nothing.
226  * @private
227  */
228 remoting.OAuth2.prototype.clearAccessToken_ = function() {
229   window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_);
230 };
231
232 /**
233  * Update state based on token response from the OAuth2 /token endpoint.
234  *
235  * @param {function(string):void} onOk Called with the new access token.
236  * @param {string} accessToken Access token.
237  * @param {number} expiresIn Expiration time for the access token.
238  * @return {void} Nothing.
239  * @private
240  */
241 remoting.OAuth2.prototype.onAccessToken_ =
242     function(onOk, accessToken, expiresIn) {
243   this.setAccessToken_(accessToken, expiresIn);
244   onOk(accessToken);
245 };
246
247 /**
248  * Update state based on token response from the OAuth2 /token endpoint.
249  *
250  * @param {function():void} onOk Called after the new tokens are stored.
251  * @param {string} refreshToken Refresh token.
252  * @param {string} accessToken Access token.
253  * @param {number} expiresIn Expiration time for the access token.
254  * @return {void} Nothing.
255  * @private
256  */
257 remoting.OAuth2.prototype.onTokens_ =
258     function(onOk, refreshToken, accessToken, expiresIn) {
259   this.setAccessToken_(accessToken, expiresIn);
260   this.setRefreshToken_(refreshToken);
261   onOk();
262 };
263
264 /**
265  * Redirect page to get a new OAuth2 Refresh Token.
266  *
267  * @return {void} Nothing.
268  */
269 remoting.OAuth2.prototype.doAuthRedirect = function() {
270   /** @type {remoting.OAuth2} */
271   var that = this;
272   var xsrf_token = remoting.generateXsrfToken();
273   window.localStorage.setItem(this.KEY_XSRF_TOKEN_, xsrf_token);
274   var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' +
275     remoting.xhr.urlencodeParamHash({
276           'client_id': this.getClientId_(),
277           'redirect_uri': this.getRedirectUri_(),
278           'scope': this.SCOPE_,
279           'state': xsrf_token,
280           'response_type': 'code',
281           'access_type': 'offline',
282           'approval_prompt': 'force'
283         });
284
285   /**
286    * Processes the results of the oauth flow.
287    *
288    * @param {Object.<string, string>} message Dictionary containing the parsed
289    *   OAuth redirect URL parameters.
290    */
291   function oauth2MessageListener(message) {
292     if ('code' in message && 'state' in message) {
293       var onDone = function() {
294         window.location.reload();
295       };
296       that.exchangeCodeForToken(
297           message['code'], message['state'], onDone);
298     } else {
299       if ('error' in message) {
300         console.error(
301             'Could not obtain authorization code: ' + message['error']);
302       } else {
303         // We intentionally don't log the response - since we don't understand
304         // it, we can't tell if it has sensitive data.
305         console.error('Invalid oauth2 response.');
306       }
307     }
308     chrome.extension.onMessage.removeListener(oauth2MessageListener);
309   }
310   chrome.extension.onMessage.addListener(oauth2MessageListener);
311   window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no');
312 };
313
314 /**
315  * Asynchronously exchanges an authorization code for a refresh token.
316  *
317  * @param {string} code The OAuth2 authorization code.
318  * @param {string} state The state parameter received from the OAuth redirect.
319  * @param {function():void} onDone Callback to invoke on completion.
320  * @return {void} Nothing.
321  */
322 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, state, onDone) {
323   var xsrf_token = window.localStorage.getItem(this.KEY_XSRF_TOKEN_);
324   window.localStorage.removeItem(this.KEY_XSRF_TOKEN_);
325   if (xsrf_token == undefined || state != xsrf_token) {
326     // Invalid XSRF token, or unexpected OAuth2 redirect. Abort.
327     onDone();
328   }
329   /** @param {remoting.Error} error */
330   var onError = function(error) {
331     console.error('Unable to exchange code for token: ', error);
332   };
333
334   remoting.OAuth2Api.exchangeCodeForTokens(
335       this.onTokens_.bind(this, onDone), onError,
336       this.getClientId_(), this.getClientSecret_(), code,
337       this.getRedirectUri_());
338 };
339
340 /**
341  * Revokes a refresh or an access token.
342  *
343  * @param {string?} token An access or refresh token.
344  * @return {void} Nothing.
345  * @private
346  */
347 remoting.OAuth2.prototype.revokeToken_ = function(token) {
348   if (!token || (token.length == 0)) {
349     return;
350   }
351
352   remoting.OAuth2Api.revokeToken(function() {}, function() {}, token);
353 };
354
355 /**
356  * Call a function with an access token, refreshing it first if necessary.
357  * The access token will remain valid for at least 2 minutes.
358  *
359  * @param {function(string):void} onOk Function to invoke with access token if
360  *     an access token was successfully retrieved.
361  * @param {function(remoting.Error):void} onError Function to invoke with an
362  *     error code on failure.
363  * @return {void} Nothing.
364  */
365 remoting.OAuth2.prototype.callWithToken = function(onOk, onError) {
366   var refreshToken = this.getRefreshToken_();
367   if (refreshToken) {
368     if (this.needsNewAccessToken_()) {
369       remoting.OAuth2Api.refreshAccessToken(
370           this.onAccessToken_.bind(this, onOk), onError,
371           this.getClientId_(), this.getClientSecret_(),
372           refreshToken);
373     } else {
374       onOk(this.getAccessTokenInternal_()['token']);
375     }
376   } else {
377     onError(remoting.Error.NOT_AUTHENTICATED);
378   }
379 };
380
381 /**
382  * Get the user's email address.
383  *
384  * @param {function(string):void} onOk Callback invoked when the email
385  *     address is available.
386  * @param {function(remoting.Error):void} onError Callback invoked if an
387  *     error occurs.
388  * @return {void} Nothing.
389  */
390 remoting.OAuth2.prototype.getEmail = function(onOk, onError) {
391   var cached = window.localStorage.getItem(this.KEY_EMAIL_);
392   if (typeof cached == 'string') {
393     onOk(cached);
394     return;
395   }
396   /** @type {remoting.OAuth2} */
397   var that = this;
398   /** @param {string} email */
399   var onResponse = function(email) {
400     window.localStorage.setItem(that.KEY_EMAIL_, email);
401     onOk(email);
402   };
403
404   this.callWithToken(
405       remoting.OAuth2Api.getEmail.bind(null, onResponse, onError), onError);
406 };
407
408 /**
409  * If the user's email address is cached, return it, otherwise return null.
410  *
411  * @return {?string} The email address, if it has been cached by a previous call
412  *     to getEmail, otherwise null.
413  */
414 remoting.OAuth2.prototype.getCachedEmail = function() {
415   var value = window.localStorage.getItem(this.KEY_EMAIL_);
416   if (typeof value == 'string') {
417     return value;
418   }
419   return null;
420 };