Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview | 6 * @fileoverview |
| 7 * OAuth2 class that handles retrieval/storage of an OAuth2 token. | 7 * OAuth2 class that handles retrieval/storage of an OAuth2 token. |
| 8 * | 8 * |
| 9 * Uses a content script to trampoline the OAuth redirect page back into the | 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 | 10 * extension context. This works around the lack of native support for |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 55 return false; | 55 return false; |
| 56 }; | 56 }; |
| 57 | 57 |
| 58 /** | 58 /** |
| 59 * Removes all storage, and effectively unauthenticates the user. | 59 * Removes all storage, and effectively unauthenticates the user. |
| 60 * | 60 * |
| 61 * @return {void} Nothing. | 61 * @return {void} Nothing. |
| 62 */ | 62 */ |
| 63 remoting.OAuth2.prototype.clear = function() { | 63 remoting.OAuth2.prototype.clear = function() { |
| 64 window.localStorage.removeItem(this.KEY_EMAIL_); | 64 window.localStorage.removeItem(this.KEY_EMAIL_); |
| 65 this.clearAccessToken(); | 65 this.clearAccessToken_(); |
| 66 this.clearRefreshToken_(); | 66 this.clearRefreshToken_(); |
| 67 }; | 67 }; |
| 68 | 68 |
| 69 /** | 69 /** |
| 70 * Sets the refresh token. | 70 * Sets the refresh token. |
| 71 * | 71 * |
| 72 * This method also marks the token as revokable, so that this object will | 72 * This method also marks the token as revokable, so that this object will |
| 73 * revoke the token when it no longer needs it. | 73 * revoke the token when it no longer needs it. |
| 74 * | 74 * |
| 75 * @param {string} token The new refresh token. | 75 * @param {string} token The new refresh token. |
| 76 * @return {void} Nothing. | 76 * @return {void} Nothing. |
| 77 */ | 77 */ |
| 78 remoting.OAuth2.prototype.setRefreshToken = function(token) { | 78 remoting.OAuth2.prototype.setRefreshToken = function(token) { |
| 79 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); | 79 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); |
| 80 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_REVOKABLE_, true); | 80 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_REVOKABLE_, true); |
| 81 this.clearAccessToken(); | 81 this.clearAccessToken_(); |
| 82 }; | 82 }; |
| 83 | 83 |
| 84 /** | 84 /** |
| 85 * Gets the refresh token. | 85 * Gets the refresh token. |
| 86 * | 86 * |
| 87 * This method also marks the refresh token as not revokable, so that this | 87 * This method also marks the refresh token as not revokable, so that this |
| 88 * object will not revoke the token when it no longer needs it. After this | 88 * object will not revoke the token when it no longer needs it. After this |
| 89 * object has exported the token, it cannot know whether it is still in use | 89 * object has exported the token, it cannot know whether it is still in use |
| 90 * when this object no longer needs it. | 90 * when this object no longer needs it. |
| 91 * | 91 * |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 156 console.log('Invalid access token stored.'); | 156 console.log('Invalid access token stored.'); |
| 157 return {'token': '', 'expiration': 0}; | 157 return {'token': '', 'expiration': 0}; |
| 158 }; | 158 }; |
| 159 | 159 |
| 160 /** | 160 /** |
| 161 * Returns true if the access token is expired, or otherwise invalid. | 161 * Returns true if the access token is expired, or otherwise invalid. |
| 162 * | 162 * |
| 163 * Will throw if !isAuthenticated(). | 163 * Will throw if !isAuthenticated(). |
| 164 * | 164 * |
| 165 * @return {boolean} True if a new access token is needed. | 165 * @return {boolean} True if a new access token is needed. |
| 166 * @private | |
| 166 */ | 167 */ |
| 167 remoting.OAuth2.prototype.needsNewAccessToken = function() { | 168 remoting.OAuth2.prototype.needsNewAccessToken_ = function() { |
| 168 if (!this.isAuthenticated()) { | 169 if (!this.isAuthenticated()) { |
| 169 throw 'Not Authenticated.'; | 170 throw 'Not Authenticated.'; |
| 170 } | 171 } |
| 171 var access_token = this.getAccessTokenInternal_(); | 172 var access_token = this.getAccessTokenInternal_(); |
| 172 if (!access_token['token']) { | 173 if (!access_token['token']) { |
| 173 return true; | 174 return true; |
| 174 } | 175 } |
| 175 if (Date.now() > access_token['expiration']) { | 176 if (Date.now() > access_token['expiration']) { |
| 176 return true; | 177 return true; |
| 177 } | 178 } |
| 178 return false; | 179 return false; |
| 179 }; | 180 }; |
| 180 | 181 |
| 181 /** | 182 /** |
| 182 * Returns the current access token. | 183 * @return {void} Nothing. |
| 183 * | |
| 184 * Will throw if !isAuthenticated() or needsNewAccessToken(). | |
| 185 * | |
| 186 * @return {string} The access token. | |
| 187 * @private | 184 * @private |
| 188 */ | 185 */ |
| 189 remoting.OAuth2.prototype.getAccessToken_ = function() { | 186 remoting.OAuth2.prototype.clearAccessToken_ = function() { |
| 190 if (this.needsNewAccessToken()) { | |
| 191 throw 'Access Token expired.'; | |
| 192 } | |
| 193 return this.getAccessTokenInternal_()['token']; | |
| 194 }; | |
| 195 | |
| 196 /** @return {void} Nothing. */ | |
| 197 remoting.OAuth2.prototype.clearAccessToken = function() { | |
| 198 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); | 187 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); |
| 199 }; | 188 }; |
| 200 | 189 |
| 201 /** | 190 /** |
| 202 * Update state based on token response from the OAuth2 /token endpoint. | 191 * Update state based on token response from the OAuth2 /token endpoint. |
| 203 * | 192 * |
| 204 * @private | 193 * @private |
| 205 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on | 194 * @param {function(XMLHttpRequest, string): void} onDone Callback to invoke on |
|
simonmorris
2012/07/17 01:25:33
Doesn't "string" need to be something like "string
Jamie
2012/07/17 01:51:58
I've changed the default to '', since that minimiz
| |
| 206 * completion. | 195 * completion. |
| 207 * @param {XMLHttpRequest} xhr The XHR object for this request. | 196 * @param {XMLHttpRequest} xhr The XHR object for this request. |
| 208 * @return {void} Nothing. | 197 * @return {void} Nothing. |
| 209 */ | 198 */ |
| 210 remoting.OAuth2.prototype.processTokenResponse_ = function(onDone, xhr) { | 199 remoting.OAuth2.prototype.processTokenResponse_ = function(onDone, xhr) { |
| 200 var accessToken = null; | |
| 211 if (xhr.status == 200) { | 201 if (xhr.status == 200) { |
| 212 try { | 202 try { |
| 213 // Don't use jsonParseSafe here unless you move the definition out of | 203 // Don't use jsonParseSafe here unless you move the definition out of |
| 214 // remoting.js, otherwise this won't work from the OAuth trampoline. | 204 // remoting.js, otherwise this won't work from the OAuth trampoline. |
| 215 // TODO(jamiewalch): Fix this once we're no longer using the trampoline. | 205 // TODO(jamiewalch): Fix this once we're no longer using the trampoline. |
| 216 var tokens = JSON.parse(xhr.responseText); | 206 var tokens = JSON.parse(xhr.responseText); |
| 217 if ('refresh_token' in tokens) { | 207 if ('refresh_token' in tokens) { |
| 218 this.setRefreshToken(tokens['refresh_token']); | 208 this.setRefreshToken(tokens['refresh_token']); |
| 219 } | 209 } |
| 220 | 210 |
| 221 // Offset by 120 seconds so that we can guarantee that the token | 211 // Offset by 120 seconds so that we can guarantee that the token |
| 222 // we return will be valid for at least 2 minutes. | 212 // we return will be valid for at least 2 minutes. |
| 223 // If the access token is to be useful, this object must make some | 213 // If the access token is to be useful, this object must make some |
| 224 // guarantee as to how long the token will be valid for. | 214 // guarantee as to how long the token will be valid for. |
| 225 // The choice of 2 minutes is arbitrary, but that length of time | 215 // The choice of 2 minutes is arbitrary, but that length of time |
| 226 // is part of the contract satisfied by callWithToken(). | 216 // is part of the contract satisfied by callWithToken(). |
| 227 // Offset by a further 30 seconds to account for RTT issues. | 217 // Offset by a further 30 seconds to account for RTT issues. |
| 228 this.setAccessToken(tokens['access_token'], | 218 accessToken = tokens['access_token']; |
| 219 this.setAccessToken(accessToken, | |
| 229 (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); | 220 (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); |
| 230 } catch (err) { | 221 } catch (err) { |
| 231 console.error('Invalid "token" response from server:', | 222 console.error('Invalid "token" response from server:', |
| 232 /** @type {*} */ (err)); | 223 /** @type {*} */ (err)); |
| 233 } | 224 } |
| 234 } else { | 225 } else { |
| 235 console.error('Failed to get tokens. Status: ' + xhr.status + | 226 console.error('Failed to get tokens. Status: ' + xhr.status + |
| 236 ' response: ' + xhr.responseText); | 227 ' response: ' + xhr.responseText); |
| 237 } | 228 } |
| 238 onDone(xhr); | 229 onDone(xhr, accessToken); |
| 239 }; | 230 }; |
| 240 | 231 |
| 241 /** | 232 /** |
| 242 * Asynchronously retrieves a new access token from the server. | 233 * Asynchronously retrieves a new access token from the server. |
| 243 * | 234 * |
| 244 * Will throw if !isAuthenticated(). | 235 * Will throw if !isAuthenticated(). |
| 245 * | 236 * |
| 246 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on | 237 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on |
| 247 * completion. | 238 * completion. |
| 248 * @return {void} Nothing. | 239 * @return {void} Nothing. |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 323 console.log('Failed to revoke token. Status: ' + xhr.status + | 314 console.log('Failed to revoke token. Status: ' + xhr.status + |
| 324 ' ; response: ' + xhr.responseText + ' ; xhr: ', xhr); | 315 ' ; response: ' + xhr.responseText + ' ; xhr: ', xhr); |
| 325 } | 316 } |
| 326 }; | 317 }; |
| 327 remoting.xhr.post(this.OAUTH2_REVOKE_TOKEN_ENDPOINT_, | 318 remoting.xhr.post(this.OAUTH2_REVOKE_TOKEN_ENDPOINT_, |
| 328 processResponse, | 319 processResponse, |
| 329 parameters); | 320 parameters); |
| 330 }; | 321 }; |
| 331 | 322 |
| 332 /** | 323 /** |
| 333 * Call myfunc with an access token as the only parameter. | 324 * Call a function with an access token, refreshing it first if necessary. |
| 334 * | |
| 335 * This will refresh the access token if necessary. If the access token | |
| 336 * cannot be refreshed, an error is thrown. | |
| 337 * | |
| 338 * The access token will remain valid for at least 2 minutes. | 325 * The access token will remain valid for at least 2 minutes. |
| 339 * | 326 * |
| 340 * @param {function(string):void} onOk Function to invoke with access token if | 327 * @param {function(string):void} onOk Function to invoke with access token if |
| 341 * an access token was successfully retrieved. | 328 * an access token was successfully retrieved. |
| 342 * @param {function(remoting.Error):void} onError Function to invoke with an | 329 * @param {function(remoting.Error):void} onError Function to invoke with an |
| 343 * error code on failure. | 330 * error code on failure. |
| 344 * @return {void} Nothing. | 331 * @return {void} Nothing. |
| 345 */ | 332 */ |
| 346 remoting.OAuth2.prototype.callWithToken = function(onOk, onError) { | 333 remoting.OAuth2.prototype.callWithToken = function(onOk, onError) { |
| 347 try { | 334 if (this.isAuthenticated()) { |
| 348 if (this.needsNewAccessToken()) { | 335 if (this.needsNewAccessToken_()) { |
| 349 this.refreshAccessToken_(this.onRefreshToken_.bind(this, onOk, onError)); | 336 this.refreshAccessToken_(this.onRefreshToken_.bind(this, onOk, onError)); |
| 350 } else { | 337 } else { |
| 351 onOk(this.getAccessToken_()); | 338 onOk(this.getAccessTokenInternal_()['token']); |
| 352 } | 339 } |
| 353 } catch (error) { | 340 } else { |
| 354 onError(remoting.Error.NOT_AUTHENTICATED); | 341 onError(remoting.Error.NOT_AUTHENTICATED); |
| 355 } | 342 } |
| 356 }; | 343 }; |
| 357 | 344 |
| 358 /** | 345 /** |
| 359 * Process token refresh results and notify caller. | 346 * Process token refresh results and notify caller. |
| 360 * | 347 * |
| 361 * @param {function(string):void} onOk Function to invoke with access token if | 348 * @param {function(string):void} onOk Function to invoke with access token if |
| 362 * an access token was successfully retrieved. | 349 * an access token was successfully retrieved. |
| 363 * @param {function(remoting.Error):void} onError Function to invoke with an | 350 * @param {function(remoting.Error):void} onError Function to invoke with an |
| 364 * error code on failure. | 351 * error code on failure. |
| 365 * @param {XMLHttpRequest} xhr The result of the refresh operation. | 352 * @param {XMLHttpRequest} xhr The result of the refresh operation. |
| 353 * @param {string} accessToken The fresh access token. | |
| 366 * @private | 354 * @private |
| 367 */ | 355 */ |
| 368 remoting.OAuth2.prototype.onRefreshToken_ = function(onOk, onError, xhr) { | 356 remoting.OAuth2.prototype.onRefreshToken_ = function(onOk, onError, xhr, |
| 357 accessToken) { | |
| 369 var error = remoting.Error.UNEXPECTED; | 358 var error = remoting.Error.UNEXPECTED; |
| 370 if (xhr.status == 200) { | 359 if (xhr.status == 200) { |
| 371 onOk(this.getAccessToken_()); | 360 onOk(accessToken); |
| 372 return; | 361 return; |
| 373 } else if (xhr.status == 400) { | 362 } else if (xhr.status == 400) { |
| 374 var result = | 363 var result = |
| 375 /** @type {{error: string}} */ (jsonParseSafe(xhr.responseText)); | 364 /** @type {{error: string}} */ (jsonParseSafe(xhr.responseText)); |
| 376 if (result && result.error == 'invalid_grant') { | 365 if (result && result.error == 'invalid_grant') { |
| 377 error = remoting.Error.AUTHENTICATION_FAILED; | 366 error = remoting.Error.AUTHENTICATION_FAILED; |
| 378 } | 367 } |
| 379 } else if (xhr.status == 401) { | 368 } else if (xhr.status == 401) { |
| 380 // According to the OAuth2 draft RFC, the server shouldn't return 401, | 369 // According to the OAuth2 draft RFC, the server shouldn't return 401, |
| 381 // but AUTHENTICATION_FAILED is the obvious interpretation if it does. | 370 // but AUTHENTICATION_FAILED is the obvious interpretation if it does. |
| (...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 433 * @return {?string} The email address, if it has been cached by a previous call | 422 * @return {?string} The email address, if it has been cached by a previous call |
| 434 * to getEmail, otherwise null. | 423 * to getEmail, otherwise null. |
| 435 */ | 424 */ |
| 436 remoting.OAuth2.prototype.getCachedEmail = function() { | 425 remoting.OAuth2.prototype.getCachedEmail = function() { |
| 437 var value = window.localStorage.getItem(this.KEY_EMAIL_); | 426 var value = window.localStorage.getItem(this.KEY_EMAIL_); |
| 438 if (typeof value == 'string') { | 427 if (typeof value == 'string') { |
| 439 return value; | 428 return value; |
| 440 } | 429 } |
| 441 return null; | 430 return null; |
| 442 }; | 431 }; |
| OLD | NEW |