| Index: chrome/browser/resources/gaia_auth_host/authenticator.js
|
| diff --git a/chrome/browser/resources/gaia_auth_host/authenticator.js b/chrome/browser/resources/gaia_auth_host/authenticator.js
|
| index a1296a4758019dc885983f75563788d15ebcd7f6..e2dc8b69d3b1f5ff64cea31a317d3d5f3eb9ff6e 100644
|
| --- a/chrome/browser/resources/gaia_auth_host/authenticator.js
|
| +++ b/chrome/browser/resources/gaia_auth_host/authenticator.js
|
| @@ -2,6 +2,8 @@
|
| // Use of this source code is governed by a BSD-style license that can be
|
| // found in the LICENSE file.
|
|
|
| +<include src="saml_handler.js">
|
| +
|
| /**
|
| * @fileoverview An UI component to authenciate to Chrome. The component hosts
|
| * IdP web pages in a webview. A client who is interested in monitoring
|
| @@ -9,6 +11,7 @@
|
| * cr.login.GaiaAuthHost.Listener as defined in this file. After initialization,
|
| * call {@code load} to start the authentication flow.
|
| */
|
| +
|
| cr.define('cr.login', function() {
|
| 'use strict';
|
|
|
| @@ -21,7 +24,6 @@ cr.define('cr.login', function() {
|
| 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/success.html';
|
| var SIGN_IN_HEADER = 'google-accounts-signin';
|
| var EMBEDDED_FORM_HEADER = 'google-accounts-embedded';
|
| - var SAML_HEADER = 'google-accounts-saml';
|
| var LOCATION_HEADER = 'location';
|
| var SET_COOKIE_HEADER = 'set-cookie';
|
| var OAUTH_CODE_COOKIE = 'oauth_code';
|
| @@ -85,7 +87,7 @@ cr.define('cr.login', function() {
|
| this.sessionIndex_ = null;
|
| this.chooseWhatToSync_ = false;
|
| this.skipForNow_ = false;
|
| - this.authFlow_ = AuthFlow.DEFAULT;
|
| + this.authFlow = AuthFlow.DEFAULT;
|
| this.loaded_ = false;
|
| this.idpOrigin_ = null;
|
| this.continueUrl_ = null;
|
| @@ -95,6 +97,26 @@ cr.define('cr.login', function() {
|
| this.trusted_ = true;
|
| this.oauth_code_ = null;
|
|
|
| + this.samlHandler_ = new cr.login.SamlHandler(this.webview_);
|
| + this.confirmPasswordCallback = null;
|
| + this.noPasswordCallback = null;
|
| + this.insecureContentBlockedCallback = null;
|
| + this.samlApiUsedCallback = null;
|
| + this.missingGaiaInfoCallback = null;
|
| + this.needPassword = true;
|
| + this.samlHandler_.addEventListener(
|
| + 'insecureContentBlocked',
|
| + this.onInsecureContentBlocked_.bind(this));
|
| + this.samlHandler_.addEventListener(
|
| + 'authPageLoaded',
|
| + this.onAuthPageLoaded_.bind(this));
|
| + Object.defineProperty(this, 'authDomain', {
|
| + get: (function() {
|
| + return this.samlHandler_.authDomain;
|
| + }).bind(this),
|
| + enumerable: true
|
| + });
|
| +
|
| this.webview_.addEventListener('droplink', this.onDropLink_.bind(this));
|
| this.webview_.addEventListener(
|
| 'newwindow', this.onNewWindow_.bind(this));
|
| @@ -120,8 +142,6 @@ cr.define('cr.login', function() {
|
| 'popstate', this.onPopState_.bind(this), false);
|
| }
|
|
|
| - // TODO(guohui,xiyuan): no need to inherit EventTarget once we deprecate the
|
| - // old event-based signin flow.
|
| Authenticator.prototype = Object.create(cr.EventTarget.prototype);
|
|
|
| /**
|
| @@ -140,7 +160,12 @@ cr.define('cr.login', function() {
|
|
|
| this.initialFrameUrl_ = this.constructInitialFrameUrl_(data);
|
| this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_;
|
| - this.authFlow_ = AuthFlow.DEFAULT;
|
| + this.authFlow = AuthFlow.DEFAULT;
|
| + this.samlHandler_.reset();
|
| + // Don't block insecure content for desktop flow because it lands on
|
| + // http. Otherwise, block insecure content as long as gaia is https.
|
| + this.samlHandler_.blockInsecureContent = authMode != AuthMode.DESKTOP &&
|
| + this.idpOrigin_.indexOf('https://') == 0;
|
|
|
| this.webview_.src = this.reloadUrl_;
|
|
|
| @@ -152,7 +177,8 @@ cr.define('cr.login', function() {
|
| */
|
| Authenticator.prototype.reload = function() {
|
| this.webview_.src = this.reloadUrl_;
|
| - this.authFlow_ = AuthFlow.DEFAULT;
|
| + this.authFlow = AuthFlow.DEFAULT;
|
| + this.samlHandler_.reset();
|
| this.loaded_ = false;
|
| };
|
|
|
| @@ -190,7 +216,7 @@ cr.define('cr.login', function() {
|
| if (currentUrl.indexOf('ntp=1') >= 0)
|
| this.skipForNow_ = true;
|
|
|
| - this.onAuthCompleted_();
|
| + this.maybeCompleteAuth_();
|
| return;
|
| }
|
|
|
| @@ -215,7 +241,6 @@ cr.define('cr.login', function() {
|
| }
|
|
|
| this.updateHistoryState_(currentUrl);
|
| -
|
| };
|
|
|
| /**
|
| @@ -278,8 +303,6 @@ cr.define('cr.login', function() {
|
| this.email_ = signinDetails['email'].slice(1, -1);
|
| this.gaiaId_ = signinDetails['obfuscatedid'].slice(1, -1);
|
| this.sessionIndex_ = signinDetails['sessionindex'];
|
| - } else if (headerName == SAML_HEADER) {
|
| - this.authFlow_ = AuthFlow.SAML;
|
| } else if (headerName == LOCATION_HEADER) {
|
| // If the "choose what to sync" checkbox was clicked, then the continue
|
| // URL will contain a source=3 field.
|
| @@ -306,6 +329,11 @@ cr.define('cr.login', function() {
|
| return;
|
| }
|
|
|
| + // Gaia messages must be an object with 'method' property.
|
| + if (typeof e.data != 'object' || !e.data.hasOwnProperty('method')) {
|
| + return;
|
| + }
|
| +
|
| var msg = e.data;
|
| if (msg.method == 'attemptLogin') {
|
| this.email_ = msg.email;
|
| @@ -321,15 +349,77 @@ cr.define('cr.login', function() {
|
| };
|
|
|
| /**
|
| - * Invoked to process authentication completion.
|
| + * Invoked by the hosting page to verify the Saml password.
|
| + */
|
| + Authenticator.prototype.verifyConfirmedPassword = function(password) {
|
| + if (!this.samlHandler_.verifyConfirmedPassword(password)) {
|
| + // Invoke confirm password callback asynchronously because the
|
| + // verification was based on messages and caller (GaiaSigninScreen)
|
| + // does not expect it to be called immediately.
|
| + // TODO(xiyuan): Change to synchronous call when iframe based code
|
| + // is removed.
|
| + var invokeConfirmPassword = (function() {
|
| + this.confirmPasswordCallback(this.samlHandler_.scrapedPasswordCount);
|
| + }).bind(this);
|
| + window.setTimeout(invokeConfirmPassword, 0);
|
| + return;
|
| + }
|
| +
|
| + this.password_ = password;
|
| + this.onAuthCompleted_();
|
| + };
|
| +
|
| + /**
|
| + * Check Saml flow and start password confirmation flow if needed. Otherwise,
|
| + * continue with auto completion.
|
| * @private
|
| */
|
| - Authenticator.prototype.onAuthCompleted_ = function() {
|
| - if (!this.email_ && !this.skipForNow_) {
|
| + Authenticator.prototype.maybeCompleteAuth_ = function() {
|
| + var missingGaiaInfo = !this.email_ || !this.gaiaId_ || !this.sessionIndex_;
|
| + if (missingGaiaInfo && !this.skipForNow_) {
|
| + if (this.missingGaiaInfoCallback)
|
| + this.missingGaiaInfoCallback();
|
| +
|
| this.webview_.src = this.initialFrameUrl_;
|
| return;
|
| }
|
|
|
| + if (this.authFlow != AuthFlow.SAML) {
|
| + this.onAuthCompleted_();
|
| + return;
|
| + }
|
| +
|
| + if (this.samlHandler_.samlApiUsed) {
|
| + if (this.samlApiUsedCallback) {
|
| + this.samlApiUsedCallback();
|
| + }
|
| + this.password_ = this.samlHandler_.apiPasswordBytes;
|
| + } else if (this.samlHandler_.scrapedPasswordCount == 0) {
|
| + if (this.noPasswordCallback) {
|
| + this.noPasswordCallback(this.email_);
|
| + } else {
|
| + console.error('Authenticator: No password scraped for SAML.');
|
| + }
|
| + return;
|
| + } else if (this.needPassword) {
|
| + if (this.confirmPasswordCallback) {
|
| + // Confirm scraped password. The flow follows in
|
| + // verifyConfirmedPassword.
|
| + this.confirmPasswordCallback(this.samlHandler_.scrapedPasswordCount);
|
| + return;
|
| + }
|
| + }
|
| +
|
| + this.onAuthCompleted_();
|
| + };
|
| +
|
| + /**
|
| + * Invoked to process authentication completion.
|
| + * @private
|
| + */
|
| + Authenticator.prototype.onAuthCompleted_ = function() {
|
| + assert(this.skipForNow_ ||
|
| + (this.email_ && this.gaiaId_ && this.sessionIndex_));
|
| this.dispatchEvent(
|
| new CustomEvent('authCompleted',
|
| // TODO(rsorokin): get rid of the stub values.
|
| @@ -337,7 +427,7 @@ cr.define('cr.login', function() {
|
| gaiaId: this.gaiaId_ || '',
|
| password: this.password_ || '',
|
| authCode: this.oauth_code_,
|
| - usingSAML: this.authFlow_ == AuthFlow.SAML,
|
| + usingSAML: this.authFlow == AuthFlow.SAML,
|
| chooseWhatToSync: this.chooseWhatToSync_,
|
| skipForNow: this.skipForNow_,
|
| sessionIndex: this.sessionIndex_ || '',
|
| @@ -345,6 +435,34 @@ cr.define('cr.login', function() {
|
| };
|
|
|
| /**
|
| + * Invoked when |samlHandler_| fires 'insecureContentBlocked' event.
|
| + * @private
|
| + */
|
| + Authenticator.prototype.onInsecureContentBlocked_ = function(e) {
|
| + if (this.insecureContentBlockedCallback) {
|
| + this.insecureContentBlockedCallback(e.detail.url);
|
| + } else {
|
| + console.error('Authenticator: Insecure content blocked.');
|
| + }
|
| + };
|
| +
|
| + /**
|
| + * Invoked when |samlHandler_| fires 'authPageLoaded' event.
|
| + * @private
|
| + */
|
| + Authenticator.prototype.onAuthPageLoaded_ = function(e) {
|
| + if (!e.detail.isSAMLPage)
|
| + return;
|
| +
|
| + if (this.authFlow != AuthFlow.SAML) {
|
| + this.authFlow = AuthFlow.SAML;
|
| + } else {
|
| + // Force an authFlowChanged event to update UI with updated auth doamin.
|
| + cr.dispatchPropertyChange(this, 'authFlow');
|
| + }
|
| + };
|
| +
|
| + /**
|
| * Invoked when a link is dropped on the webview.
|
| * @private
|
| */
|
| @@ -393,13 +511,18 @@ cr.define('cr.login', function() {
|
| * @private
|
| */
|
| Authenticator.prototype.onLoadCommit_ = function(e) {
|
| - // TODO(rsorokin): Investigate whether this breaks SAML.
|
| if (this.oauth_code_) {
|
| this.skipForNow_ = true;
|
| - this.onAuthCompleted_();
|
| + this.maybeCompleteAuth_();
|
| }
|
| };
|
|
|
| + /**
|
| + * The current auth flow of the hosted auth page.
|
| + * @type {AuthFlow}
|
| + */
|
| + cr.defineProperty(Authenticator, 'authFlow');
|
| +
|
| Authenticator.AuthFlow = AuthFlow;
|
| Authenticator.AuthMode = AuthMode;
|
| Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS;
|
|
|