| OLD | NEW |
| (Empty) |
| 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 /** | |
| 27 * @constructor | |
| 28 * @extends {remoting.Identity} | |
| 29 */ | |
| 30 remoting.OAuth2 = function() { | |
| 31 }; | |
| 32 | |
| 33 // Constants representing keys used for storing persistent state. | |
| 34 /** @private */ | |
| 35 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; | |
| 36 /** @private */ | |
| 37 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; | |
| 38 /** @private */ | |
| 39 remoting.OAuth2.prototype.KEY_EMAIL_ = 'remoting-email'; | |
| 40 /** @private */ | |
| 41 remoting.OAuth2.prototype.KEY_FULLNAME_ = 'remoting-fullname'; | |
| 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 * Remove the cached auth token, if any. | |
| 89 * | |
| 90 * @return {!Promise<null>} A promise resolved with the operation completes. | |
| 91 */ | |
| 92 remoting.OAuth2.prototype.removeCachedAuthToken = function() { | |
| 93 window.localStorage.removeItem(this.KEY_EMAIL_); | |
| 94 window.localStorage.removeItem(this.KEY_FULLNAME_); | |
| 95 this.clearAccessToken_(); | |
| 96 this.clearRefreshToken_(); | |
| 97 return Promise.resolve(null); | |
| 98 }; | |
| 99 | |
| 100 /** | |
| 101 * Sets the refresh token. | |
| 102 * | |
| 103 * @param {string} token The new refresh token. | |
| 104 * @return {void} Nothing. | |
| 105 * @private | |
| 106 */ | |
| 107 remoting.OAuth2.prototype.setRefreshToken_ = function(token) { | |
| 108 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); | |
| 109 window.localStorage.removeItem(this.KEY_EMAIL_); | |
| 110 window.localStorage.removeItem(this.KEY_FULLNAME_); | |
| 111 this.clearAccessToken_(); | |
| 112 }; | |
| 113 | |
| 114 /** | |
| 115 * @return {?string} The refresh token, if authenticated, or NULL. | |
| 116 */ | |
| 117 remoting.OAuth2.prototype.getRefreshToken = function() { | |
| 118 var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_); | |
| 119 if (typeof value == 'string') { | |
| 120 return unescape(value); | |
| 121 } | |
| 122 return null; | |
| 123 }; | |
| 124 | |
| 125 /** | |
| 126 * Clears the refresh token. | |
| 127 * | |
| 128 * @return {void} Nothing. | |
| 129 * @private | |
| 130 */ | |
| 131 remoting.OAuth2.prototype.clearRefreshToken_ = function() { | |
| 132 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); | |
| 133 }; | |
| 134 | |
| 135 /** | |
| 136 * @param {string} token The new access token. | |
| 137 * @param {number} expiration Expiration time in milliseconds since epoch. | |
| 138 * @return {void} Nothing. | |
| 139 * @private | |
| 140 */ | |
| 141 remoting.OAuth2.prototype.setAccessToken_ = function(token, expiration) { | |
| 142 // Offset expiration by 120 seconds so that we can guarantee that the token | |
| 143 // we return will be valid for at least 2 minutes. | |
| 144 // If the access token is to be useful, this object must make some | |
| 145 // guarantee as to how long the token will be valid for. | |
| 146 // The choice of 2 minutes is arbitrary, but that length of time | |
| 147 // is part of the contract satisfied by callWithToken(). | |
| 148 // Offset by a further 30 seconds to account for RTT issues. | |
| 149 var access_token = { | |
| 150 'token': token, | |
| 151 'expiration': (expiration - (120 + 30)) * 1000 + Date.now() | |
| 152 }; | |
| 153 window.localStorage.setItem(this.KEY_ACCESS_TOKEN_, | |
| 154 JSON.stringify(access_token)); | |
| 155 }; | |
| 156 | |
| 157 /** | |
| 158 * Returns the current access token, setting it to a invalid value if none | |
| 159 * existed before. | |
| 160 * | |
| 161 * @private | |
| 162 * @return {{token: string, expiration: number}} The current access token, or | |
| 163 * an invalid token if not authenticated. | |
| 164 */ | |
| 165 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() { | |
| 166 if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) { | |
| 167 // Always be able to return structured data. | |
| 168 this.setAccessToken_('', 0); | |
| 169 } | |
| 170 var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_); | |
| 171 if (typeof accessToken == 'string') { | |
| 172 var result = base.jsonParseSafe(accessToken); | |
| 173 if (result && 'token' in result && 'expiration' in result) { | |
| 174 return /** @type {{token: string, expiration: number}} */(result); | |
| 175 } | |
| 176 } | |
| 177 console.log('Invalid access token stored.'); | |
| 178 return {'token': '', 'expiration': 0}; | |
| 179 }; | |
| 180 | |
| 181 /** | |
| 182 * Returns true if the access token is expired, or otherwise invalid. | |
| 183 * | |
| 184 * Will throw if !isAuthenticated(). | |
| 185 * | |
| 186 * @return {boolean} True if a new access token is needed. | |
| 187 * @private | |
| 188 */ | |
| 189 remoting.OAuth2.prototype.needsNewAccessToken_ = function() { | |
| 190 if (!this.isAuthenticated()) { | |
| 191 throw 'Not Authenticated.'; | |
| 192 } | |
| 193 var access_token = this.getAccessTokenInternal_(); | |
| 194 if (!access_token['token']) { | |
| 195 return true; | |
| 196 } | |
| 197 if (Date.now() > access_token['expiration']) { | |
| 198 return true; | |
| 199 } | |
| 200 return false; | |
| 201 }; | |
| 202 | |
| 203 /** | |
| 204 * @return {void} Nothing. | |
| 205 * @private | |
| 206 */ | |
| 207 remoting.OAuth2.prototype.clearAccessToken_ = function() { | |
| 208 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); | |
| 209 }; | |
| 210 | |
| 211 /** | |
| 212 * Update state based on token response from the OAuth2 /token endpoint. | |
| 213 * | |
| 214 * @param {function(string):void} onOk Called with the new access token. | |
| 215 * @param {string} accessToken Access token. | |
| 216 * @param {number} expiresIn Expiration time for the access token. | |
| 217 * @return {void} Nothing. | |
| 218 * @private | |
| 219 */ | |
| 220 remoting.OAuth2.prototype.onAccessToken_ = | |
| 221 function(onOk, accessToken, expiresIn) { | |
| 222 this.setAccessToken_(accessToken, expiresIn); | |
| 223 onOk(accessToken); | |
| 224 }; | |
| 225 | |
| 226 /** | |
| 227 * Update state based on token response from the OAuth2 /token endpoint. | |
| 228 * | |
| 229 * @param {function():void} onOk Called after the new tokens are stored. | |
| 230 * @param {string} refreshToken Refresh token. | |
| 231 * @param {string} accessToken Access token. | |
| 232 * @param {number} expiresIn Expiration time for the access token. | |
| 233 * @return {void} Nothing. | |
| 234 * @private | |
| 235 */ | |
| 236 remoting.OAuth2.prototype.onTokens_ = | |
| 237 function(onOk, refreshToken, accessToken, expiresIn) { | |
| 238 this.setAccessToken_(accessToken, expiresIn); | |
| 239 this.setRefreshToken_(refreshToken); | |
| 240 onOk(); | |
| 241 }; | |
| 242 | |
| 243 /** | |
| 244 * Redirect page to get a new OAuth2 authorization code | |
| 245 * | |
| 246 * @param {function(?string):void} onDone Completion callback to receive | |
| 247 * the authorization code, or null on error. | |
| 248 * @return {void} Nothing. | |
| 249 */ | |
| 250 remoting.OAuth2.prototype.getAuthorizationCode = function(onDone) { | |
| 251 var xsrf_token = base.generateXsrfToken(); | |
| 252 var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' + | |
| 253 remoting.Xhr.urlencodeParamHash({ | |
| 254 'client_id': this.getClientId_(), | |
| 255 'redirect_uri': this.getRedirectUri_(), | |
| 256 'scope': this.SCOPE_, | |
| 257 'state': xsrf_token, | |
| 258 'response_type': 'code', | |
| 259 'access_type': 'offline', | |
| 260 'approval_prompt': 'force' | |
| 261 }); | |
| 262 | |
| 263 /** | |
| 264 * Processes the results of the oauth flow. | |
| 265 * | |
| 266 * @param {Object<string, string>} message Dictionary containing the parsed | |
| 267 * OAuth redirect URL parameters. | |
| 268 * @param {function(*)} sendResponse Function to send response. | |
| 269 */ | |
| 270 function oauth2MessageListener(message, sender, sendResponse) { | |
| 271 if ('code' in message && 'state' in message) { | |
| 272 if (message['state'] == xsrf_token) { | |
| 273 onDone(message['code']); | |
| 274 } else { | |
| 275 console.error('Invalid XSRF token.'); | |
| 276 onDone(null); | |
| 277 } | |
| 278 } else { | |
| 279 if ('error' in message) { | |
| 280 console.error( | |
| 281 'Could not obtain authorization code: ' + message['error']); | |
| 282 } else { | |
| 283 // We intentionally don't log the response - since we don't understand | |
| 284 // it, we can't tell if it has sensitive data. | |
| 285 console.error('Invalid oauth2 response.'); | |
| 286 } | |
| 287 onDone(null); | |
| 288 } | |
| 289 chrome.extension.onMessage.removeListener(oauth2MessageListener); | |
| 290 sendResponse(null); | |
| 291 } | |
| 292 chrome.extension.onMessage.addListener(oauth2MessageListener); | |
| 293 window.open(GET_CODE_URL, '_blank', 'location=yes,toolbar=no,menubar=no'); | |
| 294 }; | |
| 295 | |
| 296 /** | |
| 297 * Redirect page to get a new OAuth Refresh Token. | |
| 298 * | |
| 299 * @param {function():void} onDone Completion callback. | |
| 300 * @return {void} Nothing. | |
| 301 */ | |
| 302 remoting.OAuth2.prototype.doAuthRedirect = function(onDone) { | |
| 303 /** @type {remoting.OAuth2} */ | |
| 304 var that = this; | |
| 305 /** @param {?string} code */ | |
| 306 var onAuthorizationCode = function(code) { | |
| 307 if (code) { | |
| 308 that.exchangeCodeForToken(code, onDone); | |
| 309 } else { | |
| 310 onDone(); | |
| 311 } | |
| 312 }; | |
| 313 this.getAuthorizationCode(onAuthorizationCode); | |
| 314 }; | |
| 315 | |
| 316 /** | |
| 317 * Asynchronously exchanges an authorization code for a refresh token. | |
| 318 * | |
| 319 * @param {string} code The OAuth2 authorization code. | |
| 320 * @param {function():void} onDone Callback to invoke on completion. | |
| 321 * @return {void} Nothing. | |
| 322 */ | |
| 323 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) { | |
| 324 /** @param {!remoting.Error} error */ | |
| 325 var onError = function(error) { | |
| 326 console.error('Unable to exchange code for token: ' + error.toString()); | |
| 327 }; | |
| 328 | |
| 329 remoting.oauth2Api.exchangeCodeForTokens( | |
| 330 this.onTokens_.bind(this, onDone), onError, | |
| 331 this.getClientId_(), this.getClientSecret_(), code, | |
| 332 this.getRedirectUri_()); | |
| 333 }; | |
| 334 | |
| 335 /** | |
| 336 * Print a command-line that can be used to register a host on Linux platforms. | |
| 337 */ | |
| 338 remoting.OAuth2.prototype.printStartHostCommandLine = function() { | |
| 339 /** @type {string} */ | |
| 340 var redirectUri = this.getRedirectUri_(); | |
| 341 /** @param {?string} code */ | |
| 342 var onAuthorizationCode = function(code) { | |
| 343 if (code) { | |
| 344 console.log('Run the following command to register a host:'); | |
| 345 console.log( | |
| 346 '%c/opt/google/chrome-remote-desktop/start-host' + | |
| 347 ' --code=' + code + | |
| 348 ' --redirect-url=' + redirectUri + | |
| 349 ' --name=$HOSTNAME', 'font-weight: bold;'); | |
| 350 } | |
| 351 }; | |
| 352 this.getAuthorizationCode(onAuthorizationCode); | |
| 353 }; | |
| 354 | |
| 355 /** | |
| 356 * Get an access token, refreshing it first if necessary. The access | |
| 357 * token will remain valid for at least 2 minutes. | |
| 358 * | |
| 359 * @return {!Promise<string>} A promise resolved the an access token or | |
| 360 * rejected with a remoting.Error. | |
| 361 */ | |
| 362 remoting.OAuth2.prototype.getToken = function() { | |
| 363 /** @const */ | |
| 364 var that = this; | |
| 365 | |
| 366 return new Promise(function(resolve, reject) { | |
| 367 var refreshToken = that.getRefreshToken(); | |
| 368 if (refreshToken) { | |
| 369 if (that.needsNewAccessToken_()) { | |
| 370 remoting.oauth2Api.refreshAccessToken( | |
| 371 that.onAccessToken_.bind(that, resolve), reject, | |
| 372 that.getClientId_(), that.getClientSecret_(), | |
| 373 refreshToken); | |
| 374 } else { | |
| 375 resolve(that.getAccessTokenInternal_()['token']); | |
| 376 } | |
| 377 } else { | |
| 378 reject(new remoting.Error(remoting.Error.Tag.NOT_AUTHENTICATED)); | |
| 379 } | |
| 380 }); | |
| 381 }; | |
| 382 | |
| 383 /** | |
| 384 * Get the user's email address. | |
| 385 * | |
| 386 * @return {!Promise<string>} Promise resolved with the user's email | |
| 387 * address or rejected with a remoting.Error. | |
| 388 */ | |
| 389 remoting.OAuth2.prototype.getEmail = function() { | |
| 390 var cached = window.localStorage.getItem(this.KEY_EMAIL_); | |
| 391 if (typeof cached == 'string') { | |
| 392 return Promise.resolve(cached); | |
| 393 } | |
| 394 /** @type {remoting.OAuth2} */ | |
| 395 var that = this; | |
| 396 | |
| 397 return new Promise(function(resolve, reject) { | |
| 398 /** @param {string} email */ | |
| 399 var onResponse = function(email) { | |
| 400 window.localStorage.setItem(that.KEY_EMAIL_, email); | |
| 401 window.localStorage.setItem(that.KEY_FULLNAME_, ''); | |
| 402 resolve(email); | |
| 403 }; | |
| 404 | |
| 405 that.getToken().then( | |
| 406 remoting.oauth2Api.getEmail.bind( | |
| 407 remoting.oauth2Api, onResponse, reject), | |
| 408 reject); | |
| 409 }); | |
| 410 }; | |
| 411 | |
| 412 /** | |
| 413 * Get the user's email address and full name. | |
| 414 * | |
| 415 * @return {!Promise<{email: string, name: string}>} Promise | |
| 416 * resolved with the user's email address and full name, or rejected | |
| 417 * with a remoting.Error. | |
| 418 */ | |
| 419 remoting.OAuth2.prototype.getUserInfo = function() { | |
| 420 var cachedEmail = window.localStorage.getItem(this.KEY_EMAIL_); | |
| 421 var cachedName = window.localStorage.getItem(this.KEY_FULLNAME_); | |
| 422 if (typeof cachedEmail == 'string' && typeof cachedName == 'string') { | |
| 423 /** | |
| 424 * The temp variable is needed to work around a compiler bug. | |
| 425 * @type {{email: string, name: string}} | |
| 426 */ | |
| 427 var result = {email: cachedEmail, name: cachedName}; | |
| 428 return Promise.resolve(result); | |
| 429 } | |
| 430 | |
| 431 /** @type {remoting.OAuth2} */ | |
| 432 var that = this; | |
| 433 | |
| 434 return new Promise(function(resolve, reject) { | |
| 435 /** | |
| 436 * @param {string} email | |
| 437 * @param {string} name | |
| 438 */ | |
| 439 var onResponse = function(email, name) { | |
| 440 window.localStorage.setItem(that.KEY_EMAIL_, email); | |
| 441 window.localStorage.setItem(that.KEY_FULLNAME_, name); | |
| 442 resolve({email: email, name: name}); | |
| 443 }; | |
| 444 | |
| 445 that.getToken().then( | |
| 446 remoting.oauth2Api.getUserInfo.bind( | |
| 447 remoting.oauth2Api, onResponse, reject), | |
| 448 reject); | |
| 449 }); | |
| 450 }; | |
| OLD | NEW |