| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 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 <include src="post_message_channel.js"> | 5 // <include src="post_message_channel.js"> |
| 6 | 6 |
| 7 /** | 7 /** |
| 8 * @fileoverview Saml support for webview based auth. | 8 * @fileoverview Saml support for webview based auth. |
| 9 */ | 9 */ |
| 10 | 10 |
| 11 cr.define('cr.login', function() { | 11 cr.define('cr.login', function() { |
| 12 'use strict'; | 12 'use strict'; |
| 13 | 13 |
| 14 /** | 14 /** |
| 15 * The lowest version of the credentials passing API supported. | 15 * The lowest version of the credentials passing API supported. |
| (...skipping 16 matching lines...) Expand all Loading... |
| 32 ]; | 32 ]; |
| 33 | 33 |
| 34 /** @const */ | 34 /** @const */ |
| 35 var SAML_HEADER = 'google-accounts-saml'; | 35 var SAML_HEADER = 'google-accounts-saml'; |
| 36 | 36 |
| 37 /** | 37 /** |
| 38 * The script to inject into webview and its sub frames. | 38 * The script to inject into webview and its sub frames. |
| 39 * @type {string} | 39 * @type {string} |
| 40 */ | 40 */ |
| 41 var injectedJs = String.raw` | 41 var injectedJs = String.raw` |
| 42 <include src="webview_saml_injected.js"> | 42 // <include src="webview_saml_injected.js"> |
| 43 `; | 43 `; |
| 44 | 44 |
| 45 /** | 45 /** |
| 46 * Creates a new URL by striping all query parameters. | 46 * Creates a new URL by striping all query parameters. |
| 47 * @param {string} url The original URL. | 47 * @param {string} url The original URL. |
| 48 * @return {string} The new URL with all query parameters stripped. | 48 * @return {string} The new URL with all query parameters stripped. |
| 49 */ | 49 */ |
| 50 function stripParams(url) { | 50 function stripParams(url) { |
| 51 return url.substring(0, url.indexOf('?')) || url; | 51 return url.substring(0, url.indexOf('?')) || url; |
| 52 } | 52 } |
| (...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 136 | 136 |
| 137 /* | 137 /* |
| 138 * Whether to abort the authentication flow and show an error messagen when | 138 * Whether to abort the authentication flow and show an error messagen when |
| 139 * content served over an unencrypted connection is detected. | 139 * content served over an unencrypted connection is detected. |
| 140 * @type {boolean} | 140 * @type {boolean} |
| 141 */ | 141 */ |
| 142 this.blockInsecureContent = false; | 142 this.blockInsecureContent = false; |
| 143 | 143 |
| 144 this.webview_.addEventListener( | 144 this.webview_.addEventListener( |
| 145 'contentload', this.onContentLoad_.bind(this)); | 145 'contentload', this.onContentLoad_.bind(this)); |
| 146 this.webview_.addEventListener( | 146 this.webview_.addEventListener('loadabort', this.onLoadAbort_.bind(this)); |
| 147 'loadabort', this.onLoadAbort_.bind(this)); | 147 this.webview_.addEventListener('loadcommit', this.onLoadCommit_.bind(this)); |
| 148 this.webview_.addEventListener( | |
| 149 'loadcommit', this.onLoadCommit_.bind(this)); | |
| 150 this.webview_.addEventListener( | 148 this.webview_.addEventListener( |
| 151 'permissionrequest', this.onPermissionRequest_.bind(this)); | 149 'permissionrequest', this.onPermissionRequest_.bind(this)); |
| 152 | 150 |
| 153 this.webview_.request.onBeforeRequest.addListener( | 151 this.webview_.request.onBeforeRequest.addListener( |
| 154 this.onInsecureRequest.bind(this), | 152 this.onInsecureRequest.bind(this), |
| 155 {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']}, | 153 {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']}, ['blocking']); |
| 156 ['blocking']); | |
| 157 this.webview_.request.onHeadersReceived.addListener( | 154 this.webview_.request.onHeadersReceived.addListener( |
| 158 this.onHeadersReceived_.bind(this), | 155 this.onHeadersReceived_.bind(this), |
| 159 {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']}, | 156 {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']}, |
| 160 ['blocking', 'responseHeaders']); | 157 ['blocking', 'responseHeaders']); |
| 161 | 158 |
| 162 this.webview_.addContentScripts([{ | 159 this.webview_.addContentScripts([{ |
| 163 name: 'samlInjected', | 160 name: 'samlInjected', |
| 164 matches: ['http://*/*', 'https://*/*'], | 161 matches: ['http://*/*', 'https://*/*'], |
| 165 js: { | 162 js: {code: injectedJs}, |
| 166 code: injectedJs | |
| 167 }, | |
| 168 all_frames: true, | 163 all_frames: true, |
| 169 run_at: 'document_start' | 164 run_at: 'document_start' |
| 170 }]); | 165 }]); |
| 171 | 166 |
| 172 PostMessageChannel.runAsDaemon(this.onConnected_.bind(this)); | 167 PostMessageChannel.runAsDaemon(this.onConnected_.bind(this)); |
| 173 } | 168 } |
| 174 | 169 |
| 175 SamlHandler.prototype = { | 170 SamlHandler.prototype = { |
| 176 __proto__: cr.EventTarget.prototype, | 171 __proto__: cr.EventTarget.prototype, |
| 177 | 172 |
| (...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 283 * an unencrypted connection is detected. Determines whether the request | 278 * an unencrypted connection is detected. Determines whether the request |
| 284 * should be blocked and if so, signals that an error message needs to be | 279 * should be blocked and if so, signals that an error message needs to be |
| 285 * shown. | 280 * shown. |
| 286 * @param {Object} details | 281 * @param {Object} details |
| 287 * @return {!Object} Decision whether to block the request. | 282 * @return {!Object} Decision whether to block the request. |
| 288 */ | 283 */ |
| 289 onInsecureRequest: function(details) { | 284 onInsecureRequest: function(details) { |
| 290 if (!this.blockInsecureContent) | 285 if (!this.blockInsecureContent) |
| 291 return {}; | 286 return {}; |
| 292 var strippedUrl = stripParams(details.url); | 287 var strippedUrl = stripParams(details.url); |
| 293 this.dispatchEvent(new CustomEvent('insecureContentBlocked', | 288 this.dispatchEvent(new CustomEvent( |
| 294 {detail: {url: strippedUrl}})); | 289 'insecureContentBlocked', {detail: {url: strippedUrl}})); |
| 295 return {cancel: true}; | 290 return {cancel: true}; |
| 296 }, | 291 }, |
| 297 | 292 |
| 298 /** | 293 /** |
| 299 * Invoked when headers are received for the main frame. | 294 * Invoked when headers are received for the main frame. |
| 300 * @private | 295 * @private |
| 301 */ | 296 */ |
| 302 onHeadersReceived_: function(details) { | 297 onHeadersReceived_: function(details) { |
| 303 var headers = details.responseHeaders; | 298 var headers = details.responseHeaders; |
| 304 | 299 |
| 305 // Check whether GAIA headers indicating the start or end of a SAML | 300 // Check whether GAIA headers indicating the start or end of a SAML |
| 306 // redirect are present. If so, synthesize cookies to mark these points. | 301 // redirect are present. If so, synthesize cookies to mark these points. |
| 307 for (var i = 0; headers && i < headers.length; ++i) { | 302 for (var i = 0; headers && i < headers.length; ++i) { |
| 308 var header = headers[i]; | 303 var header = headers[i]; |
| 309 var headerName = header.name.toLowerCase(); | 304 var headerName = header.name.toLowerCase(); |
| 310 | 305 |
| 311 if (headerName == SAML_HEADER) { | 306 if (headerName == SAML_HEADER) { |
| 312 var action = header.value.toLowerCase(); | 307 var action = header.value.toLowerCase(); |
| 313 if (action == 'start') { | 308 if (action == 'start') { |
| 314 this.pendingIsSamlPage_ = true; | 309 this.pendingIsSamlPage_ = true; |
| 315 | 310 |
| 316 // GAIA is redirecting to a SAML IdP. Any cookies contained in the | 311 // GAIA is redirecting to a SAML IdP. Any cookies contained in the |
| 317 // current |headers| were set by GAIA. Any cookies set in future | 312 // current |headers| were set by GAIA. Any cookies set in future |
| 318 // requests will be coming from the IdP. Append a cookie to the | 313 // requests will be coming from the IdP. Append a cookie to the |
| 319 // current |headers| that marks the point at which the redirect | 314 // current |headers| that marks the point at which the redirect |
| 320 // occurred. | 315 // occurred. |
| 321 headers.push({name: 'Set-Cookie', | 316 headers.push( |
| 322 value: 'google-accounts-saml-start=now'}); | 317 {name: 'Set-Cookie', value: 'google-accounts-saml-start=now'}); |
| 323 return {responseHeaders: headers}; | 318 return {responseHeaders: headers}; |
| 324 } else if (action == 'end') { | 319 } else if (action == 'end') { |
| 325 this.pendingIsSamlPage_ = false; | 320 this.pendingIsSamlPage_ = false; |
| 326 | 321 |
| 327 // The SAML IdP has redirected back to GAIA. Add a cookie that marks | 322 // The SAML IdP has redirected back to GAIA. Add a cookie that marks |
| 328 // the point at which the redirect occurred occurred. It is | 323 // the point at which the redirect occurred occurred. It is |
| 329 // important that this cookie be prepended to the current |headers| | 324 // important that this cookie be prepended to the current |headers| |
| 330 // because any cookies contained in the |headers| were already set | 325 // because any cookies contained in the |headers| were already set |
| 331 // by GAIA, not the IdP. Due to limitations in the webRequest API, | 326 // by GAIA, not the IdP. Due to limitations in the webRequest API, |
| 332 // it is not trivial to prepend a cookie: | 327 // it is not trivial to prepend a cookie: |
| 333 // | 328 // |
| 334 // The webRequest API only allows for deleting and appending | 329 // The webRequest API only allows for deleting and appending |
| 335 // headers. To prepend a cookie (C), three steps are needed: | 330 // headers. To prepend a cookie (C), three steps are needed: |
| 336 // 1) Delete any headers that set cookies (e.g., A, B). | 331 // 1) Delete any headers that set cookies (e.g., A, B). |
| 337 // 2) Append a header which sets the cookie (C). | 332 // 2) Append a header which sets the cookie (C). |
| 338 // 3) Append the original headers (A, B). | 333 // 3) Append the original headers (A, B). |
| 339 // | 334 // |
| 340 // Due to a further limitation of the webRequest API, it is not | 335 // Due to a further limitation of the webRequest API, it is not |
| 341 // possible to delete a header in step 1) and append an identical | 336 // possible to delete a header in step 1) and append an identical |
| 342 // header in step 3). To work around this, a trailing semicolon is | 337 // header in step 3). To work around this, a trailing semicolon is |
| 343 // added to each header before appending it. Trailing semicolons are | 338 // added to each header before appending it. Trailing semicolons are |
| 344 // ignored by Chrome in cookie headers, causing the modified headers | 339 // ignored by Chrome in cookie headers, causing the modified headers |
| 345 // to actually set the original cookies. | 340 // to actually set the original cookies. |
| 346 var otherHeaders = []; | 341 var otherHeaders = []; |
| 347 var cookies = [{name: 'Set-Cookie', | 342 var cookies = |
| 348 value: 'google-accounts-saml-end=now'}]; | 343 [{name: 'Set-Cookie', value: 'google-accounts-saml-end=now'}]; |
| 349 for (var j = 0; j < headers.length; ++j) { | 344 for (var j = 0; j < headers.length; ++j) { |
| 350 if (headers[j].name.toLowerCase().startsWith('set-cookie')) { | 345 if (headers[j].name.toLowerCase().startsWith('set-cookie')) { |
| 351 var header = headers[j]; | 346 var header = headers[j]; |
| 352 header.value += ';'; | 347 header.value += ';'; |
| 353 cookies.push(header); | 348 cookies.push(header); |
| 354 } else { | 349 } else { |
| 355 otherHeaders.push(headers[j]); | 350 otherHeaders.push(headers[j]); |
| 356 } | 351 } |
| 357 } | 352 } |
| 358 return {responseHeaders: otherHeaders.concat(cookies)}; | 353 return {responseHeaders: otherHeaders.concat(cookies)}; |
| 359 } | 354 } |
| 360 } | 355 } |
| 361 } | 356 } |
| 362 | 357 |
| 363 return {}; | 358 return {}; |
| 364 }, | 359 }, |
| 365 | 360 |
| 366 /** | 361 /** |
| 367 * Invoked when the injected JS makes a connection. | 362 * Invoked when the injected JS makes a connection. |
| 368 */ | 363 */ |
| 369 onConnected_: function(port) { | 364 onConnected_: function(port) { |
| 370 if (port.targetWindow != this.webview_.contentWindow) | 365 if (port.targetWindow != this.webview_.contentWindow) |
| 371 return; | 366 return; |
| 372 | 367 |
| 373 var channel = Channel.create(); | 368 var channel = Channel.create(); |
| 374 channel.init(port); | 369 channel.init(port); |
| 375 | 370 |
| 376 channel.registerMessage( | 371 channel.registerMessage('apiCall', this.onAPICall_.bind(this, channel)); |
| 377 'apiCall', this.onAPICall_.bind(this, channel)); | |
| 378 channel.registerMessage( | 372 channel.registerMessage( |
| 379 'updatePassword', this.onUpdatePassword_.bind(this, channel)); | 373 'updatePassword', this.onUpdatePassword_.bind(this, channel)); |
| 380 channel.registerMessage( | 374 channel.registerMessage( |
| 381 'pageLoaded', this.onPageLoaded_.bind(this, channel)); | 375 'pageLoaded', this.onPageLoaded_.bind(this, channel)); |
| 382 channel.registerMessage( | 376 channel.registerMessage( |
| 383 'getSAMLFlag', this.onGetSAMLFlag_.bind(this, channel)); | 377 'getSAMLFlag', this.onGetSAMLFlag_.bind(this, channel)); |
| 384 }, | 378 }, |
| 385 | 379 |
| 386 sendInitializationSuccess_: function(channel) { | 380 sendInitializationSuccess_: function(channel) { |
| 387 channel.send({name: 'apiResponse', response: { | 381 channel.send({ |
| 388 result: 'initialized', | 382 name: 'apiResponse', |
| 389 version: this.apiVersion_, | 383 response: { |
| 390 keyTypes: API_KEY_TYPES | 384 result: 'initialized', |
| 391 }}); | 385 version: this.apiVersion_, |
| 386 keyTypes: API_KEY_TYPES |
| 387 } |
| 388 }); |
| 392 }, | 389 }, |
| 393 | 390 |
| 394 sendInitializationFailure_: function(channel) { | 391 sendInitializationFailure_: function(channel) { |
| 395 channel.send({ | 392 channel.send( |
| 396 name: 'apiResponse', | 393 {name: 'apiResponse', response: {result: 'initialization_failed'}}); |
| 397 response: {result: 'initialization_failed'} | |
| 398 }); | |
| 399 }, | 394 }, |
| 400 | 395 |
| 401 /** | 396 /** |
| 402 * Handlers for channel messages. | 397 * Handlers for channel messages. |
| 403 * @param {Channel} channel A channel to send back response. | 398 * @param {Channel} channel A channel to send back response. |
| 404 * @param {Object} msg Received message. | 399 * @param {Object} msg Received message. |
| 405 * @private | 400 * @private |
| 406 */ | 401 */ |
| 407 onAPICall_: function(channel, msg) { | 402 onAPICall_: function(channel, msg) { |
| 408 var call = msg.call; | 403 var call = msg.call; |
| 409 if (call.method == 'initialize') { | 404 if (call.method == 'initialize') { |
| 410 if (!Number.isInteger(call.requestedVersion) || | 405 if (!Number.isInteger(call.requestedVersion) || |
| 411 call.requestedVersion < MIN_API_VERSION_VERSION) { | 406 call.requestedVersion < MIN_API_VERSION_VERSION) { |
| 412 this.sendInitializationFailure_(channel); | 407 this.sendInitializationFailure_(channel); |
| 413 return; | 408 return; |
| 414 } | 409 } |
| 415 | 410 |
| 416 this.apiVersion_ = Math.min(call.requestedVersion, | 411 this.apiVersion_ = |
| 417 MAX_API_VERSION_VERSION); | 412 Math.min(call.requestedVersion, MAX_API_VERSION_VERSION); |
| 418 this.apiInitialized_ = true; | 413 this.apiInitialized_ = true; |
| 419 this.sendInitializationSuccess_(channel); | 414 this.sendInitializationSuccess_(channel); |
| 420 return; | 415 return; |
| 421 } | 416 } |
| 422 | 417 |
| 423 if (call.method == 'add') { | 418 if (call.method == 'add') { |
| 424 if (API_KEY_TYPES.indexOf(call.keyType) == -1) { | 419 if (API_KEY_TYPES.indexOf(call.keyType) == -1) { |
| 425 console.error('SamlHandler.onAPICall_: unsupported key type'); | 420 console.error('SamlHandler.onAPICall_: unsupported key type'); |
| 426 return; | 421 return; |
| 427 } | 422 } |
| (...skipping 11 matching lines...) Expand all Loading... |
| 439 } | 434 } |
| 440 }, | 435 }, |
| 441 | 436 |
| 442 onUpdatePassword_: function(channel, msg) { | 437 onUpdatePassword_: function(channel, msg) { |
| 443 if (this.isSamlPage_) | 438 if (this.isSamlPage_) |
| 444 this.passwordStore_[msg.id] = msg.password; | 439 this.passwordStore_[msg.id] = msg.password; |
| 445 }, | 440 }, |
| 446 | 441 |
| 447 onPageLoaded_: function(channel, msg) { | 442 onPageLoaded_: function(channel, msg) { |
| 448 this.authDomain = extractDomain(msg.url); | 443 this.authDomain = extractDomain(msg.url); |
| 449 this.dispatchEvent(new CustomEvent( | 444 this.dispatchEvent(new CustomEvent('authPageLoaded', { |
| 450 'authPageLoaded', | 445 detail: { |
| 451 {detail: {url: url, | 446 url: url, |
| 452 isSAMLPage: this.isSamlPage_, | 447 isSAMLPage: this.isSamlPage_, |
| 453 domain: this.authDomain}})); | 448 domain: this.authDomain |
| 449 } |
| 450 })); |
| 454 }, | 451 }, |
| 455 | 452 |
| 456 onPermissionRequest_: function(permissionEvent) { | 453 onPermissionRequest_: function(permissionEvent) { |
| 457 if (permissionEvent.permission === 'media') { | 454 if (permissionEvent.permission === 'media') { |
| 458 // The actual permission check happens in | 455 // The actual permission check happens in |
| 459 // WebUILoginView::RequestMediaAccessPermission(). | 456 // WebUILoginView::RequestMediaAccessPermission(). |
| 460 this.dispatchEvent(new CustomEvent('videoEnabled')); | 457 this.dispatchEvent(new CustomEvent('videoEnabled')); |
| 461 permissionEvent.request.allow(); | 458 permissionEvent.request.allow(); |
| 462 } | 459 } |
| 463 }, | 460 }, |
| 464 | 461 |
| 465 onGetSAMLFlag_: function(channel, msg) { | 462 onGetSAMLFlag_: function(channel, msg) { |
| 466 return this.isSamlPage_; | 463 return this.isSamlPage_; |
| 467 }, | 464 }, |
| 468 }; | 465 }; |
| 469 | 466 |
| 470 return { | 467 return {SamlHandler: SamlHandler}; |
| 471 SamlHandler: SamlHandler | |
| 472 }; | |
| 473 }); | 468 }); |
| OLD | NEW |