| OLD | NEW | 
 | (Empty) | 
|    1 // Copyright (c) 2011 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 'use strict'; |  | 
|   15  |  | 
|   16 /** @suppress {duplicate} */ |  | 
|   17 var remoting = remoting || {}; |  | 
|   18  |  | 
|   19 /** @type {remoting.OAuth2} */ |  | 
|   20 remoting.oauth2 = null; |  | 
|   21  |  | 
|   22  |  | 
|   23 /** @constructor */ |  | 
|   24 remoting.OAuth2 = function() { |  | 
|   25 }; |  | 
|   26  |  | 
|   27 // Constants representing keys used for storing persistent state. |  | 
|   28 /** @private */ |  | 
|   29 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; |  | 
|   30 /** @private */ |  | 
|   31 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; |  | 
|   32 /** @private */ |  | 
|   33 remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email'; |  | 
|   34  |  | 
|   35 // Constants for parameters used in retrieving the OAuth2 credentials. |  | 
|   36 /** @private */ |  | 
|   37 remoting.OAuth2.prototype.CLIENT_ID_ = |  | 
|   38       '440925447803-2pi3v45bff6tp1rde2f7q6lgbor3o5uj.' + |  | 
|   39       'apps.googleusercontent.com'; |  | 
|   40 /** @private */ |  | 
|   41 remoting.OAuth2.prototype.CLIENT_SECRET_ = 'W2ieEsG-R1gIA4MMurGrgMc_'; |  | 
|   42 /** @private */ |  | 
|   43 remoting.OAuth2.prototype.SCOPE_ = |  | 
|   44       'https://www.googleapis.com/auth/chromoting ' + |  | 
|   45       'https://www.googleapis.com/auth/googletalk ' + |  | 
|   46       'https://www.googleapis.com/auth/userinfo#email'; |  | 
|   47 /** @private */ |  | 
|   48 remoting.OAuth2.prototype.REDIRECT_URI_ = |  | 
|   49       'https://talkgadget.google.com/talkgadget/blank'; |  | 
|   50 /** @private */ |  | 
|   51 remoting.OAuth2.prototype.OAUTH2_TOKEN_ENDPOINT_ = |  | 
|   52     'https://accounts.google.com/o/oauth2/token'; |  | 
|   53  |  | 
|   54 /** @return {boolean} True if the app is already authenticated. */ |  | 
|   55 remoting.OAuth2.prototype.isAuthenticated = function() { |  | 
|   56   if (this.getRefreshToken()) { |  | 
|   57     return true; |  | 
|   58   } |  | 
|   59   return false; |  | 
|   60 }; |  | 
|   61  |  | 
|   62 /** |  | 
|   63  * Removes all storage, and effectively unauthenticates the user. |  | 
|   64  * |  | 
|   65  * @return {void} Nothing. |  | 
|   66  */ |  | 
|   67 remoting.OAuth2.prototype.clear = function() { |  | 
|   68   window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); |  | 
|   69   window.localStorage.removeItem(this.KEY_EMAIL_); |  | 
|   70   this.clearAccessToken(); |  | 
|   71 }; |  | 
|   72  |  | 
|   73 /** |  | 
|   74  * @param {string} token The new refresh token. |  | 
|   75  * @return {void} Nothing. |  | 
|   76  */ |  | 
|   77 remoting.OAuth2.prototype.setRefreshToken = function(token) { |  | 
|   78   window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); |  | 
|   79   this.clearAccessToken(); |  | 
|   80 }; |  | 
|   81  |  | 
|   82 /** @return {?string} The refresh token, if authenticated, or NULL. */ |  | 
|   83 remoting.OAuth2.prototype.getRefreshToken = function() { |  | 
|   84   var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_); |  | 
|   85   if (typeof value == 'string') { |  | 
|   86     return unescape(value); |  | 
|   87   } |  | 
|   88   return null; |  | 
|   89 }; |  | 
|   90  |  | 
|   91 /** |  | 
|   92  * @param {string} token The new access token. |  | 
|   93  * @param {number} expiration Expiration time in milliseconds since epoch. |  | 
|   94  * @return {void} Nothing. |  | 
|   95  */ |  | 
|   96 remoting.OAuth2.prototype.setAccessToken = function(token, expiration) { |  | 
|   97   var access_token = {'token': token, 'expiration': expiration}; |  | 
|   98   window.localStorage.setItem(this.KEY_ACCESS_TOKEN_, |  | 
|   99                               JSON.stringify(access_token)); |  | 
|  100 }; |  | 
|  101  |  | 
|  102 /** |  | 
|  103  * Returns the current access token, setting it to a invalid value if none |  | 
|  104  * existed before. |  | 
|  105  * |  | 
|  106  * @private |  | 
|  107  * @return {{token: string, expiration: number}} The current access token, or |  | 
|  108  * an invalid token if not authenticated. |  | 
|  109  */ |  | 
|  110 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() { |  | 
|  111   if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) { |  | 
|  112     // Always be able to return structured data. |  | 
|  113     this.setAccessToken('', 0); |  | 
|  114   } |  | 
|  115   var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_); |  | 
|  116   if (typeof accessToken == 'string') { |  | 
|  117     var result = JSON.parse(accessToken); |  | 
|  118     if ('token' in result && 'expiration' in result) { |  | 
|  119       return /** @type {{token: string, expiration: number}} */ result; |  | 
|  120     } |  | 
|  121   } |  | 
|  122   console.log('Invalid access token stored.'); |  | 
|  123   return {'token': '', 'expiration': 0}; |  | 
|  124 }; |  | 
|  125  |  | 
|  126 /** |  | 
|  127  * Returns true if the access token is expired, or otherwise invalid. |  | 
|  128  * |  | 
|  129  * Will throw if !isAuthenticated(). |  | 
|  130  * |  | 
|  131  * @return {boolean} True if a new access token is needed. |  | 
|  132  */ |  | 
|  133 remoting.OAuth2.prototype.needsNewAccessToken = function() { |  | 
|  134   if (!this.isAuthenticated()) { |  | 
|  135     throw 'Not Authenticated.'; |  | 
|  136   } |  | 
|  137   var access_token = this.getAccessTokenInternal_(); |  | 
|  138   if (!access_token['token']) { |  | 
|  139     return true; |  | 
|  140   } |  | 
|  141   if (Date.now() > access_token['expiration']) { |  | 
|  142     return true; |  | 
|  143   } |  | 
|  144   return false; |  | 
|  145 }; |  | 
|  146  |  | 
|  147 /** |  | 
|  148  * Returns the current access token. |  | 
|  149  * |  | 
|  150  * Will throw if !isAuthenticated() or needsNewAccessToken(). |  | 
|  151  * |  | 
|  152  * @return {string} The access token. |  | 
|  153  */ |  | 
|  154 remoting.OAuth2.prototype.getAccessToken = function() { |  | 
|  155   if (this.needsNewAccessToken()) { |  | 
|  156     throw 'Access Token expired.'; |  | 
|  157   } |  | 
|  158   return this.getAccessTokenInternal_()['token']; |  | 
|  159 }; |  | 
|  160  |  | 
|  161 /** @return {void} Nothing. */ |  | 
|  162 remoting.OAuth2.prototype.clearAccessToken = function() { |  | 
|  163   window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); |  | 
|  164 }; |  | 
|  165  |  | 
|  166 /** |  | 
|  167  * Update state based on token response from the OAuth2 /token endpoint. |  | 
|  168  * |  | 
|  169  * @private |  | 
|  170  * @param {XMLHttpRequest} xhr The XHR object for this request. |  | 
|  171  * @return {void} Nothing. |  | 
|  172  */ |  | 
|  173 remoting.OAuth2.prototype.processTokenResponse_ = function(xhr) { |  | 
|  174   if (xhr.status == 200) { |  | 
|  175     var tokens = JSON.parse(xhr.responseText); |  | 
|  176     if ('refresh_token' in tokens) { |  | 
|  177       this.setRefreshToken(tokens['refresh_token']); |  | 
|  178     } |  | 
|  179  |  | 
|  180     // Offset by 120 seconds so that we can guarantee that the token |  | 
|  181     // we return will be valid for at least 2 minutes. |  | 
|  182     // If the access token is to be useful, this object must make some |  | 
|  183     // guarantee as to how long the token will be valid for. |  | 
|  184     // The choice of 2 minutes is arbitrary, but that length of time |  | 
|  185     // is part of the contract satisfied by callWithToken(). |  | 
|  186     // Offset by a further 30 seconds to account for RTT issues. |  | 
|  187     this.setAccessToken(tokens['access_token'], |  | 
|  188         (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); |  | 
|  189   } else { |  | 
|  190     console.log('Failed to get tokens. Status: ' + xhr.status + |  | 
|  191                 ' response: ' + xhr.responseText); |  | 
|  192   } |  | 
|  193 }; |  | 
|  194  |  | 
|  195 /** |  | 
|  196  * Asynchronously retrieves a new access token from the server. |  | 
|  197  * |  | 
|  198  * Will throw if !isAuthenticated(). |  | 
|  199  * |  | 
|  200  * @param {function(XMLHttpRequest): void} onDone Callback to invoke on |  | 
|  201  *     completion. |  | 
|  202  * @return {void} Nothing. |  | 
|  203  */ |  | 
|  204 remoting.OAuth2.prototype.refreshAccessToken = function(onDone) { |  | 
|  205   if (!this.isAuthenticated()) { |  | 
|  206     throw 'Not Authenticated.'; |  | 
|  207   } |  | 
|  208  |  | 
|  209   var parameters = { |  | 
|  210     'client_id': this.CLIENT_ID_, |  | 
|  211     'client_secret': this.CLIENT_SECRET_, |  | 
|  212     'refresh_token': this.getRefreshToken(), |  | 
|  213     'grant_type': 'refresh_token' |  | 
|  214   }; |  | 
|  215  |  | 
|  216   /** @type {remoting.OAuth2} */ |  | 
|  217   var that = this; |  | 
|  218   /** @param {XMLHttpRequest} xhr The XHR reply. */ |  | 
|  219   var processTokenResponse = function(xhr) { |  | 
|  220     that.processTokenResponse_(xhr); |  | 
|  221     onDone(xhr); |  | 
|  222   }; |  | 
|  223   remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, |  | 
|  224                     processTokenResponse, |  | 
|  225                     parameters); |  | 
|  226 }; |  | 
|  227  |  | 
|  228 /** |  | 
|  229  * Redirect page to get a new OAuth2 Refresh Token. |  | 
|  230  * |  | 
|  231  * @return {void} Nothing. |  | 
|  232  */ |  | 
|  233 remoting.OAuth2.prototype.doAuthRedirect = function() { |  | 
|  234   var GET_CODE_URL = 'https://accounts.google.com/o/oauth2/auth?' + |  | 
|  235     remoting.xhr.urlencodeParamHash({ |  | 
|  236           'client_id': this.CLIENT_ID_, |  | 
|  237           'redirect_uri': this.REDIRECT_URI_, |  | 
|  238           'scope': this.SCOPE_, |  | 
|  239           'response_type': 'code', |  | 
|  240           'access_type': 'offline', |  | 
|  241           'approval_prompt': 'force' |  | 
|  242         }); |  | 
|  243   window.location.replace(GET_CODE_URL); |  | 
|  244 }; |  | 
|  245  |  | 
|  246 /** |  | 
|  247  * Asynchronously exchanges an authorization code for a refresh token. |  | 
|  248  * |  | 
|  249  * @param {string} code The new refresh token. |  | 
|  250  * @param {function(XMLHttpRequest):void} onDone Callback to invoke on |  | 
|  251  *     completion. |  | 
|  252  * @return {void} Nothing. |  | 
|  253  */ |  | 
|  254 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) { |  | 
|  255   var parameters = { |  | 
|  256     'client_id': this.CLIENT_ID_, |  | 
|  257     'client_secret': this.CLIENT_SECRET_, |  | 
|  258     'redirect_uri': this.REDIRECT_URI_, |  | 
|  259     'code': code, |  | 
|  260     'grant_type': 'authorization_code' |  | 
|  261   }; |  | 
|  262  |  | 
|  263   /** @type {remoting.OAuth2} */ |  | 
|  264   var that = this; |  | 
|  265   /** @param {XMLHttpRequest} xhr The XHR reply. */ |  | 
|  266   var processTokenResponse = function(xhr) { |  | 
|  267     that.processTokenResponse_(xhr); |  | 
|  268     onDone(xhr); |  | 
|  269   }; |  | 
|  270   remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, |  | 
|  271                     processTokenResponse, |  | 
|  272                     parameters); |  | 
|  273 }; |  | 
|  274  |  | 
|  275 /** |  | 
|  276  * Call myfunc with an access token as the only parameter. |  | 
|  277  * |  | 
|  278  * This will refresh the access token if necessary.  If the access token |  | 
|  279  * cannot be refreshed, an error is thrown. |  | 
|  280  * |  | 
|  281  * The access token will remain valid for at least 2 minutes. |  | 
|  282  * |  | 
|  283  * @param {function(string):void} myfunc |  | 
|  284  *        Function to invoke with access token. |  | 
|  285  * @return {void} Nothing. |  | 
|  286  */ |  | 
|  287 remoting.OAuth2.prototype.callWithToken = function(myfunc) { |  | 
|  288   /** @type {remoting.OAuth2} */ |  | 
|  289   var that = this; |  | 
|  290   if (remoting.oauth2.needsNewAccessToken()) { |  | 
|  291     remoting.oauth2.refreshAccessToken(function() { |  | 
|  292       if (remoting.oauth2.needsNewAccessToken()) { |  | 
|  293         // If we still need it, we're going to infinite loop. |  | 
|  294         throw 'Unable to get access token.'; |  | 
|  295       } |  | 
|  296       myfunc(that.getAccessToken()); |  | 
|  297     }); |  | 
|  298     return; |  | 
|  299   } |  | 
|  300  |  | 
|  301   myfunc(this.getAccessToken()); |  | 
|  302 }; |  | 
|  303  |  | 
|  304 /** |  | 
|  305  * Get the user's email address. |  | 
|  306  * |  | 
|  307  * @param {function(?string):void} setEmail Callback invoked when the email |  | 
|  308  *     address is available, or on error. |  | 
|  309  * @return {void} Nothing. |  | 
|  310  */ |  | 
|  311 remoting.OAuth2.prototype.getEmail = function(setEmail) { |  | 
|  312   /** @type {remoting.OAuth2} */ |  | 
|  313   var that = this; |  | 
|  314   /** @param {XMLHttpRequest} xhr The XHR response. */ |  | 
|  315   var onResponse = function(xhr) { |  | 
|  316     that.email = null; |  | 
|  317     if (xhr.status == 200) { |  | 
|  318       // TODO(ajwong): See if we can't find a JSON endpoint. |  | 
|  319       that.email = xhr.responseText.split('&')[0].split('=')[1]; |  | 
|  320     } |  | 
|  321     window.localStorage.setItem(that.KEY_EMAIL_, that.email); |  | 
|  322     setEmail(that.email); |  | 
|  323   }; |  | 
|  324  |  | 
|  325   /** @param {string} token The access token. */ |  | 
|  326   var getEmailFromToken = function(token) { |  | 
|  327     var headers = { 'Authorization': 'OAuth ' + token }; |  | 
|  328     // TODO(ajwong): Update to new v2 API. |  | 
|  329     remoting.xhr.get('https://www.googleapis.com/userinfo/email', |  | 
|  330                      onResponse, '', headers); |  | 
|  331   }; |  | 
|  332  |  | 
|  333   this.callWithToken(getEmailFromToken); |  | 
|  334 }; |  | 
|  335  |  | 
|  336 /** |  | 
|  337  * If the user's email address is cached, return it, otherwise return null. |  | 
|  338  * |  | 
|  339  * @return {?string} The email address, if it has been cached by a previous call |  | 
|  340  *     to getEmail, otherwise null. |  | 
|  341  */ |  | 
|  342 remoting.OAuth2.prototype.getCachedEmail = function() { |  | 
|  343   var value = window.localStorage.getItem(this.KEY_EMAIL_); |  | 
|  344   if (typeof value == 'string') { |  | 
|  345     return value; |  | 
|  346   } |  | 
|  347   return null; |  | 
|  348 }; |  | 
| OLD | NEW |