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 |