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 120 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 |