Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 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="saml_handler.js"> | |
| 6 | |
| 5 /** | 7 /** |
| 6 * @fileoverview An UI component to authenciate to Chrome. The component hosts | 8 * @fileoverview An UI component to authenciate to Chrome. The component hosts |
| 7 * IdP web pages in a webview. A client who is interested in monitoring | 9 * IdP web pages in a webview. A client who is interested in monitoring |
| 8 * authentication events should pass a listener object of type | 10 * authentication events should pass a listener object of type |
| 9 * cr.login.GaiaAuthHost.Listener as defined in this file. After initialization, | 11 * cr.login.GaiaAuthHost.Listener as defined in this file. After initialization, |
| 10 * call {@code load} to start the authentication flow. | 12 * call {@code load} to start the authentication flow. |
| 11 */ | 13 */ |
| 14 | |
| 12 cr.define('cr.login', function() { | 15 cr.define('cr.login', function() { |
| 13 'use strict'; | 16 'use strict'; |
| 14 | 17 |
| 15 // TODO(rogerta): should use gaia URL from GaiaUrls::gaia_url() instead | 18 // TODO(rogerta): should use gaia URL from GaiaUrls::gaia_url() instead |
| 16 // of hardcoding the prod URL here. As is, this does not work with staging | 19 // of hardcoding the prod URL here. As is, this does not work with staging |
| 17 // environments. | 20 // environments. |
| 18 var IDP_ORIGIN = 'https://accounts.google.com/'; | 21 var IDP_ORIGIN = 'https://accounts.google.com/'; |
| 19 var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide'; | 22 var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide'; |
| 20 var CONTINUE_URL = | 23 var CONTINUE_URL = |
| 21 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html'; | 24 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html'; |
| (...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 88 this.authFlow_ = AuthFlow.DEFAULT; | 91 this.authFlow_ = AuthFlow.DEFAULT; |
| 89 this.loaded_ = false; | 92 this.loaded_ = false; |
| 90 this.idpOrigin_ = null; | 93 this.idpOrigin_ = null; |
| 91 this.continueUrl_ = null; | 94 this.continueUrl_ = null; |
| 92 this.continueUrlWithoutParams_ = null; | 95 this.continueUrlWithoutParams_ = null; |
| 93 this.initialFrameUrl_ = null; | 96 this.initialFrameUrl_ = null; |
| 94 this.reloadUrl_ = null; | 97 this.reloadUrl_ = null; |
| 95 this.trusted_ = true; | 98 this.trusted_ = true; |
| 96 this.oauth_code_ = null; | 99 this.oauth_code_ = null; |
| 97 | 100 |
| 101 this.samlHandler_ = new cr.login.SamlHandler(this.webview_); | |
| 102 this.confirmPasswordCallback = null; | |
| 103 this.noPasswordCallback = null; | |
| 104 this.insecureContentBlockedCallback = null; | |
| 105 this.samlApiUsedCallback = null; | |
| 106 this.needPassword = true; | |
| 107 this.samlHandler_.addEventListener( | |
| 108 'insecureContentBlocked', | |
| 109 this.onInsecureContentBlocked_.bind(this)); | |
| 110 | |
| 98 this.webview_.addEventListener('droplink', this.onDropLink_.bind(this)); | 111 this.webview_.addEventListener('droplink', this.onDropLink_.bind(this)); |
| 99 this.webview_.addEventListener( | 112 this.webview_.addEventListener( |
| 100 'newwindow', this.onNewWindow_.bind(this)); | 113 'newwindow', this.onNewWindow_.bind(this)); |
| 101 this.webview_.addEventListener( | 114 this.webview_.addEventListener( |
| 102 'contentload', this.onContentLoad_.bind(this)); | 115 'contentload', this.onContentLoad_.bind(this)); |
| 103 this.webview_.addEventListener( | 116 this.webview_.addEventListener( |
| 104 'loadstop', this.onLoadStop_.bind(this)); | 117 'loadstop', this.onLoadStop_.bind(this)); |
| 105 this.webview_.addEventListener( | 118 this.webview_.addEventListener( |
| 106 'loadcommit', this.onLoadCommit_.bind(this)); | 119 'loadcommit', this.onLoadCommit_.bind(this)); |
| 107 this.webview_.request.onCompleted.addListener( | 120 this.webview_.request.onCompleted.addListener( |
| 108 this.onRequestCompleted_.bind(this), | 121 this.onRequestCompleted_.bind(this), |
| 109 {urls: ['<all_urls>'], types: ['main_frame']}, | 122 {urls: ['<all_urls>'], types: ['main_frame']}, |
| 110 ['responseHeaders']); | 123 ['responseHeaders']); |
| 111 this.webview_.request.onHeadersReceived.addListener( | 124 this.webview_.request.onHeadersReceived.addListener( |
| 112 this.onHeadersReceived_.bind(this), | 125 this.onHeadersReceived_.bind(this), |
| 113 {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']}, | 126 {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']}, |
| 114 ['responseHeaders']); | 127 ['responseHeaders']); |
| 115 window.addEventListener( | 128 window.addEventListener( |
| 116 'message', this.onMessageFromWebview_.bind(this), false); | 129 'message', this.onMessageFromWebview_.bind(this), false); |
| 117 window.addEventListener( | 130 window.addEventListener( |
| 118 'focus', this.onFocus_.bind(this), false); | 131 'focus', this.onFocus_.bind(this), false); |
| 119 window.addEventListener( | 132 window.addEventListener( |
| 120 'popstate', this.onPopState_.bind(this), false); | 133 'popstate', this.onPopState_.bind(this), false); |
| 121 } | 134 } |
| 122 | 135 |
| 123 // TODO(guohui,xiyuan): no need to inherit EventTarget once we deprecate the | |
| 124 // old event-based signin flow. | |
| 125 Authenticator.prototype = Object.create(cr.EventTarget.prototype); | 136 Authenticator.prototype = Object.create(cr.EventTarget.prototype); |
| 126 | 137 |
| 127 /** | 138 /** |
| 128 * Loads the authenticator component with the given parameters. | 139 * Loads the authenticator component with the given parameters. |
| 129 * @param {AuthMode} authMode Authorization mode. | 140 * @param {AuthMode} authMode Authorization mode. |
| 130 * @param {Object} data Parameters for the authorization flow. | 141 * @param {Object} data Parameters for the authorization flow. |
| 131 */ | 142 */ |
| 132 Authenticator.prototype.load = function(authMode, data) { | 143 Authenticator.prototype.load = function(authMode, data) { |
| 133 this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN; | 144 this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN; |
| 134 this.continueUrl_ = data.continueUrl || CONTINUE_URL; | 145 this.continueUrl_ = data.continueUrl || CONTINUE_URL; |
| 135 this.continueUrlWithoutParams_ = | 146 this.continueUrlWithoutParams_ = |
| 136 this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) || | 147 this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) || |
| 137 this.continueUrl_; | 148 this.continueUrl_; |
| 138 this.isConstrainedWindow_ = data.constrained == '1'; | 149 this.isConstrainedWindow_ = data.constrained == '1'; |
| 139 this.isMinuteMaidChromeOS = data.isMinuteMaidChromeOS; | 150 this.isMinuteMaidChromeOS = data.isMinuteMaidChromeOS; |
| 140 | 151 |
| 141 this.initialFrameUrl_ = this.constructInitialFrameUrl_(data); | 152 this.initialFrameUrl_ = this.constructInitialFrameUrl_(data); |
| 142 this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_; | 153 this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_; |
| 143 this.authFlow_ = AuthFlow.DEFAULT; | 154 this.authFlow_ = AuthFlow.DEFAULT; |
| 155 this.samlHandler_.reset(); | |
| 156 // Don't block insecure content for desktop flow because it lands on | |
| 157 // http. Otherwise, block insecure content as long as gaia is https. | |
| 158 this.samlHandler_.blockInsecureContent = authMode != AuthMode.DESKTOP && | |
| 159 this.idpOrigin_.indexOf('https://') == 0; | |
| 144 | 160 |
| 145 this.webview_.src = this.reloadUrl_; | 161 this.webview_.src = this.reloadUrl_; |
| 146 | 162 |
| 147 this.loaded_ = false; | 163 this.loaded_ = false; |
| 148 }; | 164 }; |
| 149 | 165 |
| 150 /** | 166 /** |
| 151 * Reloads the authenticator component. | 167 * Reloads the authenticator component. |
| 152 */ | 168 */ |
| 153 Authenticator.prototype.reload = function() { | 169 Authenticator.prototype.reload = function() { |
| 154 this.webview_.src = this.reloadUrl_; | 170 this.webview_.src = this.reloadUrl_; |
| 155 this.authFlow_ = AuthFlow.DEFAULT; | 171 this.authFlow_ = AuthFlow.DEFAULT; |
| 172 this.samlHandler_.reset(); | |
| 156 this.loaded_ = false; | 173 this.loaded_ = false; |
| 157 }; | 174 }; |
| 158 | 175 |
| 159 Authenticator.prototype.constructInitialFrameUrl_ = function(data) { | 176 Authenticator.prototype.constructInitialFrameUrl_ = function(data) { |
| 160 var url = this.idpOrigin_ + (data.gaiaPath || IDP_PATH); | 177 var url = this.idpOrigin_ + (data.gaiaPath || IDP_PATH); |
| 161 | 178 |
| 162 if (this.isMinuteMaidChromeOS) { | 179 if (this.isMinuteMaidChromeOS) { |
| 163 if (data.chromeType) | 180 if (data.chromeType) |
| 164 url = appendParam(url, 'chrometype', data.chromeType); | 181 url = appendParam(url, 'chrometype', data.chromeType); |
| 165 if (data.clientId) | 182 if (data.clientId) |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 183 * Invoked when a main frame request in the webview has completed. | 200 * Invoked when a main frame request in the webview has completed. |
| 184 * @private | 201 * @private |
| 185 */ | 202 */ |
| 186 Authenticator.prototype.onRequestCompleted_ = function(details) { | 203 Authenticator.prototype.onRequestCompleted_ = function(details) { |
| 187 var currentUrl = details.url; | 204 var currentUrl = details.url; |
| 188 | 205 |
| 189 if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) { | 206 if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) { |
| 190 if (currentUrl.indexOf('ntp=1') >= 0) | 207 if (currentUrl.indexOf('ntp=1') >= 0) |
| 191 this.skipForNow_ = true; | 208 this.skipForNow_ = true; |
| 192 | 209 |
| 193 this.onAuthCompleted_(); | 210 this.maybeCompleteAuth_(); |
| 194 return; | 211 return; |
| 195 } | 212 } |
| 196 | 213 |
| 197 if (currentUrl.indexOf('https') != 0) | 214 if (currentUrl.indexOf('https') != 0) |
| 198 this.trusted_ = false; | 215 this.trusted_ = false; |
| 199 | 216 |
| 200 if (this.isConstrainedWindow_) { | 217 if (this.isConstrainedWindow_) { |
| 201 var isEmbeddedPage = false; | 218 var isEmbeddedPage = false; |
| 202 if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) { | 219 if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) { |
| 203 var headers = details.responseHeaders; | 220 var headers = details.responseHeaders; |
| 204 for (var i = 0; headers && i < headers.length; ++i) { | 221 for (var i = 0; headers && i < headers.length; ++i) { |
| 205 if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) { | 222 if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) { |
| 206 isEmbeddedPage = true; | 223 isEmbeddedPage = true; |
| 207 break; | 224 break; |
| 208 } | 225 } |
| 209 } | 226 } |
| 210 } | 227 } |
| 211 if (!isEmbeddedPage) { | 228 if (!isEmbeddedPage) { |
| 212 this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl})); | 229 this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl})); |
| 213 return; | 230 return; |
| 214 } | 231 } |
| 215 } | 232 } |
| 216 | 233 |
| 217 this.updateHistoryState_(currentUrl); | 234 this.updateHistoryState_(currentUrl); |
| 218 | |
| 219 }; | 235 }; |
| 220 | 236 |
| 221 /** | 237 /** |
| 222 * Manually updates the history. Invoked upon completion of a webview | 238 * Manually updates the history. Invoked upon completion of a webview |
| 223 * navigation. | 239 * navigation. |
| 224 * @param {string} url Request URL. | 240 * @param {string} url Request URL. |
| 225 * @private | 241 * @private |
| 226 */ | 242 */ |
| 227 Authenticator.prototype.updateHistoryState_ = function(url) { | 243 Authenticator.prototype.updateHistoryState_ = function(url) { |
| 228 if (history.state && history.state.url != url) | 244 if (history.state && history.state.url != url) |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 299 * Invoked when an HTML5 message is received from the webview element. | 315 * Invoked when an HTML5 message is received from the webview element. |
| 300 * @param {object} e Payload of the received HTML5 message. | 316 * @param {object} e Payload of the received HTML5 message. |
| 301 * @private | 317 * @private |
| 302 */ | 318 */ |
| 303 Authenticator.prototype.onMessageFromWebview_ = function(e) { | 319 Authenticator.prototype.onMessageFromWebview_ = function(e) { |
| 304 // The event origin does not have a trailing slash. | 320 // The event origin does not have a trailing slash. |
| 305 if (e.origin != this.idpOrigin_.substring(0, this.idpOrigin_.length - 1)) { | 321 if (e.origin != this.idpOrigin_.substring(0, this.idpOrigin_.length - 1)) { |
| 306 return; | 322 return; |
| 307 } | 323 } |
| 308 | 324 |
| 325 // Gaia messages must be an object with 'method' property. | |
| 326 if (typeof e.data != 'object' || !e.data.hasOwnProperty('method')) { | |
| 327 return; | |
| 328 } | |
| 329 | |
| 309 var msg = e.data; | 330 var msg = e.data; |
| 310 if (msg.method == 'attemptLogin') { | 331 if (msg.method == 'attemptLogin') { |
| 311 this.email_ = msg.email; | 332 this.email_ = msg.email; |
| 312 this.password_ = msg.password; | 333 this.password_ = msg.password; |
| 313 this.chooseWhatToSync_ = msg.chooseWhatToSync; | 334 this.chooseWhatToSync_ = msg.chooseWhatToSync; |
| 314 } | 335 } |
| 315 }; | 336 }; |
| 316 | 337 |
| 317 /** | 338 /** |
| 339 * Invoked by the hosting page to verify the Saml password. | |
| 340 */ | |
| 341 Authenticator.prototype.verifyConfirmedPassword = function(password) { | |
| 342 if (!this.samlHandler_.verifyConfirmedPassword(password)) { | |
| 343 // Invoke confirm password callback asynchronously because the | |
| 344 // verification was based on messages and caller (GaiaSigninScreen) | |
| 345 // does not expect it to be called immediately. | |
| 346 // TODO(xiyuan): Change to synchronous call when iframe based code | |
| 347 // is removed. | |
| 348 var invokeConfirmPassword = (function() { | |
| 349 this.confirmPasswordCallback(this.samlHandler_.scrapedPasswordCount); | |
| 350 }).bind(this); | |
| 351 window.setTimeout(invokeConfirmPassword, 0); | |
| 352 return; | |
| 353 } | |
| 354 | |
| 355 this.password_ = password; | |
| 356 this.onAuthCompleted_(); | |
| 357 }; | |
| 358 | |
| 359 /** | |
| 360 * Check Saml flow and start password confirmation flow if needed. Otherwise, | |
| 361 * continue with auto completion. | |
| 362 * @private | |
| 363 */ | |
| 364 Authenticator.prototype.maybeCompleteAuth_ = function() { | |
| 365 if (this.authFlow_ != AuthFlow.SAML) { | |
| 366 this.onAuthCompleted_(); | |
| 367 return; | |
| 368 } | |
| 369 | |
| 370 if (this.samlHandler_.samlApiUsed) { | |
| 371 if (this.samlApiUsedCallback) { | |
| 372 this.samlApiUsedCallback(); | |
| 373 } | |
| 374 this.password_ = this.samlHandler_.apiPasswordBytes; | |
| 375 } else if (this.samlHandler_.scrapedPasswordCount == 0) { | |
| 376 if (this.noPasswordCallback) { | |
| 377 this.noPasswordCallback(this.email_); | |
| 378 } else { | |
| 379 console.error('Authenticator: No password scraped for SAML.'); | |
| 380 return; | |
| 381 } | |
| 382 } else if (this.needPassword) { | |
| 383 if (this.confirmPasswordCallback) { | |
| 384 // Confirm scraped password. The flow follows in | |
| 385 // verifyConfirmedPassword. | |
| 386 this.confirmPasswordCallback(this.samlHandler_.scrapedPasswordCount); | |
| 387 return; | |
| 388 } | |
| 389 } | |
| 390 | |
| 391 this.onAuthCompleted_(); | |
| 392 }; | |
| 393 | |
| 394 /** | |
| 318 * Invoked to process authentication completion. | 395 * Invoked to process authentication completion. |
| 319 * @private | 396 * @private |
| 320 */ | 397 */ |
| 321 Authenticator.prototype.onAuthCompleted_ = function() { | 398 Authenticator.prototype.onAuthCompleted_ = function() { |
| 322 if (!this.email_ && !this.skipForNow_) { | 399 if (!this.email_ && !this.skipForNow_) { |
| 323 this.webview_.src = this.initialFrameUrl_; | 400 this.webview_.src = this.initialFrameUrl_; |
| 324 return; | 401 return; |
| 325 } | 402 } |
| 326 | 403 |
| 327 this.dispatchEvent( | 404 this.dispatchEvent( |
| 328 new CustomEvent('authCompleted', | 405 new CustomEvent('authCompleted', |
| 329 // TODO(rsorokin): get rid of the stub values. | 406 // TODO(rsorokin): get rid of the stub values. |
| 330 {detail: {email: this.email_ || '', | 407 {detail: {email: this.email_ || '', |
| 331 gaiaId: this.gaiaId_ || '', | 408 gaiaId: this.gaiaId_ || '', |
| 332 password: this.password_ || '', | 409 password: this.password_ || '', |
| 333 authCode: this.oauth_code_, | 410 authCode: this.oauth_code_, |
| 334 usingSAML: this.authFlow_ == AuthFlow.SAML, | 411 usingSAML: this.authFlow_ == AuthFlow.SAML, |
| 335 chooseWhatToSync: this.chooseWhatToSync_, | 412 chooseWhatToSync: this.chooseWhatToSync_, |
| 336 skipForNow: this.skipForNow_, | 413 skipForNow: this.skipForNow_, |
| 337 sessionIndex: this.sessionIndex_ || '', | 414 sessionIndex: this.sessionIndex_ || '', |
| 338 trusted: this.trusted_}})); | 415 trusted: this.trusted_}})); |
| 339 }; | 416 }; |
| 340 | 417 |
| 341 /** | 418 /** |
| 419 * Invoked when |samlHandler_| fires 'insecureContentBlocked' event. | |
| 420 * @private | |
| 421 */ | |
| 422 Authenticator.prototype.onInsecureContentBlocked_ = function(e) { | |
| 423 if (this.insecureContentBlockedCallback) { | |
| 424 this.insecureContentBlockedCallback(e.details.url); | |
| 425 } else { | |
| 426 console.error('Authenticator: Insecure content blocked.'); | |
| 427 } | |
| 428 }; | |
| 429 | |
| 430 /** | |
| 342 * Invoked when a link is dropped on the webview. | 431 * Invoked when a link is dropped on the webview. |
| 343 * @private | 432 * @private |
| 344 */ | 433 */ |
| 345 Authenticator.prototype.onDropLink_ = function(e) { | 434 Authenticator.prototype.onDropLink_ = function(e) { |
| 346 this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url})); | 435 this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url})); |
| 347 }; | 436 }; |
| 348 | 437 |
| 349 /** | 438 /** |
| 350 * Invoked when the webview attempts to open a new window. | 439 * Invoked when the webview attempts to open a new window. |
| 351 * @private | 440 * @private |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 380 // Focus webview after dispatching event when webview is already visible. | 469 // Focus webview after dispatching event when webview is already visible. |
| 381 this.webview_.focus(); | 470 this.webview_.focus(); |
| 382 } | 471 } |
| 383 }; | 472 }; |
| 384 | 473 |
| 385 /** | 474 /** |
| 386 * Invoked when the webview navigates withing the current document. | 475 * Invoked when the webview navigates withing the current document. |
| 387 * @private | 476 * @private |
| 388 */ | 477 */ |
| 389 Authenticator.prototype.onLoadCommit_ = function(e) { | 478 Authenticator.prototype.onLoadCommit_ = function(e) { |
| 390 // TODO(rsorokin): Investigate whether this breaks SAML. | 479 // TODO(rsorokin): Investigate whether this breaks SAML. |
|
Dmitry Polukhin
2015/03/13 11:56:19
nit, it seems this todo can be removed now.
xiyuan
2015/03/17 22:00:15
Done.
| |
| 391 if (this.oauth_code_) { | 480 if (this.oauth_code_) { |
| 392 this.skipForNow_ = true; | 481 this.skipForNow_ = true; |
| 393 this.onAuthCompleted_(); | 482 this.maybeCompleteAuth_(); |
| 394 } | 483 } |
| 395 }; | 484 }; |
| 396 | 485 |
| 397 Authenticator.AuthFlow = AuthFlow; | 486 Authenticator.AuthFlow = AuthFlow; |
| 398 Authenticator.AuthMode = AuthMode; | 487 Authenticator.AuthMode = AuthMode; |
| 399 Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS; | 488 Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS; |
| 400 | 489 |
| 401 return { | 490 return { |
| 402 // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old | 491 // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old |
| 403 // iframe-based flow is deprecated. | 492 // iframe-based flow is deprecated. |
| 404 GaiaAuthHost: Authenticator | 493 GaiaAuthHost: Authenticator |
| 405 }; | 494 }; |
| 406 }); | 495 }); |
| OLD | NEW |