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 |
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 /** @type {string} */ |
| 201 var accessToken = ''; |
211 if (xhr.status == 200) { | 202 if (xhr.status == 200) { |
212 try { | 203 try { |
213 // Don't use jsonParseSafe here unless you move the definition out of | 204 // Don't use jsonParseSafe here unless you move the definition out of |
214 // remoting.js, otherwise this won't work from the OAuth trampoline. | 205 // remoting.js, otherwise this won't work from the OAuth trampoline. |
215 // TODO(jamiewalch): Fix this once we're no longer using the trampoline. | 206 // TODO(jamiewalch): Fix this once we're no longer using the trampoline. |
216 var tokens = JSON.parse(xhr.responseText); | 207 var tokens = JSON.parse(xhr.responseText); |
217 if ('refresh_token' in tokens) { | 208 if ('refresh_token' in tokens) { |
218 this.setRefreshToken(tokens['refresh_token']); | 209 this.setRefreshToken(tokens['refresh_token']); |
219 } | 210 } |
220 | 211 |
221 // Offset by 120 seconds so that we can guarantee that the token | 212 // Offset by 120 seconds so that we can guarantee that the token |
222 // we return will be valid for at least 2 minutes. | 213 // we return will be valid for at least 2 minutes. |
223 // If the access token is to be useful, this object must make some | 214 // 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. | 215 // guarantee as to how long the token will be valid for. |
225 // The choice of 2 minutes is arbitrary, but that length of time | 216 // The choice of 2 minutes is arbitrary, but that length of time |
226 // is part of the contract satisfied by callWithToken(). | 217 // is part of the contract satisfied by callWithToken(). |
227 // Offset by a further 30 seconds to account for RTT issues. | 218 // Offset by a further 30 seconds to account for RTT issues. |
228 this.setAccessToken(tokens['access_token'], | 219 accessToken = /** @type {string} */ (tokens['access_token']); |
| 220 this.setAccessToken(accessToken, |
229 (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); | 221 (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); |
230 } catch (err) { | 222 } catch (err) { |
231 console.error('Invalid "token" response from server:', | 223 console.error('Invalid "token" response from server:', |
232 /** @type {*} */ (err)); | 224 /** @type {*} */ (err)); |
233 } | 225 } |
234 } else { | 226 } else { |
235 console.error('Failed to get tokens. Status: ' + xhr.status + | 227 console.error('Failed to get tokens. Status: ' + xhr.status + |
236 ' response: ' + xhr.responseText); | 228 ' response: ' + xhr.responseText); |
237 } | 229 } |
238 onDone(xhr); | 230 onDone(xhr, accessToken); |
239 }; | 231 }; |
240 | 232 |
241 /** | 233 /** |
242 * Asynchronously retrieves a new access token from the server. | 234 * Asynchronously retrieves a new access token from the server. |
243 * | 235 * |
244 * Will throw if !isAuthenticated(). | 236 * Will throw if !isAuthenticated(). |
245 * | 237 * |
246 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on | 238 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on |
247 * completion. | 239 * completion. |
248 * @return {void} Nothing. | 240 * @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 + | 315 console.log('Failed to revoke token. Status: ' + xhr.status + |
324 ' ; response: ' + xhr.responseText + ' ; xhr: ', xhr); | 316 ' ; response: ' + xhr.responseText + ' ; xhr: ', xhr); |
325 } | 317 } |
326 }; | 318 }; |
327 remoting.xhr.post(this.OAUTH2_REVOKE_TOKEN_ENDPOINT_, | 319 remoting.xhr.post(this.OAUTH2_REVOKE_TOKEN_ENDPOINT_, |
328 processResponse, | 320 processResponse, |
329 parameters); | 321 parameters); |
330 }; | 322 }; |
331 | 323 |
332 /** | 324 /** |
333 * Call myfunc with an access token as the only parameter. | 325 * 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. | 326 * The access token will remain valid for at least 2 minutes. |
339 * | 327 * |
340 * @param {function(string):void} onOk Function to invoke with access token if | 328 * @param {function(string):void} onOk Function to invoke with access token if |
341 * an access token was successfully retrieved. | 329 * an access token was successfully retrieved. |
342 * @param {function(remoting.Error):void} onError Function to invoke with an | 330 * @param {function(remoting.Error):void} onError Function to invoke with an |
343 * error code on failure. | 331 * error code on failure. |
344 * @return {void} Nothing. | 332 * @return {void} Nothing. |
345 */ | 333 */ |
346 remoting.OAuth2.prototype.callWithToken = function(onOk, onError) { | 334 remoting.OAuth2.prototype.callWithToken = function(onOk, onError) { |
347 try { | 335 if (this.isAuthenticated()) { |
348 if (this.needsNewAccessToken()) { | 336 if (this.needsNewAccessToken_()) { |
349 this.refreshAccessToken_(this.onRefreshToken_.bind(this, onOk, onError)); | 337 this.refreshAccessToken_(this.onRefreshToken_.bind(this, onOk, onError)); |
350 } else { | 338 } else { |
351 onOk(this.getAccessToken_()); | 339 onOk(this.getAccessTokenInternal_()['token']); |
352 } | 340 } |
353 } catch (error) { | 341 } else { |
354 onError(remoting.Error.NOT_AUTHENTICATED); | 342 onError(remoting.Error.NOT_AUTHENTICATED); |
355 } | 343 } |
356 }; | 344 }; |
357 | 345 |
358 /** | 346 /** |
359 * Process token refresh results and notify caller. | 347 * Process token refresh results and notify caller. |
360 * | 348 * |
361 * @param {function(string):void} onOk Function to invoke with access token if | 349 * @param {function(string):void} onOk Function to invoke with access token if |
362 * an access token was successfully retrieved. | 350 * an access token was successfully retrieved. |
363 * @param {function(remoting.Error):void} onError Function to invoke with an | 351 * @param {function(remoting.Error):void} onError Function to invoke with an |
364 * error code on failure. | 352 * error code on failure. |
365 * @param {XMLHttpRequest} xhr The result of the refresh operation. | 353 * @param {XMLHttpRequest} xhr The result of the refresh operation. |
| 354 * @param {string} accessToken The fresh access token. |
366 * @private | 355 * @private |
367 */ | 356 */ |
368 remoting.OAuth2.prototype.onRefreshToken_ = function(onOk, onError, xhr) { | 357 remoting.OAuth2.prototype.onRefreshToken_ = function(onOk, onError, xhr, |
| 358 accessToken) { |
369 var error = remoting.Error.UNEXPECTED; | 359 var error = remoting.Error.UNEXPECTED; |
370 if (xhr.status == 200) { | 360 if (xhr.status == 200) { |
371 onOk(this.getAccessToken_()); | 361 onOk(accessToken); |
372 return; | 362 return; |
373 } else if (xhr.status == 400) { | 363 } else if (xhr.status == 400) { |
374 var result = | 364 var result = |
375 /** @type {{error: string}} */ (jsonParseSafe(xhr.responseText)); | 365 /** @type {{error: string}} */ (jsonParseSafe(xhr.responseText)); |
376 if (result && result.error == 'invalid_grant') { | 366 if (result && result.error == 'invalid_grant') { |
377 error = remoting.Error.AUTHENTICATION_FAILED; | 367 error = remoting.Error.AUTHENTICATION_FAILED; |
378 } | 368 } |
379 } else if (xhr.status == 401) { | 369 } else if (xhr.status == 401) { |
380 // According to the OAuth2 draft RFC, the server shouldn't return 401, | 370 // According to the OAuth2 draft RFC, the server shouldn't return 401, |
381 // but AUTHENTICATION_FAILED is the obvious interpretation if it does. | 371 // 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 | 423 * @return {?string} The email address, if it has been cached by a previous call |
434 * to getEmail, otherwise null. | 424 * to getEmail, otherwise null. |
435 */ | 425 */ |
436 remoting.OAuth2.prototype.getCachedEmail = function() { | 426 remoting.OAuth2.prototype.getCachedEmail = function() { |
437 var value = window.localStorage.getItem(this.KEY_EMAIL_); | 427 var value = window.localStorage.getItem(this.KEY_EMAIL_); |
438 if (typeof value == 'string') { | 428 if (typeof value == 'string') { |
439 return value; | 429 return value; |
440 } | 430 } |
441 return null; | 431 return null; |
442 }; | 432 }; |
OLD | NEW |