| OLD | NEW |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 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 | 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 |
| 11 * chrome-extensions in OAuth2. | 11 * chrome-extensions in OAuth2. |
| 12 */ | 12 */ |
| 13 | 13 |
| 14 'use strict'; | 14 'use strict'; |
| 15 | 15 |
| 16 /** @suppress {duplicate} */ | 16 /** @suppress {duplicate} */ |
| 17 var remoting = remoting || {}; | 17 var remoting = remoting || {}; |
| 18 | 18 |
| 19 (function() { | 19 /** @type {remoting.OAuth2} */ |
| 20 remoting.oauth2 = null; |
| 21 |
| 22 |
| 20 /** @constructor */ | 23 /** @constructor */ |
| 21 remoting.OAuth2 = function() { | 24 remoting.OAuth2 = function() { |
| 22 } | 25 }; |
| 23 | 26 |
| 24 // Constants representing keys used for storing persistent state. | 27 // Constants representing keys used for storing persistent state. |
| 28 /** @private */ |
| 25 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; | 29 remoting.OAuth2.prototype.KEY_REFRESH_TOKEN_ = 'oauth2-refresh-token'; |
| 30 /** @private */ |
| 26 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; | 31 remoting.OAuth2.prototype.KEY_ACCESS_TOKEN_ = 'oauth2-access-token'; |
| 27 | 32 |
| 28 // Constants for parameters used in retrieving the OAuth2 credentials. | 33 // Constants for parameters used in retrieving the OAuth2 credentials. |
| 29 remoting.OAuth2.prototype.CLIENT_ID_ = | 34 /** @private */ remoting.OAuth2.prototype.CLIENT_ID_ = |
| 30 '440925447803-2pi3v45bff6tp1rde2f7q6lgbor3o5uj.' + | 35 '440925447803-2pi3v45bff6tp1rde2f7q6lgbor3o5uj.' + |
| 31 'apps.googleusercontent.com'; | 36 'apps.googleusercontent.com'; |
| 37 /** @private */ |
| 32 remoting.OAuth2.prototype.CLIENT_SECRET_ = 'W2ieEsG-R1gIA4MMurGrgMc_'; | 38 remoting.OAuth2.prototype.CLIENT_SECRET_ = 'W2ieEsG-R1gIA4MMurGrgMc_'; |
| 33 remoting.OAuth2.prototype.SCOPE_ = | 39 /** @private */ remoting.OAuth2.prototype.SCOPE_ = |
| 34 'https://www.googleapis.com/auth/chromoting ' + | 40 'https://www.googleapis.com/auth/chromoting ' + |
| 35 'https://www.googleapis.com/auth/googletalk ' + | 41 'https://www.googleapis.com/auth/googletalk ' + |
| 36 'https://www.googleapis.com/auth/userinfo#email'; | 42 'https://www.googleapis.com/auth/userinfo#email'; |
| 37 remoting.OAuth2.prototype.REDIRECT_URI_ = | 43 /** @private */ remoting.OAuth2.prototype.REDIRECT_URI_ = |
| 38 'https://talkgadget.google.com/talkgadget/blank'; | 44 'https://talkgadget.google.com/talkgadget/blank'; |
| 39 remoting.OAuth2.prototype.OAUTH2_TOKEN_ENDPOINT_ = | 45 /** @private */ remoting.OAuth2.prototype.OAUTH2_TOKEN_ENDPOINT_ = |
| 40 'https://accounts.google.com/o/oauth2/token'; | 46 'https://accounts.google.com/o/oauth2/token'; |
| 41 | 47 |
| 42 /** @return {boolean} True if the app is already authenticated. */ | 48 /** @return {boolean} True if the app is already authenticated. */ |
| 43 remoting.OAuth2.prototype.isAuthenticated = function() { | 49 remoting.OAuth2.prototype.isAuthenticated = function() { |
| 44 if (this.getRefreshToken()) { | 50 if (this.getRefreshToken()) { |
| 45 return true; | 51 return true; |
| 46 } | 52 } |
| 47 return false; | 53 return false; |
| 48 } | 54 }; |
| 49 | 55 |
| 50 /** | 56 /** |
| 51 * Removes all storage, and effectively unauthenticates the user. | 57 * Removes all storage, and effectively unauthenticates the user. |
| 52 * | 58 * |
| 53 * @return {void} Nothing. | 59 * @return {void} Nothing. |
| 54 */ | 60 */ |
| 55 remoting.OAuth2.prototype.clear = function() { | 61 remoting.OAuth2.prototype.clear = function() { |
| 56 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); | 62 window.localStorage.removeItem(this.KEY_REFRESH_TOKEN_); |
| 57 this.clearAccessToken(); | 63 this.clearAccessToken(); |
| 58 } | 64 }; |
| 59 | 65 |
| 60 /** | 66 /** |
| 61 * @param {string} token The new refresh token. | 67 * @param {string} token The new refresh token. |
| 62 * @return {void} Nothing. | 68 * @return {void} Nothing. |
| 63 */ | 69 */ |
| 64 remoting.OAuth2.prototype.setRefreshToken = function(token) { | 70 remoting.OAuth2.prototype.setRefreshToken = function(token) { |
| 65 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); | 71 window.localStorage.setItem(this.KEY_REFRESH_TOKEN_, escape(token)); |
| 66 this.clearAccessToken(); | 72 this.clearAccessToken(); |
| 67 } | 73 }; |
| 68 | 74 |
| 69 /** @return {?string} The refresh token, if authenticated, or NULL. */ | 75 /** @return {?string} The refresh token, if authenticated, or NULL. */ |
| 70 remoting.OAuth2.prototype.getRefreshToken = function() { | 76 remoting.OAuth2.prototype.getRefreshToken = function() { |
| 71 var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_); | 77 var value = window.localStorage.getItem(this.KEY_REFRESH_TOKEN_); |
| 72 if (typeof value == 'string') { | 78 if (typeof value == 'string') { |
| 73 return unescape(value); | 79 return unescape(value); |
| 74 } | 80 } |
| 75 return null; | 81 return null; |
| 76 } | 82 }; |
| 77 | 83 |
| 78 /** | 84 /** |
| 79 * @param {string} token The new access token. | 85 * @param {string} token The new access token. |
| 80 * @param {number} expiration Expiration time in milliseconds since epoch. | 86 * @param {number} expiration Expiration time in milliseconds since epoch. |
| 81 * @return {void} Nothing. | 87 * @return {void} Nothing. |
| 82 */ | 88 */ |
| 83 remoting.OAuth2.prototype.setAccessToken = function(token, expiration) { | 89 remoting.OAuth2.prototype.setAccessToken = function(token, expiration) { |
| 84 var access_token = {'token': token, 'expiration': expiration}; | 90 var access_token = {'token': token, 'expiration': expiration}; |
| 85 window.localStorage.setItem(this.KEY_ACCESS_TOKEN_, | 91 window.localStorage.setItem(this.KEY_ACCESS_TOKEN_, |
| 86 JSON.stringify(access_token)); | 92 JSON.stringify(access_token)); |
| 87 } | 93 }; |
| 88 | 94 |
| 89 /** | 95 /** |
| 90 * Returns the current access token, setting it to a invalid value if none | 96 * Returns the current access token, setting it to a invalid value if none |
| 91 * existed before. | 97 * existed before. |
| 92 * | 98 * |
| 99 * @private |
| 93 * @return {{token: string, expiration: number}} The current access token, or | 100 * @return {{token: string, expiration: number}} The current access token, or |
| 94 * an invalid token if not authenticated. | 101 * an invalid token if not authenticated. |
| 95 */ | 102 */ |
| 96 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() { | 103 remoting.OAuth2.prototype.getAccessTokenInternal_ = function() { |
| 97 if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) { | 104 if (!window.localStorage.getItem(this.KEY_ACCESS_TOKEN_)) { |
| 98 // Always be able to return structured data. | 105 // Always be able to return structured data. |
| 99 this.setAccessToken('', 0); | 106 this.setAccessToken('', 0); |
| 100 } | 107 } |
| 101 var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_); | 108 var accessToken = window.localStorage.getItem(this.KEY_ACCESS_TOKEN_); |
| 102 if (typeof accessToken == 'string') { | 109 if (typeof accessToken == 'string') { |
| 103 var result = JSON.parse(accessToken); | 110 var result = JSON.parse(accessToken); |
| 104 if ('token' in result && 'expiration' in result) { | 111 if ('token' in result && 'expiration' in result) { |
| 105 return /** @type {{token: string, expiration: number}} */ result; | 112 return /** @type {{token: string, expiration: number}} */ result; |
| 106 } | 113 } |
| 107 } | 114 } |
| 108 console.log('Invalid access token stored.'); | 115 console.log('Invalid access token stored.'); |
| 109 return {'token': '', 'expiration': 0}; | 116 return {'token': '', 'expiration': 0}; |
| 110 } | 117 }; |
| 111 | 118 |
| 112 /** | 119 /** |
| 113 * Returns true if the access token is expired, or otherwise invalid. | 120 * Returns true if the access token is expired, or otherwise invalid. |
| 114 * | 121 * |
| 115 * Will throw if !isAuthenticated(). | 122 * Will throw if !isAuthenticated(). |
| 116 * | 123 * |
| 117 * @return {boolean} True if a new access token is needed. | 124 * @return {boolean} True if a new access token is needed. |
| 118 */ | 125 */ |
| 119 remoting.OAuth2.prototype.needsNewAccessToken = function() { | 126 remoting.OAuth2.prototype.needsNewAccessToken = function() { |
| 120 if (!this.isAuthenticated()) { | 127 if (!this.isAuthenticated()) { |
| 121 throw 'Not Authenticated.'; | 128 throw 'Not Authenticated.'; |
| 122 } | 129 } |
| 123 var access_token = this.getAccessTokenInternal_(); | 130 var access_token = this.getAccessTokenInternal_(); |
| 124 if (!access_token['token']) { | 131 if (!access_token['token']) { |
| 125 return true; | 132 return true; |
| 126 } | 133 } |
| 127 if (Date.now() > access_token['expiration']) { | 134 if (Date.now() > access_token['expiration']) { |
| 128 return true; | 135 return true; |
| 129 } | 136 } |
| 130 return false; | 137 return false; |
| 131 } | 138 }; |
| 132 | 139 |
| 133 /** | 140 /** |
| 134 * Returns the current access token. | 141 * Returns the current access token. |
| 135 * | 142 * |
| 136 * Will throw if !isAuthenticated() or needsNewAccessToken(). | 143 * Will throw if !isAuthenticated() or needsNewAccessToken(). |
| 137 * | 144 * |
| 138 * @return {{token: string, expiration: number}} | 145 * @return {string} The access token. |
| 139 */ | 146 */ |
| 140 remoting.OAuth2.prototype.getAccessToken = function() { | 147 remoting.OAuth2.prototype.getAccessToken = function() { |
| 141 if (this.needsNewAccessToken()) { | 148 if (this.needsNewAccessToken()) { |
| 142 throw 'Access Token expired.'; | 149 throw 'Access Token expired.'; |
| 143 } | 150 } |
| 144 return this.getAccessTokenInternal_()['token']; | 151 return this.getAccessTokenInternal_()['token']; |
| 145 } | 152 }; |
| 146 | 153 |
| 147 /** @return {void} Nothing. */ | 154 /** @return {void} Nothing. */ |
| 148 remoting.OAuth2.prototype.clearAccessToken = function() { | 155 remoting.OAuth2.prototype.clearAccessToken = function() { |
| 149 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); | 156 window.localStorage.removeItem(this.KEY_ACCESS_TOKEN_); |
| 150 } | 157 }; |
| 151 | 158 |
| 152 /** | 159 /** |
| 153 * Update state based on token response from the OAuth2 /token endpoint. | 160 * Update state based on token response from the OAuth2 /token endpoint. |
| 154 * | 161 * |
| 162 * @private |
| 155 * @param {XMLHttpRequest} xhr The XHR object for this request. | 163 * @param {XMLHttpRequest} xhr The XHR object for this request. |
| 156 * @return {void} Nothing. | 164 * @return {void} Nothing. |
| 157 */ | 165 */ |
| 158 remoting.OAuth2.prototype.processTokenResponse_ = function(xhr) { | 166 remoting.OAuth2.prototype.processTokenResponse_ = function(xhr) { |
| 159 if (xhr.status == 200) { | 167 if (xhr.status == 200) { |
| 160 var tokens = JSON.parse(xhr.responseText); | 168 var tokens = JSON.parse(xhr.responseText); |
| 161 if ('refresh_token' in tokens) { | 169 if ('refresh_token' in tokens) { |
| 162 this.setRefreshToken(tokens['refresh_token']); | 170 this.setRefreshToken(tokens['refresh_token']); |
| 163 } | 171 } |
| 164 | 172 |
| 165 // Offset by 120 seconds so that we can guarantee that the token | 173 // Offset by 120 seconds so that we can guarantee that the token |
| 166 // we return will be valid for at least 2 minutes. | 174 // we return will be valid for at least 2 minutes. |
| 167 // If the access token is to be useful, this object must make some | 175 // If the access token is to be useful, this object must make some |
| 168 // guarantee as to how long the token will be valid for. | 176 // guarantee as to how long the token will be valid for. |
| 169 // The choice of 2 minutes is arbitrary, but that length of time | 177 // The choice of 2 minutes is arbitrary, but that length of time |
| 170 // is part of the contract satisfied by callWithToken(). | 178 // is part of the contract satisfied by callWithToken(). |
| 171 // Offset by a further 30 seconds to account for RTT issues. | 179 // Offset by a further 30 seconds to account for RTT issues. |
| 172 this.setAccessToken(tokens['access_token'], | 180 this.setAccessToken(tokens['access_token'], |
| 173 (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); | 181 (tokens['expires_in'] - (120 + 30)) * 1000 + Date.now()); |
| 174 } else { | 182 } else { |
| 175 console.log('Failed to get tokens. Status: ' + xhr.status + | 183 console.log('Failed to get tokens. Status: ' + xhr.status + |
| 176 ' response: ' + xhr.responseText); | 184 ' response: ' + xhr.responseText); |
| 177 } | 185 } |
| 178 } | 186 }; |
| 179 | 187 |
| 180 /** | 188 /** |
| 181 * Asynchronously retrieves a new access token from the server. | 189 * Asynchronously retrieves a new access token from the server. |
| 182 * | 190 * |
| 183 * Will throw if !isAuthenticated(). | 191 * Will throw if !isAuthenticated(). |
| 184 * | 192 * |
| 185 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on | 193 * @param {function(XMLHttpRequest): void} onDone Callback to invoke on |
| 186 * completion. | 194 * completion. |
| 187 * @return {void} Nothing. | 195 * @return {void} Nothing. |
| 188 */ | 196 */ |
| 189 remoting.OAuth2.prototype.refreshAccessToken = function(onDone) { | 197 remoting.OAuth2.prototype.refreshAccessToken = function(onDone) { |
| 190 if (!this.isAuthenticated()) { | 198 if (!this.isAuthenticated()) { |
| 191 throw 'Not Authenticated.'; | 199 throw 'Not Authenticated.'; |
| 192 } | 200 } |
| 193 | 201 |
| 194 var parameters = { | 202 var parameters = { |
| 195 'client_id': this.CLIENT_ID_, | 203 'client_id': this.CLIENT_ID_, |
| 196 'client_secret': this.CLIENT_SECRET_, | 204 'client_secret': this.CLIENT_SECRET_, |
| 197 'refresh_token': this.getRefreshToken(), | 205 'refresh_token': this.getRefreshToken(), |
| 198 'grant_type': 'refresh_token' | 206 'grant_type': 'refresh_token' |
| 199 }; | 207 }; |
| 200 | 208 |
| 209 /** @type {remoting.OAuth2} */ |
| 201 var that = this; | 210 var that = this; |
| 211 /** @param {XMLHttpRequest} xhr The XHR reply. */ |
| 212 var processTokenResponse = function(xhr) { |
| 213 that.processTokenResponse_(xhr); |
| 214 onDone(xhr); |
| 215 }; |
| 202 remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, | 216 remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, |
| 203 function(xhr) { | 217 processTokenResponse, |
| 204 that.processTokenResponse_(xhr); | |
| 205 onDone(xhr); | |
| 206 }, | |
| 207 parameters); | 218 parameters); |
| 208 } | 219 }; |
| 209 | 220 |
| 210 /** | 221 /** |
| 211 * Redirect page to get a new OAuth2 Refresh Token. | 222 * Redirect page to get a new OAuth2 Refresh Token. |
| 212 * | 223 * |
| 213 * @return {void} Nothing. | 224 * @return {void} Nothing. |
| 214 */ | 225 */ |
| 215 remoting.OAuth2.prototype.doAuthRedirect = function() { | 226 remoting.OAuth2.prototype.doAuthRedirect = function() { |
| 216 var GET_CODE_URL = 'https://accounts.google.com/o/oauth2/auth?' + | 227 var GET_CODE_URL = 'https://accounts.google.com/o/oauth2/auth?' + |
| 217 remoting.xhr.urlencodeParamHash({ | 228 remoting.xhr.urlencodeParamHash({ |
| 218 'client_id': this.CLIENT_ID_, | 229 'client_id': this.CLIENT_ID_, |
| 219 'redirect_uri': this.REDIRECT_URI_, | 230 'redirect_uri': this.REDIRECT_URI_, |
| 220 'scope': this.SCOPE_, | 231 'scope': this.SCOPE_, |
| 221 'response_type': 'code' | 232 'response_type': 'code' |
| 222 }); | 233 }); |
| 223 window.location.replace(GET_CODE_URL); | 234 window.location.replace(GET_CODE_URL); |
| 224 } | 235 }; |
| 225 | 236 |
| 226 /** | 237 /** |
| 227 * Asynchronously exchanges an authorization code for a refresh token. | 238 * Asynchronously exchanges an authorization code for a refresh token. |
| 228 * | 239 * |
| 229 * @param {string} code The new refresh token. | 240 * @param {string} code The new refresh token. |
| 230 * @param {function(XMLHttpRequest):void} onDone Callback to invoke on | 241 * @param {function(XMLHttpRequest):void} onDone Callback to invoke on |
| 231 * completion. | 242 * completion. |
| 232 * @return {void} Nothing. | 243 * @return {void} Nothing. |
| 233 */ | 244 */ |
| 234 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) { | 245 remoting.OAuth2.prototype.exchangeCodeForToken = function(code, onDone) { |
| 235 var parameters = { | 246 var parameters = { |
| 236 'client_id': this.CLIENT_ID_, | 247 'client_id': this.CLIENT_ID_, |
| 237 'client_secret': this.CLIENT_SECRET_, | 248 'client_secret': this.CLIENT_SECRET_, |
| 238 'redirect_uri': this.REDIRECT_URI_, | 249 'redirect_uri': this.REDIRECT_URI_, |
| 239 'code': code, | 250 'code': code, |
| 240 'grant_type': 'authorization_code' | 251 'grant_type': 'authorization_code' |
| 241 }; | 252 }; |
| 242 | 253 |
| 254 /** @type {remoting.OAuth2} */ |
| 243 var that = this; | 255 var that = this; |
| 256 /** @param {XMLHttpRequest} xhr The XHR reply. */ |
| 257 var processTokenResponse = function(xhr) { |
| 258 that.processTokenResponse_(xhr); |
| 259 onDone(xhr); |
| 260 }; |
| 244 remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, | 261 remoting.xhr.post(this.OAUTH2_TOKEN_ENDPOINT_, |
| 245 function(xhr) { | 262 processTokenResponse, |
| 246 that.processTokenResponse_(xhr); | |
| 247 onDone(xhr); | |
| 248 }, | |
| 249 parameters); | 263 parameters); |
| 250 } | 264 }; |
| 251 | 265 |
| 252 /** | 266 /** |
| 253 * Call myfunc with an access token as the only parameter. | 267 * Call myfunc with an access token as the only parameter. |
| 254 * | 268 * |
| 255 * This will refresh the access token if necessary. If the access token | 269 * This will refresh the access token if necessary. If the access token |
| 256 * cannot be refreshed, an error is thrown. | 270 * cannot be refreshed, an error is thrown. |
| 257 * | 271 * |
| 258 * The access token will remain valid for at least 2 minutes. | 272 * The access token will remain valid for at least 2 minutes. |
| 259 * | 273 * |
| 260 * @param {function({token: string, expiration: number}):void} myfunc | 274 * @param {function(string):void} myfunc |
| 261 * Function to invoke with access token. | 275 * Function to invoke with access token. |
| 262 * @return {void} Nothing. | 276 * @return {void} Nothing. |
| 263 */ | 277 */ |
| 264 remoting.OAuth2.prototype.callWithToken = function(myfunc) { | 278 remoting.OAuth2.prototype.callWithToken = function(myfunc) { |
| 279 /** @type {remoting.OAuth2} */ |
| 265 var that = this; | 280 var that = this; |
| 266 if (remoting.oauth2.needsNewAccessToken()) { | 281 if (remoting.oauth2.needsNewAccessToken()) { |
| 267 remoting.oauth2.refreshAccessToken(function() { | 282 remoting.oauth2.refreshAccessToken(function() { |
| 268 if (remoting.oauth2.needsNewAccessToken()) { | 283 if (remoting.oauth2.needsNewAccessToken()) { |
| 269 // If we still need it, we're going to infinite loop. | 284 // If we still need it, we're going to infinite loop. |
| 270 throw 'Unable to get access token.'; | 285 throw 'Unable to get access token.'; |
| 271 } | 286 } |
| 272 myfunc(that.getAccessToken()); | 287 myfunc(that.getAccessToken()); |
| 273 }); | 288 }); |
| 274 return; | 289 return; |
| 275 } | 290 } |
| 276 | 291 |
| 277 myfunc(this.getAccessToken()); | 292 myfunc(this.getAccessToken()); |
| 278 } | 293 }; |
| 279 }()); | |
| OLD | NEW |