| 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 |