| OLD | NEW |
| (Empty) |
| 1 // Copyright 2013 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 * A background script of the auth extension that bridges the communication | |
| 8 * between the main and injected scripts. | |
| 9 * | |
| 10 * Here is an overview of the communication flow when SAML is being used: | |
| 11 * 1. The main script sends the |startAuth| signal to this background script, | |
| 12 * indicating that the authentication flow has started and SAML pages may be | |
| 13 * loaded from now on. | |
| 14 * 2. A script is injected into each SAML page. The injected script sends three | |
| 15 * main types of messages to this background script: | |
| 16 * a) A |pageLoaded| message is sent when the page has been loaded. This is | |
| 17 * forwarded to the main script as |onAuthPageLoaded|. | |
| 18 * b) If the SAML provider supports the credential passing API, the API calls | |
| 19 * are sent to this background script as |apiCall| messages. These | |
| 20 * messages are forwarded unmodified to the main script. | |
| 21 * c) The injected script scrapes passwords. They are sent to this background | |
| 22 * script in |updatePassword| messages. The main script can request a list | |
| 23 * of the scraped passwords by sending the |getScrapedPasswords| message. | |
| 24 */ | |
| 25 | |
| 26 /** | |
| 27 * BackgroundBridgeManager maintains an array of BackgroundBridge, indexed by | |
| 28 * the associated tab id. | |
| 29 */ | |
| 30 function BackgroundBridgeManager() { | |
| 31 this.bridges_ = {}; | |
| 32 } | |
| 33 | |
| 34 BackgroundBridgeManager.prototype = { | |
| 35 CONTINUE_URL_BASE: 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik' + | |
| 36 '/success.html', | |
| 37 // Maps a tab id to its associated BackgroundBridge. | |
| 38 bridges_: null, | |
| 39 | |
| 40 run: function() { | |
| 41 chrome.runtime.onConnect.addListener(this.onConnect_.bind(this)); | |
| 42 | |
| 43 chrome.webRequest.onBeforeRequest.addListener( | |
| 44 function(details) { | |
| 45 if (this.bridges_[details.tabId]) | |
| 46 return this.bridges_[details.tabId].onInsecureRequest(details.url); | |
| 47 }.bind(this), | |
| 48 {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']}, | |
| 49 ['blocking']); | |
| 50 | |
| 51 chrome.webRequest.onBeforeSendHeaders.addListener( | |
| 52 function(details) { | |
| 53 if (this.bridges_[details.tabId]) | |
| 54 return this.bridges_[details.tabId].onBeforeSendHeaders(details); | |
| 55 else | |
| 56 return {requestHeaders: details.requestHeaders}; | |
| 57 }.bind(this), | |
| 58 {urls: ['*://*/*'], types: ['sub_frame']}, | |
| 59 ['blocking', 'requestHeaders']); | |
| 60 | |
| 61 chrome.webRequest.onHeadersReceived.addListener( | |
| 62 function(details) { | |
| 63 if (this.bridges_[details.tabId]) | |
| 64 return this.bridges_[details.tabId].onHeadersReceived(details); | |
| 65 }.bind(this), | |
| 66 {urls: ['*://*/*'], types: ['sub_frame']}, | |
| 67 ['blocking', 'responseHeaders']); | |
| 68 | |
| 69 chrome.webRequest.onCompleted.addListener( | |
| 70 function(details) { | |
| 71 if (this.bridges_[details.tabId]) | |
| 72 this.bridges_[details.tabId].onCompleted(details); | |
| 73 }.bind(this), | |
| 74 {urls: ['*://*/*', this.CONTINUE_URL_BASE + '*'], types: ['sub_frame']}, | |
| 75 ['responseHeaders']); | |
| 76 }, | |
| 77 | |
| 78 onConnect_: function(port) { | |
| 79 var tabId = this.getTabIdFromPort_(port); | |
| 80 if (!this.bridges_[tabId]) | |
| 81 this.bridges_[tabId] = new BackgroundBridge(tabId); | |
| 82 if (port.name == 'authMain') { | |
| 83 this.bridges_[tabId].setupForAuthMain(port); | |
| 84 port.onDisconnect.addListener(function() { | |
| 85 delete this.bridges_[tabId]; | |
| 86 }.bind(this)); | |
| 87 } else if (port.name == 'injected') { | |
| 88 this.bridges_[tabId].setupForInjected(port); | |
| 89 } else { | |
| 90 console.error('Unexpected connection, port.name=' + port.name); | |
| 91 } | |
| 92 }, | |
| 93 | |
| 94 getTabIdFromPort_: function(port) { | |
| 95 return port.sender.tab ? port.sender.tab.id : -1; | |
| 96 } | |
| 97 }; | |
| 98 | |
| 99 /** | |
| 100 * BackgroundBridge allows the main script and the injected script to | |
| 101 * collaborate. It forwards credentials API calls to the main script and | |
| 102 * maintains a list of scraped passwords. | |
| 103 * @param {string} tabId The associated tab ID. | |
| 104 */ | |
| 105 function BackgroundBridge(tabId) { | |
| 106 this.tabId_ = tabId; | |
| 107 this.passwordStore_ = {}; | |
| 108 } | |
| 109 | |
| 110 BackgroundBridge.prototype = { | |
| 111 // The associated tab ID. Only used for debugging now. | |
| 112 tabId: null, | |
| 113 | |
| 114 // The initial URL loaded in the gaia iframe. We only want to handle | |
| 115 // onCompleted() for the frame that loaded this URL. | |
| 116 initialFrameUrlWithoutParams: null, | |
| 117 | |
| 118 // On process onCompleted() requests that come from this frame Id. | |
| 119 frameId: -1, | |
| 120 | |
| 121 isDesktopFlow_: false, | |
| 122 | |
| 123 // Whether the extension is loaded in a constrained window. | |
| 124 // Set from main auth script. | |
| 125 isConstrainedWindow_: null, | |
| 126 | |
| 127 // Email of the newly authenticated user based on the gaia response header | |
| 128 // 'google-accounts-signin'. | |
| 129 email_: null, | |
| 130 | |
| 131 // Gaia Id of the newly authenticated user based on the gaia response | |
| 132 // header 'google-accounts-signin'. | |
| 133 gaiaId_: null, | |
| 134 | |
| 135 // Session index of the newly authenticated user based on the gaia response | |
| 136 // header 'google-accounts-signin'. | |
| 137 sessionIndex_: null, | |
| 138 | |
| 139 // Gaia URL base that is set from main auth script. | |
| 140 gaiaUrl_: null, | |
| 141 | |
| 142 // Whether to abort the authentication flow and show an error messagen when | |
| 143 // content served over an unencrypted connection is detected. | |
| 144 blockInsecureContent_: false, | |
| 145 | |
| 146 // Whether auth flow has started. It is used as a signal of whether the | |
| 147 // injected script should scrape passwords. | |
| 148 authStarted_: false, | |
| 149 | |
| 150 // Whether SAML flow is going. | |
| 151 isSAML_: false, | |
| 152 | |
| 153 passwordStore_: null, | |
| 154 | |
| 155 channelMain_: null, | |
| 156 channelInjected_: null, | |
| 157 | |
| 158 /** | |
| 159 * Sets up the communication channel with the main script. | |
| 160 */ | |
| 161 setupForAuthMain: function(port) { | |
| 162 this.channelMain_ = new Channel(); | |
| 163 this.channelMain_.init(port); | |
| 164 | |
| 165 // Registers for desktop related messages. | |
| 166 this.channelMain_.registerMessage( | |
| 167 'initDesktopFlow', this.onInitDesktopFlow_.bind(this)); | |
| 168 | |
| 169 // Registers for SAML related messages. | |
| 170 this.channelMain_.registerMessage( | |
| 171 'setGaiaUrl', this.onSetGaiaUrl_.bind(this)); | |
| 172 this.channelMain_.registerMessage( | |
| 173 'setBlockInsecureContent', this.onSetBlockInsecureContent_.bind(this)); | |
| 174 this.channelMain_.registerMessage( | |
| 175 'resetAuth', this.onResetAuth_.bind(this)); | |
| 176 this.channelMain_.registerMessage( | |
| 177 'startAuth', this.onAuthStarted_.bind(this)); | |
| 178 this.channelMain_.registerMessage( | |
| 179 'getScrapedPasswords', | |
| 180 this.onGetScrapedPasswords_.bind(this)); | |
| 181 this.channelMain_.registerMessage( | |
| 182 'apiResponse', this.onAPIResponse_.bind(this)); | |
| 183 | |
| 184 this.channelMain_.send({ | |
| 185 'name': 'channelConnected' | |
| 186 }); | |
| 187 }, | |
| 188 | |
| 189 /** | |
| 190 * Sets up the communication channel with the injected script. | |
| 191 */ | |
| 192 setupForInjected: function(port) { | |
| 193 this.channelInjected_ = new Channel(); | |
| 194 this.channelInjected_.init(port); | |
| 195 | |
| 196 this.channelInjected_.registerMessage( | |
| 197 'apiCall', this.onAPICall_.bind(this)); | |
| 198 this.channelInjected_.registerMessage( | |
| 199 'updatePassword', this.onUpdatePassword_.bind(this)); | |
| 200 this.channelInjected_.registerMessage( | |
| 201 'pageLoaded', this.onPageLoaded_.bind(this)); | |
| 202 this.channelInjected_.registerMessage( | |
| 203 'getSAMLFlag', this.onGetSAMLFlag_.bind(this)); | |
| 204 }, | |
| 205 | |
| 206 /** | |
| 207 * Handler for 'initDesktopFlow' signal sent from the main script. | |
| 208 * Only called in desktop mode. | |
| 209 */ | |
| 210 onInitDesktopFlow_: function(msg) { | |
| 211 this.isDesktopFlow_ = true; | |
| 212 this.gaiaUrl_ = msg.gaiaUrl; | |
| 213 this.isConstrainedWindow_ = msg.isConstrainedWindow; | |
| 214 this.initialFrameUrlWithoutParams = msg.initialFrameUrlWithoutParams; | |
| 215 }, | |
| 216 | |
| 217 /** | |
| 218 * Handler for webRequest.onCompleted. It 1) detects loading of continue URL | |
| 219 * and notifies the main script of signin completion; 2) detects if the | |
| 220 * current page could be loaded in a constrained window and signals the main | |
| 221 * script of switching to full tab if necessary. | |
| 222 */ | |
| 223 onCompleted: function(details) { | |
| 224 // Only monitors requests in the gaia frame. The gaia frame is the one | |
| 225 // where the initial frame URL completes. | |
| 226 if (details.url.lastIndexOf( | |
| 227 this.initialFrameUrlWithoutParams, 0) == 0) { | |
| 228 this.frameId = details.frameId; | |
| 229 } | |
| 230 if (this.frameId == -1) { | |
| 231 // If for some reason the frameId could not be set above, just make sure | |
| 232 // the frame is more than two levels deep (since the gaia frame is at | |
| 233 // least three levels deep). | |
| 234 if (details.parentFrameId <= 0) | |
| 235 return; | |
| 236 } else if (details.frameId != this.frameId) { | |
| 237 return; | |
| 238 } | |
| 239 | |
| 240 if (details.url.lastIndexOf(backgroundBridgeManager.CONTINUE_URL_BASE, 0) == | |
| 241 0) { | |
| 242 var skipForNow = false; | |
| 243 if (details.url.indexOf('ntp=1') >= 0) | |
| 244 skipForNow = true; | |
| 245 | |
| 246 // TOOD(guohui): For desktop SAML flow, show password confirmation UI. | |
| 247 var passwords = this.onGetScrapedPasswords_(); | |
| 248 var msg = { | |
| 249 'name': 'completeLogin', | |
| 250 'email': this.email_, | |
| 251 'gaiaId': this.gaiaId_, | |
| 252 'password': passwords[0], | |
| 253 'sessionIndex': this.sessionIndex_, | |
| 254 'skipForNow': skipForNow | |
| 255 }; | |
| 256 this.channelMain_.send(msg); | |
| 257 } else if (this.isConstrainedWindow_) { | |
| 258 // The header google-accounts-embedded is only set on gaia domain. | |
| 259 if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) { | |
| 260 var headers = details.responseHeaders; | |
| 261 for (var i = 0; headers && i < headers.length; ++i) { | |
| 262 if (headers[i].name.toLowerCase() == 'google-accounts-embedded') | |
| 263 return; | |
| 264 } | |
| 265 } | |
| 266 var msg = { | |
| 267 'name': 'switchToFullTab', | |
| 268 'url': details.url | |
| 269 }; | |
| 270 this.channelMain_.send(msg); | |
| 271 } | |
| 272 }, | |
| 273 | |
| 274 /** | |
| 275 * Handler for webRequest.onBeforeRequest, invoked when content served over an | |
| 276 * unencrypted connection is detected. Determines whether the request should | |
| 277 * be blocked and if so, signals that an error message needs to be shown. | |
| 278 * @param {string} url The URL that was blocked. | |
| 279 * @return {!Object} Decision whether to block the request. | |
| 280 */ | |
| 281 onInsecureRequest: function(url) { | |
| 282 if (!this.blockInsecureContent_) | |
| 283 return {}; | |
| 284 this.channelMain_.send({name: 'onInsecureContentBlocked', url: url}); | |
| 285 return {cancel: true}; | |
| 286 }, | |
| 287 | |
| 288 /** | |
| 289 * Handler or webRequest.onHeadersReceived. It reads the authenticated user | |
| 290 * email from google-accounts-signin-header. | |
| 291 * @return {!Object} Modified request headers. | |
| 292 */ | |
| 293 onHeadersReceived: function(details) { | |
| 294 var headers = details.responseHeaders; | |
| 295 | |
| 296 if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) { | |
| 297 for (var i = 0; headers && i < headers.length; ++i) { | |
| 298 if (headers[i].name.toLowerCase() == 'google-accounts-signin') { | |
| 299 var headerValues = headers[i].value.toLowerCase().split(','); | |
| 300 var signinDetails = {}; | |
| 301 headerValues.forEach(function(e) { | |
| 302 var pair = e.split('='); | |
| 303 signinDetails[pair[0].trim()] = pair[1].trim(); | |
| 304 }); | |
| 305 // Remove "" around. | |
| 306 this.email_ = signinDetails['email'].slice(1, -1); | |
| 307 this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1); | |
| 308 this.sessionIndex_ = signinDetails['sessionindex']; | |
| 309 break; | |
| 310 } | |
| 311 } | |
| 312 } | |
| 313 | |
| 314 if (!this.isDesktopFlow_) { | |
| 315 // Check whether GAIA headers indicating the start or end of a SAML | |
| 316 // redirect are present. If so, synthesize cookies to mark these points. | |
| 317 for (var i = 0; headers && i < headers.length; ++i) { | |
| 318 if (headers[i].name.toLowerCase() == 'google-accounts-saml') { | |
| 319 var action = headers[i].value.toLowerCase(); | |
| 320 if (action == 'start') { | |
| 321 this.isSAML_ = true; | |
| 322 // GAIA is redirecting to a SAML IdP. Any cookies contained in the | |
| 323 // current |headers| were set by GAIA. Any cookies set in future | |
| 324 // requests will be coming from the IdP. Append a cookie to the | |
| 325 // current |headers| that marks the point at which the redirect | |
| 326 // occurred. | |
| 327 headers.push({name: 'Set-Cookie', | |
| 328 value: 'google-accounts-saml-start=now'}); | |
| 329 return {responseHeaders: headers}; | |
| 330 } else if (action == 'end') { | |
| 331 this.isSAML_ = false; | |
| 332 // The SAML IdP has redirected back to GAIA. Add a cookie that marks | |
| 333 // the point at which the redirect occurred occurred. It is | |
| 334 // important that this cookie be prepended to the current |headers| | |
| 335 // because any cookies contained in the |headers| were already set | |
| 336 // by GAIA, not the IdP. Due to limitations in the webRequest API, | |
| 337 // it is not trivial to prepend a cookie: | |
| 338 // | |
| 339 // The webRequest API only allows for deleting and appending | |
| 340 // headers. To prepend a cookie (C), three steps are needed: | |
| 341 // 1) Delete any headers that set cookies (e.g., A, B). | |
| 342 // 2) Append a header which sets the cookie (C). | |
| 343 // 3) Append the original headers (A, B). | |
| 344 // | |
| 345 // Due to a further limitation of the webRequest API, it is not | |
| 346 // possible to delete a header in step 1) and append an identical | |
| 347 // header in step 3). To work around this, a trailing semicolon is | |
| 348 // added to each header before appending it. Trailing semicolons are | |
| 349 // ignored by Chrome in cookie headers, causing the modified headers | |
| 350 // to actually set the original cookies. | |
| 351 var otherHeaders = []; | |
| 352 var cookies = [{name: 'Set-Cookie', | |
| 353 value: 'google-accounts-saml-end=now'}]; | |
| 354 for (var j = 0; j < headers.length; ++j) { | |
| 355 if (headers[j].name.toLowerCase().startsWith('set-cookie')) { | |
| 356 var header = headers[j]; | |
| 357 header.value += ';'; | |
| 358 cookies.push(header); | |
| 359 } else { | |
| 360 otherHeaders.push(headers[j]); | |
| 361 } | |
| 362 } | |
| 363 return {responseHeaders: otherHeaders.concat(cookies)}; | |
| 364 } | |
| 365 } | |
| 366 } | |
| 367 } | |
| 368 | |
| 369 return {}; | |
| 370 }, | |
| 371 | |
| 372 /** | |
| 373 * Handler for webRequest.onBeforeSendHeaders. | |
| 374 * @return {!Object} Modified request headers. | |
| 375 */ | |
| 376 onBeforeSendHeaders: function(details) { | |
| 377 if (!this.isDesktopFlow_ && this.gaiaUrl_ && | |
| 378 details.url.startsWith(this.gaiaUrl_)) { | |
| 379 details.requestHeaders.push({ | |
| 380 name: 'X-Cros-Auth-Ext-Support', | |
| 381 value: 'SAML' | |
| 382 }); | |
| 383 } | |
| 384 return {requestHeaders: details.requestHeaders}; | |
| 385 }, | |
| 386 | |
| 387 /** | |
| 388 * Handler for 'setGaiaUrl' signal sent from the main script. | |
| 389 */ | |
| 390 onSetGaiaUrl_: function(msg) { | |
| 391 this.gaiaUrl_ = msg.gaiaUrl; | |
| 392 }, | |
| 393 | |
| 394 /** | |
| 395 * Handler for 'setBlockInsecureContent' signal sent from the main script. | |
| 396 */ | |
| 397 onSetBlockInsecureContent_: function(msg) { | |
| 398 this.blockInsecureContent_ = msg.blockInsecureContent; | |
| 399 }, | |
| 400 | |
| 401 /** | |
| 402 * Handler for 'resetAuth' signal sent from the main script. | |
| 403 */ | |
| 404 onResetAuth_: function() { | |
| 405 this.authStarted_ = false; | |
| 406 this.passwordStore_ = {}; | |
| 407 this.isSAML_ = false; | |
| 408 }, | |
| 409 | |
| 410 /** | |
| 411 * Handler for 'authStarted' signal sent from the main script. | |
| 412 */ | |
| 413 onAuthStarted_: function() { | |
| 414 this.authStarted_ = true; | |
| 415 this.passwordStore_ = {}; | |
| 416 this.isSAML_ = false; | |
| 417 }, | |
| 418 | |
| 419 /** | |
| 420 * Handler for 'getScrapedPasswords' request sent from the main script. | |
| 421 * @return {Array<string>} The array with de-duped scraped passwords. | |
| 422 */ | |
| 423 onGetScrapedPasswords_: function() { | |
| 424 var passwords = {}; | |
| 425 for (var property in this.passwordStore_) { | |
| 426 passwords[this.passwordStore_[property]] = true; | |
| 427 } | |
| 428 return Object.keys(passwords); | |
| 429 }, | |
| 430 | |
| 431 /** | |
| 432 * Handler for 'apiResponse' signal sent from the main script. Passes on the | |
| 433 * |msg| to the injected script. | |
| 434 */ | |
| 435 onAPIResponse_: function(msg) { | |
| 436 this.channelInjected_.send(msg); | |
| 437 }, | |
| 438 | |
| 439 onAPICall_: function(msg) { | |
| 440 this.channelMain_.send(msg); | |
| 441 }, | |
| 442 | |
| 443 onUpdatePassword_: function(msg) { | |
| 444 if (!this.authStarted_) | |
| 445 return; | |
| 446 | |
| 447 this.passwordStore_[msg.id] = msg.password; | |
| 448 }, | |
| 449 | |
| 450 onPageLoaded_: function(msg) { | |
| 451 if (this.channelMain_) | |
| 452 this.channelMain_.send({name: 'onAuthPageLoaded', | |
| 453 url: msg.url, | |
| 454 isSAMLPage: this.isSAML_}); | |
| 455 }, | |
| 456 | |
| 457 onGetSAMLFlag_: function(msg) { | |
| 458 return this.isSAML_; | |
| 459 } | |
| 460 }; | |
| 461 | |
| 462 var backgroundBridgeManager = new BackgroundBridgeManager(); | |
| 463 backgroundBridgeManager.run(); | |
| OLD | NEW |