diff options
author | Allan Sandfeld Jensen <allan.jensen@theqtcompany.com> | 2015-06-18 14:10:49 +0200 |
---|---|---|
committer | Oswald Buddenhagen <oswald.buddenhagen@theqtcompany.com> | 2015-06-18 13:53:24 +0000 |
commit | 813fbf95af77a531c57a8c497345ad2c61d475b3 (patch) | |
tree | 821b2c8de8365f21b6c9ba17a236fb3006a1d506 /chromium/chrome/browser/resources/gaia_auth_host | |
parent | af6588f8d723931a298c995fa97259bb7f7deb55 (diff) | |
download | qtwebengine-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')
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"> |