summaryrefslogtreecommitdiff
path: root/chromium/chrome/browser/resources/gaia_auth_host
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@theqtcompany.com>2015-06-18 14:10:49 +0200
committerOswald Buddenhagen <oswald.buddenhagen@theqtcompany.com>2015-06-18 13:53:24 +0000
commit813fbf95af77a531c57a8c497345ad2c61d475b3 (patch)
tree821b2c8de8365f21b6c9ba17a236fb3006a1d506 /chromium/chrome/browser/resources/gaia_auth_host
parentaf6588f8d723931a298c995fa97259bb7f7deb55 (diff)
downloadqtwebengine-chromium-813fbf95af77a531c57a8c497345ad2c61d475b3.tar.gz
BASELINE: Update chromium to 44.0.2403.47
Change-Id: Ie056fedba95cf5e5c76b30c4b2c80fca4764aa2f Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@theqtcompany.com>
Diffstat (limited to 'chromium/chrome/browser/resources/gaia_auth_host')
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/authenticator.js601
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js62
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/post_message_channel.js372
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js453
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/webview_saml_injected.js6
5 files changed, 1368 insertions, 126 deletions
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/authenticator.js b/chromium/chrome/browser/resources/gaia_auth_host/authenticator.js
index 4a1bf5aa7ff..870b368258f 100644
--- a/chromium/chrome/browser/resources/gaia_auth_host/authenticator.js
+++ b/chromium/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,16 +11,24 @@
* 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';
+ // TODO(rogerta): should use gaia URL from GaiaUrls::gaia_url() instead
+ // of hardcoding the prod URL here. As is, this does not work with staging
+ // environments.
var IDP_ORIGIN = 'https://accounts.google.com/';
var IDP_PATH = 'ServiceLogin?skipvpage=true&sarp=1&rm=hide';
var CONTINUE_URL =
'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';
+ var SERVICE_ID = 'chromeoslogin';
+ var EMBEDDED_SETUP_CHROMEOS_ENDPOINT = 'embedded/setup/chromeos';
/**
* The source URL parameter for the constrained signin flow.
@@ -46,79 +56,128 @@ cr.define('cr.login', function() {
};
/**
+ * Supported Authenticator params.
+ * @type {!Array<string>}
+ * @const
+ */
+ var SUPPORTED_PARAMS = [
+ 'gaiaId', // Obfuscated GAIA ID to skip the email prompt page
+ // during the re-auth flow.
+ 'gaiaUrl', // Gaia url to use.
+ 'gaiaPath', // Gaia path to use without a leading slash.
+ 'hl', // Language code for the user interface.
+ 'email', // Pre-fill the email field in Gaia UI.
+ 'service', // Name of Gaia service.
+ 'continueUrl', // Continue url to use.
+ 'frameUrl', // Initial frame URL to use. If empty defaults to
+ // gaiaUrl.
+ 'constrained', // Whether the extension is loaded in a constrained
+ // window.
+ 'clientId', // Chrome client id.
+ 'useEafe', // Whether to use EAFE.
+ 'needPassword', // Whether the host is interested in getting a password.
+ // If this set to |false|, |confirmPasswordCallback| is
+ // not called before dispatching |authCopleted|.
+ // Default is |true|.
+ 'flow', // One of 'default', 'enterprise', or 'theftprotection'.
+ 'enterpriseDomain', // Domain in which hosting device is (or should be)
+ // enrolled.
+ 'emailDomain', // Value used to prefill domain for email.
+ 'clientVersion', // Version of the Chrome build.
+ 'platformVersion', // Version of the OS build.
+ 'releaseChannel', // Installation channel.
+ 'endpointGen', // Current endpoint generation.
+ ];
+
+ /**
* Initializes the authenticator component.
* @param {webview|string} webview The webview element or its ID to host IdP
* web pages.
- * @param {Authenticator.Listener=} opt_listener An optional listener for
- * authentication events.
* @constructor
- * @extends {cr.EventTarget}
*/
- function Authenticator(webview, opt_listener) {
+ function Authenticator(webview) {
this.webview_ = typeof webview == 'string' ? $(webview) : webview;
assert(this.webview_);
- this.listener_ = opt_listener || null;
-
this.email_ = null;
this.password_ = null;
+ this.gaiaId_ = null,
this.sessionIndex_ = null;
this.chooseWhatToSync_ = false;
this.skipForNow_ = false;
- this.authFlow_ = AuthFlow.DEFAULT;
+ this.authFlow = AuthFlow.DEFAULT;
+ this.authDomain = '';
this.loaded_ = false;
this.idpOrigin_ = null;
this.continueUrl_ = null;
this.continueUrlWithoutParams_ = null;
this.initialFrameUrl_ = null;
this.reloadUrl_ = null;
+ this.trusted_ = true;
+ this.oauth_code_ = null;
+
+ this.useEafe_ = false;
+ this.clientId_ = 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));
+
+ this.webview_.addEventListener('droplink', this.onDropLink_.bind(this));
+ this.webview_.addEventListener(
+ 'newwindow', this.onNewWindow_.bind(this));
+ this.webview_.addEventListener(
+ 'contentload', this.onContentLoad_.bind(this));
+ this.webview_.addEventListener(
+ 'loadabort', this.onLoadAbort_.bind(this));
+ this.webview_.addEventListener(
+ 'loadstop', this.onLoadStop_.bind(this));
+ this.webview_.addEventListener(
+ 'loadcommit', this.onLoadCommit_.bind(this));
+ this.webview_.request.onCompleted.addListener(
+ this.onRequestCompleted_.bind(this),
+ {urls: ['<all_urls>'], types: ['main_frame']},
+ ['responseHeaders']);
+ this.webview_.request.onHeadersReceived.addListener(
+ this.onHeadersReceived_.bind(this),
+ {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
+ ['responseHeaders']);
+ window.addEventListener(
+ 'message', this.onMessageFromWebview_.bind(this), false);
+ window.addEventListener(
+ 'focus', this.onFocus_.bind(this), false);
+ window.addEventListener(
+ '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);
/**
- * An interface for receiving notifications upon authentication events.
- * @interface
- */
- Authenticator.Listener = function() {};
-
- /**
- * Invoked when authentication UI is ready.
- */
- Authenticator.Listener.prototype.onReady = function(e) {};
-
- /**
- * Invoked when authentication is completed successfully with credential data.
- * A credential data object looks like this:
- * <pre>
- * {@code
- * {
- * email: 'xx@gmail.com',
- * password: 'xxxx', // May be null or empty.
- * usingSAML: false,
- * chooseWhatToSync: false,
- * skipForNow: false,
- * sessionIndex: '0'
- * }
- * }
- * </pre>
- * @param {Object} credentials A credential data object.
- */
- Authenticator.Listener.prototype.onSuccess = function(credentials) {};
-
- /**
- * Invoked when the requested URL does not fit the container.
- * @param {string} url Request URL.
- */
- Authenticator.Listener.prototype.onResize = function(url) {};
-
- /**
- * Invoked when a new window event is fired.
- * @param {Event} e Event object.
+ * Reinitializes authentication parameters so that a failed login attempt
+ * would not result in an infinite loop.
*/
- Authenticator.Listener.prototype.onNewWindow = function(e) {};
+ Authenticator.prototype.clearCredentials_ = function() {
+ this.email_ = null;
+ this.gaiaId_ = null;
+ this.password_ = null;
+ this.oauth_code_ = null;
+ this.chooseWhatToSync_ = false;
+ this.skipForNow_ = false;
+ this.sessionIndex_ = null;
+ this.trusted_ = true;
+ this.authFlow = AuthFlow.DEFAULT;
+ this.samlHandler_.reset();
+ };
/**
* Loads the authenticator component with the given parameters.
@@ -126,52 +185,84 @@ cr.define('cr.login', function() {
* @param {Object} data Parameters for the authorization flow.
*/
Authenticator.prototype.load = function(authMode, data) {
+ this.authMode = authMode;
+ this.clearCredentials_();
+ this.loaded_ = false;
this.idpOrigin_ = data.gaiaUrl || IDP_ORIGIN;
this.continueUrl_ = data.continueUrl || CONTINUE_URL;
this.continueUrlWithoutParams_ =
this.continueUrl_.substring(0, this.continueUrl_.indexOf('?')) ||
this.continueUrl_;
this.isConstrainedWindow_ = data.constrained == '1';
+ this.isNewGaiaFlowChromeOS = data.isNewGaiaFlowChromeOS;
+ this.useEafe_ = data.useEafe || false;
+ this.clientId_ = data.clientId;
this.initialFrameUrl_ = this.constructInitialFrameUrl_(data);
this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_;
- this.authFlow_ = AuthFlow.DEFAULT;
+ // 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.needPassword = !('needPassword' in data) || data.needPassword;
+
+ if (this.isNewGaiaFlowChromeOS) {
+ this.webview_.contextMenus.onShow.addListener(function(e) {
+ e.preventDefault();
+ });
+ }
this.webview_.src = this.reloadUrl_;
- this.webview_.addEventListener(
- 'newwindow', this.onNewWindow_.bind(this));
- this.webview_.request.onCompleted.addListener(
- this.onRequestCompleted_.bind(this),
- {urls: ['*://*/*', this.continueUrlWithoutParams_ + '*'],
- types: ['main_frame']},
- ['responseHeaders']);
- this.webview_.request.onHeadersReceived.addListener(
- this.onHeadersReceived_.bind(this),
- {urls: [this.idpOrigin_ + '*'], types: ['main_frame']},
- ['responseHeaders']);
- window.addEventListener(
- 'message', this.onMessage_.bind(this), false);
};
/**
* Reloads the authenticator component.
*/
Authenticator.prototype.reload = function() {
+ this.clearCredentials_();
+ this.loaded_ = false;
this.webview_.src = this.reloadUrl_;
- this.authFlow_ = AuthFlow.DEFAULT;
};
Authenticator.prototype.constructInitialFrameUrl_ = function(data) {
- var url = this.idpOrigin_ + (data.gaiaPath || IDP_PATH);
-
- url = appendParam(url, 'continue', this.continueUrl_);
- url = appendParam(url, 'service', data.service);
+ var path = data.gaiaPath;
+ if (!path && this.isNewGaiaFlowChromeOS)
+ path = EMBEDDED_SETUP_CHROMEOS_ENDPOINT;
+ if (!path)
+ path = IDP_PATH;
+ var url = this.idpOrigin_ + path;
+
+ if (this.isNewGaiaFlowChromeOS) {
+ if (data.chromeType)
+ url = appendParam(url, 'chrometype', data.chromeType);
+ if (data.clientId)
+ url = appendParam(url, 'client_id', data.clientId);
+ if (data.enterpriseDomain)
+ url = appendParam(url, 'manageddomain', data.enterpriseDomain);
+ if (data.clientVersion)
+ url = appendParam(url, 'client_version', data.clientVersion);
+ if (data.platformVersion)
+ url = appendParam(url, 'platform_version', data.platformVersion);
+ if (data.releaseChannel)
+ url = appendParam(url, 'release_channel', data.releaseChannel);
+ if (data.endpointGen)
+ url = appendParam(url, 'endpoint_gen', data.endpointGen);
+ } else {
+ url = appendParam(url, 'continue', this.continueUrl_);
+ url = appendParam(url, 'service', data.service || SERVICE_ID);
+ }
if (data.hl)
url = appendParam(url, 'hl', data.hl);
+ if (data.gaiaId)
+ url = appendParam(url, 'user_id', data.gaiaId);
if (data.email)
url = appendParam(url, 'Email', data.email);
if (this.isConstrainedWindow_)
url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE);
+ if (data.flow)
+ url = appendParam(url, 'flow', data.flow);
+ if (data.emailDomain)
+ url = appendParam(url, 'emaildomain', data.emailDomain);
return url;
};
@@ -181,14 +272,18 @@ cr.define('cr.login', function() {
*/
Authenticator.prototype.onRequestCompleted_ = function(details) {
var currentUrl = details.url;
+
if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
- if (currentUrl.indexOf('ntp=1') >= 0) {
+ if (currentUrl.indexOf('ntp=1') >= 0)
this.skipForNow_ = true;
- }
- this.onAuthCompleted_();
+
+ this.maybeCompleteAuth_();
return;
}
+ if (currentUrl.indexOf('https') != 0)
+ this.trusted_ = false;
+
if (this.isConstrainedWindow_) {
var isEmbeddedPage = false;
if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
@@ -200,22 +295,47 @@ cr.define('cr.login', function() {
}
}
}
- if (!isEmbeddedPage && this.listener_) {
- this.listener_.onResize(currentUrl);
+ if (!isEmbeddedPage) {
+ this.dispatchEvent(new CustomEvent('resize', {detail: currentUrl}));
return;
}
}
- if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
- this.webview_.contentWindow.postMessage({}, currentUrl);
- }
+ this.updateHistoryState_(currentUrl);
+ };
- if (!this.loaded_) {
- this.loaded_ = true;
- if (this.listener_) {
- this.listener_.onReady();
- }
- }
+ /**
+ * Manually updates the history. Invoked upon completion of a webview
+ * navigation.
+ * @param {string} url Request URL.
+ * @private
+ */
+ Authenticator.prototype.updateHistoryState_ = function(url) {
+ if (history.state && history.state.url != url)
+ history.pushState({url: url}, '');
+ else
+ history.replaceState({url: url}, '');
+ };
+
+ /**
+ * Invoked when the sign-in page takes focus.
+ * @param {object} e The focus event being triggered.
+ * @private
+ */
+ Authenticator.prototype.onFocus_ = function(e) {
+ if (this.authMode == AuthMode.DESKTOP)
+ this.webview_.focus();
+ };
+
+ /**
+ * Invoked when the history state is changed.
+ * @param {object} e The popstate event being triggered.
+ * @private
+ */
+ Authenticator.prototype.onPopState_ = function(e) {
+ var state = e.state;
+ if (state && state.url)
+ this.webview_.src = state.url;
};
/**
@@ -226,6 +346,10 @@ cr.define('cr.login', function() {
* @private
*/
Authenticator.prototype.onHeadersReceived_ = function(details) {
+ var currentUrl = details.url;
+ if (currentUrl.lastIndexOf(this.idpOrigin_, 0) != 0)
+ return;
+
var headers = details.responseHeaders;
for (var i = 0; headers && i < headers.length; ++i) {
var header = headers[i];
@@ -238,58 +362,220 @@ cr.define('cr.login', function() {
signinDetails[pair[0].trim()] = pair[1].trim();
});
// Removes "" around.
- var email = signinDetails['email'].slice(1, -1);
- if (this.email_ != email) {
- this.email_ = email;
- // Clears the scraped password if the email has changed.
- this.password_ = null;
- }
+ 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.
+ var location = decodeURIComponent(header.value);
+ this.chooseWhatToSync_ = !!location.match(/(\?|&)source=3($|&)/);
+ } else if (
+ this.isNewGaiaFlowChromeOS && headerName == SET_COOKIE_HEADER) {
+ var headerValue = header.value;
+ if (headerValue.indexOf(OAUTH_CODE_COOKIE + '=', 0) == 0) {
+ this.oauth_code_ =
+ headerValue.substring(OAUTH_CODE_COOKIE.length + 1).split(';')[0];
+ }
}
}
};
/**
- * Invoked when an HTML5 message is received.
+ * Returns true if given HTML5 message is received from the webview element.
* @param {object} e Payload of the received HTML5 message.
- * @private
*/
- Authenticator.prototype.onMessage_ = function(e) {
- if (e.origin != this.idpOrigin_) {
+ Authenticator.prototype.isGaiaMessage = function(e) {
+ if (!this.isWebviewEvent_(e))
+ return false;
+
+ // The event origin does not have a trailing slash.
+ if (e.origin != this.idpOrigin_.substring(0, this.idpOrigin_.length - 1)) {
+ return false;
+ }
+
+ // EAFE passes back auth code via message.
+ if (this.useEafe_ &&
+ typeof e.data == 'object' &&
+ e.data.hasOwnProperty('authorizationCode')) {
+ assert(!this.oauth_code_);
+ this.oauth_code_ = e.data.authorizationCode;
+ this.dispatchEvent(
+ new CustomEvent('authCompleted',
+ {
+ detail: {
+ authCodeOnly: true,
+ authCode: this.oauth_code_
+ }
+ }));
return;
}
- var msg = e.data;
+ // Gaia messages must be an object with 'method' property.
+ if (typeof e.data != 'object' || !e.data.hasOwnProperty('method')) {
+ return false;
+ }
+ return true;
+ };
+
+ /**
+ * Invoked when an HTML5 message is received from the webview element.
+ * @param {object} e Payload of the received HTML5 message.
+ * @private
+ */
+ Authenticator.prototype.onMessageFromWebview_ = function(e) {
+ if (!this.isGaiaMessage(e))
+ return;
+ var msg = e.data;
if (msg.method == 'attemptLogin') {
this.email_ = msg.email;
this.password_ = msg.password;
this.chooseWhatToSync_ = msg.chooseWhatToSync;
+ // We need to dispatch only first event, before user enters password.
+ if (!msg.password) {
+ this.dispatchEvent(
+ new CustomEvent('attemptLogin', {detail: msg.email}));
+ }
+ } else if (msg.method == 'dialogShown') {
+ this.dispatchEvent(new Event('dialogShown'));
+ } else if (msg.method == 'dialogHidden') {
+ this.dispatchEvent(new Event('dialogHidden'));
+ } else if (msg.method == 'backButton') {
+ this.dispatchEvent(new CustomEvent('backButton', {detail: msg.show}));
+ } else if (msg.method == 'showView') {
+ this.dispatchEvent(new Event('showView'));
+ } else {
+ console.warn('Unrecognized message from GAIA: ' + msg.method);
}
};
/**
- * Invoked to process authentication completion.
- * @private
+ * Invoked by the hosting page to verify the Saml password.
*/
- Authenticator.prototype.onAuthCompleted_ = function() {
- if (!this.listener_) {
+ 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.email_,
+ this.samlHandler_.scrapedPasswordCount);
+ }).bind(this);
+ window.setTimeout(invokeConfirmPassword, 0);
return;
}
- if (!this.email_ && !this.skipForNow_) {
+ this.password_ = password;
+ this.onAuthCompleted_();
+ };
+
+ /**
+ * Check Saml flow and start password confirmation flow if needed. Otherwise,
+ * continue with auto completion.
+ * @private
+ */
+ 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;
}
- this.listener_.onSuccess({email: this.email_,
- password: this.password_,
- usingSAML: this.authFlow_ == AuthFlow.SAML,
- chooseWhatToSync: this.chooseWhatToSync_,
- skipForNow: this.skipForNow_,
- sessionIndex: this.sessionIndex_ || ''});
+ 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_);
+ return;
+ }
+
+ // Fall through to finish the auth flow even if this.needPassword
+ // is true. This is because the flag is used as an intention to get
+ // password when it is available but not a mandatory requirement.
+ console.warn('Authenticator: No password scraped for SAML.');
+ } else if (this.needPassword) {
+ if (this.confirmPasswordCallback) {
+ // Confirm scraped password. The flow follows in
+ // verifyConfirmedPassword.
+ this.confirmPasswordCallback(this.email_,
+ 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.
+ {
+ detail: {
+ email: this.email_ || '',
+ gaiaId: this.gaiaId_ || '',
+ password: this.password_ || '',
+ authCode: this.oauth_code_,
+ usingSAML: this.authFlow == AuthFlow.SAML,
+ chooseWhatToSync: this.chooseWhatToSync_,
+ skipForNow: this.skipForNow_,
+ sessionIndex: this.sessionIndex_ || '',
+ trusted: this.trusted_
+ }
+ }));
+ this.clearCredentials_();
+ };
+
+ /**
+ * 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;
+
+ this.authDomain = this.samlHandler_.authDomain;
+ this.authFlow = AuthFlow.SAML;
+ };
+
+ /**
+ * Invoked when a link is dropped on the webview.
+ * @private
+ */
+ Authenticator.prototype.onDropLink_ = function(e) {
+ this.dispatchEvent(new CustomEvent('dropLink', {detail: e.url}));
};
/**
@@ -297,19 +583,114 @@ cr.define('cr.login', function() {
* @private
*/
Authenticator.prototype.onNewWindow_ = function(e) {
- if (!this.listener_) {
- return;
+ this.dispatchEvent(new CustomEvent('newWindow', {detail: e}));
+ };
+
+ /**
+ * Invoked when a new document is loaded.
+ * @private
+ */
+ Authenticator.prototype.onContentLoad_ = function(e) {
+ if (this.isConstrainedWindow_) {
+ // Signin content in constrained windows should not zoom. Isolate the
+ // webview from the zooming of other webviews using the 'per-view' zoom
+ // mode, and then set it to 100% zoom.
+ this.webview_.setZoomMode('per-view');
+ this.webview_.setZoom(1);
+ }
+
+ // Posts a message to IdP pages to initiate communication.
+ var currentUrl = this.webview_.src;
+ if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
+ var msg = {
+ 'method': 'handshake',
+ };
+
+ this.webview_.contentWindow.postMessage(msg, currentUrl);
+ }
+ };
+
+ /**
+ * Invoked when the webview fails loading a page.
+ * @private
+ */
+ Authenticator.prototype.onLoadAbort_ = function(e) {
+ this.dispatchEvent(new CustomEvent('loadAbort',
+ {detail: {error: e.reason,
+ src: this.webview_.src}}));
+ };
+
+ /**
+ * Invoked when the webview finishes loading a page.
+ * @private
+ */
+ Authenticator.prototype.onLoadStop_ = function(e) {
+ if (!this.loaded_) {
+ this.loaded_ = true;
+ this.dispatchEvent(new Event('ready'));
+ // Focus webview after dispatching event when webview is already visible.
+ this.webview_.focus();
+ }
+
+ // Sends client id to EAFE on every loadstop after a small timeout. This is
+ // needed because EAFE sits behind SSO and initialize asynchrounouly
+ // and we don't know for sure when it is loaded and ready to listen
+ // for message. The postMessage is guarded by EAFE's origin.
+ if (this.useEafe_) {
+ // An arbitrary small timeout for delivering the initial message.
+ var EAFE_INITIAL_MESSAGE_DELAY_IN_MS = 500;
+ window.setTimeout((function() {
+ var msg = {
+ 'clientId': this.clientId_
+ };
+ this.webview_.contentWindow.postMessage(msg, this.idpOrigin_);
+ }).bind(this), EAFE_INITIAL_MESSAGE_DELAY_IN_MS);
}
+ };
- this.listener_.onNewWindow(e);
+ /**
+ * Invoked when the webview navigates withing the current document.
+ * @private
+ */
+ Authenticator.prototype.onLoadCommit_ = function(e) {
+ if (this.oauth_code_) {
+ this.skipForNow_ = true;
+ this.maybeCompleteAuth_();
+ }
};
+ /**
+ * Returns |true| if event |e| was sent from the hosted webview.
+ * @private
+ */
+ Authenticator.prototype.isWebviewEvent_ = function(e) {
+ // Note: <webview> prints error message to console if |contentWindow| is not
+ // defined.
+ // TODO(dzhioev): remove the message. http://crbug.com/469522
+ var webviewWindow = this.webview_.contentWindow;
+ return !!webviewWindow && webviewWindow === e.source;
+ };
+
+ /**
+ * The current auth flow of the hosted auth page.
+ * @type {AuthFlow}
+ */
+ cr.defineProperty(Authenticator, 'authFlow');
+
+ /**
+ * The domain name of the current auth page.
+ * @type {string}
+ */
+ cr.defineProperty(Authenticator, 'authDomain');
+
Authenticator.AuthFlow = AuthFlow;
Authenticator.AuthMode = AuthMode;
+ Authenticator.SUPPORTED_PARAMS = SUPPORTED_PARAMS;
return {
// TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old
// iframe-based flow is deprecated.
- GaiaAuthHost: Authenticator
+ GaiaAuthHost: Authenticator,
+ Authenticator: Authenticator
};
});
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js b/chromium/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js
index cf870cb53ba..e568fc7d81e 100644
--- a/chromium/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js
+++ b/chromium/chrome/browser/resources/gaia_auth_host/gaia_auth_host.js
@@ -43,7 +43,7 @@ cr.define('cr.login', function() {
/**
* Supported params of auth extension. For a complete list, check out the
* auth extension's main.js.
- * @type {!Array.<string>}
+ * @type {!Array<string>}
* @const
*/
var SUPPORTED_PARAMS = [
@@ -54,13 +54,15 @@ cr.define('cr.login', function() {
'service', // Name of Gaia service;
'continueUrl', // Continue url to use;
'frameUrl', // Initial frame URL to use. If empty defaults to gaiaUrl.
+ 'useEafe', // Whether to use EAFE.
+ 'clientId', // Chrome's client id.
'constrained' // Whether the extension is loaded in a constrained window;
];
/**
* Supported localized strings. For a complete list, check out the auth
* extension's offline.js
- * @type {!Array.<string>}
+ * @type {!Array<string>}
* @const
*/
var LOCALIZED_STRING_PARAMS = [
@@ -110,6 +112,12 @@ cr.define('cr.login', function() {
__proto__: cr.EventTarget.prototype,
/**
+ * Auth extension params
+ * @type {Object}
+ */
+ authParams_: {},
+
+ /**
* An url to use with {@code reload}.
* @type {?string}
* @private
@@ -117,12 +125,6 @@ cr.define('cr.login', function() {
reloadUrl_: null,
/**
- * The domain name of the current auth page.
- * @type {string}
- */
- authDomain: '',
-
- /**
* Invoked when authentication is completed successfully with credential
* data. A credential data object looks like this:
* <pre>
@@ -237,7 +239,7 @@ cr.define('cr.login', function() {
* invoked with a credential object.
*/
load: function(authMode, data, successCallback) {
- var params = [];
+ var params = {};
var populateParams = function(nameList, values) {
if (!values)
@@ -246,13 +248,13 @@ cr.define('cr.login', function() {
for (var i in nameList) {
var name = nameList[i];
if (values[name])
- params.push(name + '=' + encodeURIComponent(values[name]));
+ params[name] = values[name];
}
};
populateParams(SUPPORTED_PARAMS, data);
populateParams(LOCALIZED_STRING_PARAMS, data.localizedStrings);
- params.push('parentPage=' + encodeURIComponent(window.location.origin));
+ params['needPassword'] = true;
var url;
switch (authMode) {
@@ -261,17 +263,17 @@ cr.define('cr.login', function() {
break;
case AuthMode.DESKTOP:
url = AUTH_URL;
- params.push('desktopMode=1');
+ params['desktopMode'] = true;
break;
default:
url = AUTH_URL;
}
- url += '?' + params.join('&');
- this.frame_.src = url;
+ this.authParams_ = params;
this.reloadUrl_ = url;
this.successCallback_ = successCallback;
- this.authFlow = AuthFlow.GAIA;
+
+ this.reload();
},
/**
@@ -328,6 +330,11 @@ cr.define('cr.login', function() {
if (!this.isAuthExtMessage_(e))
return;
+ if (msg.method == 'loginUIDOMContentLoaded') {
+ this.frame_.contentWindow.postMessage(this.authParams_, AUTH_URL_BASE);
+ return;
+ }
+
if (msg.method == 'loginUILoaded') {
cr.dispatchSimpleEvent(this, 'ready');
return;
@@ -350,9 +357,21 @@ cr.define('cr.login', function() {
return;
}
+ if (msg.method == 'completeAuthenticationAuthCodeOnly') {
+ if (!msg.authCode) {
+ console.error(
+ 'GaiaAuthHost: completeAuthentication without auth code.');
+ var msg = {method: 'redirectToSignin'};
+ this.frame_.contentWindow.postMessage(msg, AUTH_URL_BASE);
+ return;
+ }
+ this.onAuthSuccess_({authCodeOnly: true, authCode: msg.authCode});
+ return;
+ }
+
if (msg.method == 'confirmPassword') {
if (this.confirmPasswordCallback_)
- this.confirmPasswordCallback_(msg.passwordCount);
+ this.confirmPasswordCallback_(msg.email, msg.passwordCount);
else
console.error('GaiaAuthHost: Invalid confirmPasswordCallback_.');
return;
@@ -372,6 +391,11 @@ cr.define('cr.login', function() {
return;
}
+ if (msg.method == 'resetAuthFlow') {
+ this.authFlow = AuthFlow.GAIA;
+ return;
+ }
+
if (msg.method == 'insecureContentBlocked') {
if (this.insecureContentBlockedCallback_) {
this.insecureContentBlockedCallback_(msg.url);
@@ -410,6 +434,12 @@ cr.define('cr.login', function() {
};
/**
+ * The domain name of the current auth page.
+ * @type {string}
+ */
+ cr.defineProperty(GaiaAuthHost, 'authDomain');
+
+ /**
* The current auth flow of the hosted gaia_auth extension.
* @type {AuthFlow}
*/
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/post_message_channel.js b/chromium/chrome/browser/resources/gaia_auth_host/post_message_channel.js
new file mode 100644
index 00000000000..b63f93b2039
--- /dev/null
+++ b/chromium/chrome/browser/resources/gaia_auth_host/post_message_channel.js
@@ -0,0 +1,372 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview
+ * Provides a HTML5 postMessage channel to the injected JS to talk back
+ * to Authenticator.
+ */
+'use strict';
+
+<include src="../gaia_auth/channel.js">
+
+var PostMessageChannel = (function() {
+ /**
+ * Allowed origins of the hosting page.
+ * @type {Array.<string>}
+ */
+ var ALLOWED_ORIGINS = [
+ 'chrome://oobe',
+ 'chrome://chrome-signin'
+ ];
+
+ /** @const */
+ var PORT_MESSAGE = 'post-message-port-message';
+
+ /** @const */
+ var CHANNEL_INIT_MESSAGE = 'post-message-channel-init';
+
+ /** @const */
+ var CHANNEL_CONNECT_MESSAGE = 'post-message-channel-connect';
+
+ /**
+ * Whether the script runs in a top level window.
+ */
+ function isTopLevel() {
+ return window === window.top;
+ }
+
+ /**
+ * A simple event target.
+ */
+ function EventTarget() {
+ this.listeners_ = [];
+ }
+
+ EventTarget.prototype = {
+ /**
+ * Add an event listener.
+ */
+ addListener: function(listener) {
+ this.listeners_.push(listener);
+ },
+
+ /**
+ * Dispatches a given event to all listeners.
+ */
+ dispatch: function(e) {
+ for (var i = 0; i < this.listeners_.length; ++i) {
+ this.listeners_[i].call(undefined, e);
+ }
+ }
+ };
+
+ /**
+ * ChannelManager handles window message events by dispatching them to
+ * PostMessagePorts or forwarding to other windows (up/down the hierarchy).
+ * @constructor
+ */
+ function ChannelManager() {
+ /**
+ * Window and origin to forward message up the hierarchy. For subframes,
+ * they defaults to window.parent and any origin. For top level window,
+ * this would be set to the hosting webview on CHANNEL_INIT_MESSAGE.
+ */
+ this.upperWindow = isTopLevel() ? null : window.parent;
+ this.upperOrigin = isTopLevel() ? '' : '*';
+
+ /**
+ * Channle Id to port map.
+ * @type {Object.<number, PostMessagePort>}
+ */
+ this.channels_ = {};
+
+ /**
+ * Deferred messages to be posted to |upperWindow|.
+ * @type {Array}
+ */
+ this.deferredUpperWindowMessages_ = [];
+
+ /**
+ * Ports that depend on upperWindow and need to be setup when its available.
+ */
+ this.deferredUpperWindowPorts_ = [];
+
+ /**
+ * Whether the ChannelManager runs in daemon mode and accepts connections.
+ */
+ this.isDaemon = false;
+
+ /**
+ * Fires when ChannelManager is in listening mode and a
+ * CHANNEL_CONNECT_MESSAGE is received.
+ */
+ this.onConnect = new EventTarget();
+
+ window.addEventListener('message', this.onMessage_.bind(this));
+ }
+
+ ChannelManager.prototype = {
+ /**
+ * Gets a global unique id to use.
+ * @return {number}
+ */
+ createChannelId_: function() {
+ return (new Date()).getTime();
+ },
+
+ /**
+ * Posts data to upperWindow. Queue it if upperWindow is not available.
+ */
+ postToUpperWindow: function(data) {
+ if (this.upperWindow == null) {
+ this.deferredUpperWindowMessages_.push(data);
+ return;
+ }
+
+ this.upperWindow.postMessage(data, this.upperOrigin);
+ },
+
+ /**
+ * Creates a port and register it in |channels_|.
+ * @param {number} channelId
+ * @param {string} channelName
+ * @param {DOMWindow=} opt_targetWindow
+ * @param {string=} opt_targetOrigin
+ */
+ createPort: function(
+ channelId, channelName, opt_targetWindow, opt_targetOrigin) {
+ var port = new PostMessagePort(channelId, channelName);
+ if (opt_targetWindow)
+ port.setTarget(opt_targetWindow, opt_targetOrigin);
+ this.channels_[channelId] = port;
+ return port;
+ },
+
+ /*
+ * Returns a message forward handler for the given proxy port.
+ * @private
+ */
+ getProxyPortForwardHandler_: function(proxyPort) {
+ return function(msg) { proxyPort.postMessage(msg); };
+ },
+
+ /**
+ * Creates a forwarding porxy port.
+ * @param {number} channelId
+ * @param {string} channelName
+ * @param {!DOMWindow} targetWindow
+ * @param {!string} targetOrigin
+ */
+ createProxyPort: function(
+ channelId, channelName, targetWindow, targetOrigin) {
+ var port = this.createPort(
+ channelId, channelName, targetWindow, targetOrigin);
+ port.onMessage.addListener(this.getProxyPortForwardHandler_(port));
+ return port;
+ },
+
+ /**
+ * Creates a connecting port to the daemon and request connection.
+ * @param {string} name
+ * @return {PostMessagePort}
+ */
+ connectToDaemon: function(name) {
+ if (this.isDaemon) {
+ console.error(
+ 'Error: Connecting from the daemon page is not supported.');
+ return;
+ }
+
+ var port = this.createPort(this.createChannelId_(), name);
+ if (this.upperWindow) {
+ port.setTarget(this.upperWindow, this.upperOrigin);
+ } else {
+ this.deferredUpperWindowPorts_.push(port);
+ }
+
+ this.postToUpperWindow({
+ type: CHANNEL_CONNECT_MESSAGE,
+ channelId: port.channelId,
+ channelName: port.name
+ });
+ return port;
+ },
+
+ /**
+ * Dispatches a 'message' event to port.
+ * @private
+ */
+ dispatchMessageToPort_: function(e) {
+ var channelId = e.data.channelId;
+ var port = this.channels_[channelId];
+ if (!port) {
+ console.error('Error: Unable to dispatch message. Unknown channel.');
+ return;
+ }
+
+ port.handleWindowMessage(e);
+ },
+
+ /**
+ * Window 'message' handler.
+ */
+ onMessage_: function(e) {
+ if (typeof e.data != 'object' ||
+ !e.data.hasOwnProperty('type')) {
+ return;
+ }
+
+ if (e.data.type === PORT_MESSAGE) {
+ // Dispatch port message to ports if this is the daemon page or
+ // the message is from upperWindow. In case of null upperWindow,
+ // the message is assumed to be forwarded to upperWindow and queued.
+ if (this.isDaemon ||
+ (this.upperWindow && e.source === this.upperWindow)) {
+ this.dispatchMessageToPort_(e);
+ } else {
+ this.postToUpperWindow(e.data);
+ }
+ } else if (e.data.type === CHANNEL_CONNECT_MESSAGE) {
+ var channelId = e.data.channelId;
+ var channelName = e.data.channelName;
+
+ if (this.isDaemon) {
+ var port = this.createPort(
+ channelId, channelName, e.source, e.origin);
+ this.onConnect.dispatch(port);
+ } else {
+ this.createProxyPort(channelId, channelName, e.source, e.origin);
+ this.postToUpperWindow(e.data);
+ }
+ } else if (e.data.type === CHANNEL_INIT_MESSAGE) {
+ if (ALLOWED_ORIGINS.indexOf(e.origin) == -1)
+ return;
+
+ this.upperWindow = e.source;
+ this.upperOrigin = e.origin;
+
+ for (var i = 0; i < this.deferredUpperWindowMessages_.length; ++i) {
+ this.upperWindow.postMessage(this.deferredUpperWindowMessages_[i],
+ this.upperOrigin);
+ }
+ this.deferredUpperWindowMessages_ = [];
+
+ for (var i = 0; i < this.deferredUpperWindowPorts_.length; ++i) {
+ this.deferredUpperWindowPorts_[i].setTarget(this.upperWindow,
+ this.upperOrigin);
+ }
+ this.deferredUpperWindowPorts_ = [];
+ }
+ }
+ };
+
+ /**
+ * Singleton instance of ChannelManager.
+ * @type {ChannelManager}
+ */
+ var channelManager = new ChannelManager();
+
+ /**
+ * A HTML5 postMessage based port that provides the same port interface
+ * as the messaging API port.
+ * @param {number} channelId
+ * @param {string} name
+ */
+ function PostMessagePort(channelId, name) {
+ this.channelId = channelId;
+ this.name = name;
+ this.targetWindow = null;
+ this.targetOrigin = '';
+ this.deferredMessages_ = [];
+
+ this.onMessage = new EventTarget();
+ };
+
+ PostMessagePort.prototype = {
+ /**
+ * Sets the target window and origin.
+ * @param {DOMWindow} targetWindow
+ * @param {string} targetOrigin
+ */
+ setTarget: function(targetWindow, targetOrigin) {
+ this.targetWindow = targetWindow;
+ this.targetOrigin = targetOrigin;
+
+ for (var i = 0; i < this.deferredMessages_.length; ++i) {
+ this.postMessage(this.deferredMessages_[i]);
+ }
+ this.deferredMessages_ = [];
+ },
+
+ postMessage: function(msg) {
+ if (!this.targetWindow) {
+ this.deferredMessages_.push(msg);
+ return;
+ }
+
+ this.targetWindow.postMessage({
+ type: PORT_MESSAGE,
+ channelId: this.channelId,
+ payload: msg
+ }, this.targetOrigin);
+ },
+
+ handleWindowMessage: function(e) {
+ this.onMessage.dispatch(e.data.payload);
+ }
+ };
+
+ /**
+ * A message channel based on PostMessagePort.
+ * @extends {Channel}
+ * @constructor
+ */
+ function PostMessageChannel() {
+ };
+
+ PostMessageChannel.prototype = {
+ __proto__: Channel.prototype,
+
+ /** @override */
+ connect: function(name) {
+ this.port_ = channelManager.connectToDaemon(name);
+ this.port_.onMessage.addListener(this.onMessage_.bind(this));
+ },
+ };
+
+ /**
+ * Initialize webview content window for postMessage channel.
+ * @param {DOMWindow} webViewContentWindow Content window of the webview.
+ */
+ PostMessageChannel.init = function(webViewContentWindow) {
+ webViewContentWindow.postMessage({
+ type: CHANNEL_INIT_MESSAGE
+ }, '*');
+ };
+
+ /**
+ * Run in daemon mode and listen for incoming connections. Note that the
+ * current implementation assumes the daemon runs in the hosting page
+ * at the upper layer of the DOM tree. That is, all connect requests go
+ * up the DOM tree instead of going into sub frames.
+ * @param {function(PostMessagePort)} callback Invoked when a connection is
+ * made.
+ */
+ PostMessageChannel.runAsDaemon = function(callback) {
+ channelManager.isDaemon = true;
+
+ var onConnect = function(port) {
+ callback(port);
+ };
+ channelManager.onConnect.addListener(onConnect);
+ };
+
+ return PostMessageChannel;
+})();
+
+/** @override */
+Channel.create = function() {
+ return new PostMessageChannel();
+};
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js b/chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js
new file mode 100644
index 00000000000..bd8603613d3
--- /dev/null
+++ b/chromium/chrome/browser/resources/gaia_auth_host/saml_handler.js
@@ -0,0 +1,453 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+<include src="post_message_channel.js">
+
+/**
+ * @fileoverview Saml support for webview based auth.
+ */
+
+cr.define('cr.login', function() {
+ 'use strict';
+
+ /**
+ * The lowest version of the credentials passing API supported.
+ * @type {number}
+ */
+ var MIN_API_VERSION_VERSION = 1;
+
+ /**
+ * The highest version of the credentials passing API supported.
+ * @type {number}
+ */
+ var MAX_API_VERSION_VERSION = 1;
+
+ /**
+ * The key types supported by the credentials passing API.
+ * @type {Array} Array of strings.
+ */
+ var API_KEY_TYPES = [
+ 'KEY_TYPE_PASSWORD_PLAIN',
+ ];
+
+ /** @const */
+ var SAML_HEADER = 'google-accounts-saml';
+
+ /**
+ * The script to inject into webview and its sub frames.
+ * @type {string}
+ */
+ var injectedJs = String.raw`
+ <include src="webview_saml_injected.js">
+ `;
+
+ /**
+ * Creates a new URL by striping all query parameters.
+ * @param {string} url The original URL.
+ * @return {string} The new URL with all query parameters stripped.
+ */
+ function stripParams(url) {
+ return url.substring(0, url.indexOf('?')) || url;
+ }
+
+ /**
+ * Extract domain name from an URL.
+ * @param {string} url An URL string.
+ * @return {string} The host name of the URL.
+ */
+ function extractDomain(url) {
+ var a = document.createElement('a');
+ a.href = url;
+ return a.hostname;
+ }
+
+ /**
+ * A handler to provide saml support for the given webview that hosts the
+ * auth IdP pages.
+ * @extends {cr.EventTarget}
+ * @param {webview} webview
+ * @constructor
+ */
+ function SamlHandler(webview) {
+ /**
+ * The webview that serves IdP pages.
+ * @type {webview}
+ */
+ this.webview_ = webview;
+
+ /**
+ * Whether a Saml IdP page is display in the webview.
+ * @type {boolean}
+ */
+ this.isSamlPage_ = false;
+
+ /**
+ * Pending Saml IdP page flag that is set when a SAML_HEADER is received
+ * and is copied to |isSamlPage_| in loadcommit.
+ * @type {boolean}
+ */
+ this.pendingIsSamlPage_ = false;
+
+ /**
+ * The last aborted top level url. It is recorded in loadabort event and
+ * used to skip injection into Chrome's error page in the following
+ * loadcommit event.
+ * @type {string}
+ */
+ this.abortedTopLevelUrl_ = null;
+
+ /**
+ * The domain of the Saml IdP.
+ * @type {string}
+ */
+ this.authDomain = '';
+
+ /**
+ * Scraped password stored in an id to password field value map.
+ * @type {Object.<string, string>}
+ * @private
+ */
+ this.passwordStore_ = {};
+
+ /**
+ * Whether Saml API is initialized.
+ * @type {boolean}
+ */
+ this.apiInitialized_ = false;
+
+ /**
+ * Saml API version to use.
+ * @type {number}
+ */
+ this.apiVersion_ = 0;
+
+ /**
+ * Saml API token received.
+ * @type {string}
+ */
+ this.apiToken_ = null;
+
+ /**
+ * Saml API password bytes.
+ * @type {string}
+ */
+ this.apiPasswordBytes_ = null;
+
+ /*
+ * Whether to abort the authentication flow and show an error messagen when
+ * content served over an unencrypted connection is detected.
+ * @type {boolean}
+ */
+ this.blockInsecureContent = false;
+
+ this.webview_.addEventListener(
+ 'contentload', this.onContentLoad_.bind(this));
+ this.webview_.addEventListener(
+ 'loadabort', this.onLoadAbort_.bind(this));
+ this.webview_.addEventListener(
+ 'loadcommit', this.onLoadCommit_.bind(this));
+
+ this.webview_.request.onBeforeRequest.addListener(
+ this.onInsecureRequest.bind(this),
+ {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']},
+ ['blocking']);
+ this.webview_.request.onHeadersReceived.addListener(
+ this.onHeadersReceived_.bind(this),
+ {urls: ['<all_urls>'], types: ['main_frame', 'xmlhttprequest']},
+ ['blocking', 'responseHeaders']);
+
+ this.webview_.addContentScripts([{
+ name: 'samlInjected',
+ matches: ['http://*/*', 'https://*/*'],
+ js: {
+ code: injectedJs
+ },
+ all_frames: true,
+ run_at: 'document_start'
+ }]);
+
+ PostMessageChannel.runAsDaemon(this.onConnected_.bind(this));
+ }
+
+ SamlHandler.prototype = {
+ __proto__: cr.EventTarget.prototype,
+
+ /**
+ * Whether Saml API is used during auth.
+ * @return {boolean}
+ */
+ get samlApiUsed() {
+ return !!this.apiPasswordBytes_;
+ },
+
+ /**
+ * Returns the Saml API password bytes.
+ * @return {string}
+ */
+ get apiPasswordBytes() {
+ return this.apiPasswordBytes_;
+ },
+
+ /**
+ * Returns the number of scraped passwords.
+ * @return {number}
+ */
+ get scrapedPasswordCount() {
+ return this.getConsolidatedScrapedPasswords_().length;
+ },
+
+ /**
+ * Gets the de-duped scraped passwords.
+ * @return {Array.<string>}
+ * @private
+ */
+ getConsolidatedScrapedPasswords_: function() {
+ var passwords = {};
+ for (var property in this.passwordStore_) {
+ passwords[this.passwordStore_[property]] = true;
+ }
+ return Object.keys(passwords);
+ },
+
+ /**
+ * Resets all auth states
+ */
+ reset: function() {
+ this.isSamlPage_ = false;
+ this.pendingIsSamlPage_ = false;
+ this.passwordStore_ = {};
+
+ this.apiInitialized_ = false;
+ this.apiVersion_ = 0;
+ this.apiToken_ = null;
+ this.apiPasswordBytes_ = null;
+ },
+
+ /**
+ * Check whether the given |password| is in the scraped passwords.
+ * @return {boolean} True if the |password| is found.
+ */
+ verifyConfirmedPassword: function(password) {
+ return this.getConsolidatedScrapedPasswords_().indexOf(password) >= 0;
+ },
+
+ /**
+ * Invoked on the webview's contentload event.
+ * @private
+ */
+ onContentLoad_: function(e) {
+ PostMessageChannel.init(this.webview_.contentWindow);
+ },
+
+ /**
+ * Invoked on the webview's loadabort event.
+ * @private
+ */
+ onLoadAbort_: function(e) {
+ if (e.isTopLevel)
+ this.abortedTopLevelUrl_ = e.url;
+ },
+
+ /**
+ * Invoked on the webview's loadcommit event for both main and sub frames.
+ * @private
+ */
+ onLoadCommit_: function(e) {
+ // Skip this loadcommit if the top level load is just aborted.
+ if (e.isTopLevel && e.url === this.abortedTopLevelUrl_) {
+ this.abortedTopLevelUrl_ = null;
+ return;
+ }
+
+ // Skip for none http/https url.
+ if (e.url.indexOf('https://') != 0 &&
+ e.url.indexOf('http://') != 0) {
+ return;
+ }
+
+ this.isSamlPage_ = this.pendingIsSamlPage_;
+ },
+
+ /**
+ * Handler for webRequest.onBeforeRequest, invoked when content served over
+ * an unencrypted connection is detected. Determines whether the request
+ * should be blocked and if so, signals that an error message needs to be
+ * shown.
+ * @param {Object} details
+ * @return {!Object} Decision whether to block the request.
+ */
+ onInsecureRequest: function(details) {
+ if (!this.blockInsecureContent)
+ return {};
+ var strippedUrl = stripParams(details.url);
+ this.dispatchEvent(new CustomEvent('insecureContentBlocked',
+ {detail: {url: strippedUrl}}));
+ return {cancel: true};
+ },
+
+ /**
+ * Invoked when headers are received for the main frame.
+ * @private
+ */
+ onHeadersReceived_: function(details) {
+ var headers = details.responseHeaders;
+
+ // Check whether GAIA headers indicating the start or end of a SAML
+ // redirect are present. If so, synthesize cookies to mark these points.
+ for (var i = 0; headers && i < headers.length; ++i) {
+ var header = headers[i];
+ var headerName = header.name.toLowerCase();
+
+ if (headerName == SAML_HEADER) {
+ var action = header.value.toLowerCase();
+ if (action == 'start') {
+ this.pendingIsSamlPage_ = true;
+
+ // GAIA is redirecting to a SAML IdP. Any cookies contained in the
+ // current |headers| were set by GAIA. Any cookies set in future
+ // requests will be coming from the IdP. Append a cookie to the
+ // current |headers| that marks the point at which the redirect
+ // occurred.
+ headers.push({name: 'Set-Cookie',
+ value: 'google-accounts-saml-start=now'});
+ return {responseHeaders: headers};
+ } else if (action == 'end') {
+ this.pendingIsSamlPage_ = false;
+
+ // The SAML IdP has redirected back to GAIA. Add a cookie that marks
+ // the point at which the redirect occurred occurred. It is
+ // important that this cookie be prepended to the current |headers|
+ // because any cookies contained in the |headers| were already set
+ // by GAIA, not the IdP. Due to limitations in the webRequest API,
+ // it is not trivial to prepend a cookie:
+ //
+ // The webRequest API only allows for deleting and appending
+ // headers. To prepend a cookie (C), three steps are needed:
+ // 1) Delete any headers that set cookies (e.g., A, B).
+ // 2) Append a header which sets the cookie (C).
+ // 3) Append the original headers (A, B).
+ //
+ // Due to a further limitation of the webRequest API, it is not
+ // possible to delete a header in step 1) and append an identical
+ // header in step 3). To work around this, a trailing semicolon is
+ // added to each header before appending it. Trailing semicolons are
+ // ignored by Chrome in cookie headers, causing the modified headers
+ // to actually set the original cookies.
+ var otherHeaders = [];
+ var cookies = [{name: 'Set-Cookie',
+ value: 'google-accounts-saml-end=now'}];
+ for (var j = 0; j < headers.length; ++j) {
+ if (headers[j].name.toLowerCase().indexOf('set-cookie') == 0) {
+ var header = headers[j];
+ header.value += ';';
+ cookies.push(header);
+ } else {
+ otherHeaders.push(headers[j]);
+ }
+ }
+ return {responseHeaders: otherHeaders.concat(cookies)};
+ }
+ }
+ }
+
+ return {};
+ },
+
+ /**
+ * Invoked when the injected JS makes a connection.
+ */
+ onConnected_: function(port) {
+ if (port.targetWindow != this.webview_.contentWindow)
+ return;
+
+ var channel = Channel.create();
+ channel.init(port);
+
+ channel.registerMessage(
+ 'apiCall', this.onAPICall_.bind(this, channel));
+ channel.registerMessage(
+ 'updatePassword', this.onUpdatePassword_.bind(this, channel));
+ channel.registerMessage(
+ 'pageLoaded', this.onPageLoaded_.bind(this, channel));
+ channel.registerMessage(
+ 'getSAMLFlag', this.onGetSAMLFlag_.bind(this, channel));
+ },
+
+ sendInitializationSuccess_: function(channel) {
+ channel.send({name: 'apiResponse', response: {
+ result: 'initialized',
+ version: this.apiVersion_,
+ keyTypes: API_KEY_TYPES
+ }});
+ },
+
+ sendInitializationFailure_: function(channel) {
+ channel.send({
+ name: 'apiResponse',
+ response: {result: 'initialization_failed'}
+ });
+ },
+
+ /**
+ * Handlers for channel messages.
+ * @param {Channel} channel A channel to send back response.
+ * @param {Object} msg Received message.
+ * @private
+ */
+ onAPICall_: function(channel, msg) {
+ var call = msg.call;
+ if (call.method == 'initialize') {
+ if (!Number.isInteger(call.requestedVersion) ||
+ call.requestedVersion < MIN_API_VERSION_VERSION) {
+ this.sendInitializationFailure_(channel);
+ return;
+ }
+
+ this.apiVersion_ = Math.min(call.requestedVersion,
+ MAX_API_VERSION_VERSION);
+ this.apiInitialized_ = true;
+ this.sendInitializationSuccess_(channel);
+ return;
+ }
+
+ if (call.method == 'add') {
+ if (API_KEY_TYPES.indexOf(call.keyType) == -1) {
+ console.error('SamlHandler.onAPICall_: unsupported key type');
+ return;
+ }
+ // Not setting |email_| and |gaiaId_| because this API call will
+ // eventually be followed by onCompleteLogin_() which does set it.
+ this.apiToken_ = call.token;
+ this.apiPasswordBytes_ = call.passwordBytes;
+ } else if (call.method == 'confirm') {
+ if (call.token != this.apiToken_)
+ console.error('SamlHandler.onAPICall_: token mismatch');
+ } else {
+ console.error('SamlHandler.onAPICall_: unknown message');
+ }
+ },
+
+ onUpdatePassword_: function(channel, msg) {
+ if (this.isSamlPage_)
+ this.passwordStore_[msg.id] = msg.password;
+ },
+
+ onPageLoaded_: function(channel, msg) {
+ this.authDomain = extractDomain(msg.url);
+ this.dispatchEvent(new CustomEvent(
+ 'authPageLoaded',
+ {detail: {url: url,
+ isSAMLPage: this.isSamlPage_,
+ domain: this.authDomain}}));
+ },
+
+ onGetSAMLFlag_: function(channel, msg) {
+ return this.isSamlPage_;
+ },
+ };
+
+ return {
+ SamlHandler: SamlHandler
+ };
+});
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/webview_saml_injected.js b/chromium/chrome/browser/resources/gaia_auth_host/webview_saml_injected.js
new file mode 100644
index 00000000000..84dcb2a51c3
--- /dev/null
+++ b/chromium/chrome/browser/resources/gaia_auth_host/webview_saml_injected.js
@@ -0,0 +1,6 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+<include src="post_message_channel.js">
+<include src="../gaia_auth/saml_injected.js">