diff options
author | Jocelyn Turcotte <jocelyn.turcotte@digia.com> | 2014-08-08 14:30:41 +0200 |
---|---|---|
committer | Jocelyn Turcotte <jocelyn.turcotte@digia.com> | 2014-08-12 13:49:54 +0200 |
commit | ab0a50979b9eb4dfa3320eff7e187e41efedf7a9 (patch) | |
tree | 498dfb8a97ff3361a9f7486863a52bb4e26bb898 /chromium/chrome/browser/resources/gaia_auth | |
parent | 4ce69f7403811819800e7c5ae1318b2647e778d1 (diff) | |
download | qtwebengine-chromium-ab0a50979b9eb4dfa3320eff7e187e41efedf7a9.tar.gz |
Update Chromium to beta version 37.0.2062.68
Change-Id: I188e3b5aff1bec75566014291b654eb19f5bc8ca
Reviewed-by: Andras Becsi <andras.becsi@digia.com>
Diffstat (limited to 'chromium/chrome/browser/resources/gaia_auth')
11 files changed, 623 insertions, 312 deletions
diff --git a/chromium/chrome/browser/resources/gaia_auth/OWNERS b/chromium/chrome/browser/resources/gaia_auth/OWNERS index cac125975e2..932dfbf8be8 100644 --- a/chromium/chrome/browser/resources/gaia_auth/OWNERS +++ b/chromium/chrome/browser/resources/gaia_auth/OWNERS @@ -1,3 +1,4 @@ nkostylev@chromium.org xiyuan@chromium.org zelidrag@chromium.org +guohui@chromium.org
\ No newline at end of file diff --git a/chromium/chrome/browser/resources/gaia_auth/background.js b/chromium/chrome/browser/resources/gaia_auth/background.js index 96f9e39c858..3baafc2c332 100644 --- a/chromium/chrome/browser/resources/gaia_auth/background.js +++ b/chromium/chrome/browser/resources/gaia_auth/background.js @@ -4,90 +4,182 @@ /** * @fileoverview - * The background script of auth extension that bridges the communications - * between main and injected script. - * Here are the communications along a SAML sign-in flow: - * 1. Main script sends an 'onAuthStarted' signal to indicate the authentication - * flow is started and SAML pages might be loaded from now on; - * 2. After the 'onAuthTstarted' signal, injected script starts to scraping - * all password fields on normal page (i.e. http or https) and sends page - * load signal as well as the passwords to the background script here; + * A background script of the auth extension that bridges the communication + * between the main and injected scripts. + * + * Here is an overview of the communication flow when SAML is being used: + * 1. The main script sends the |startAuth| signal to this background script, + * indicating that the authentication flow has started and SAML pages may be + * loaded from now on. + * 2. A script is injected into each SAML page. The injected script sends three + * main types of messages to this background script: + * a) A |pageLoaded| message is sent when the page has been loaded. This is + * forwarded to the main script as |onAuthPageLoaded|. + * b) If the SAML provider supports the credential passing API, the API calls + * are sent to this background script as |apiCall| messages. These + * messages are forwarded unmodified to the main script. + * c) The injected script scrapes passwords. They are sent to this background + * script in |updatePassword| messages. The main script can request a list + * of the scraped passwords by sending the |getScrapedPasswords| message. */ /** - * BackgroundBridge holds the main script's state and the scraped passwords - * from the injected script to help the two collaborate. + * BackgroundBridgeManager maintains an array of BackgroundBridge, indexed by + * the associated tab id. */ -function BackgroundBridge() { +function BackgroundBridgeManager() { } -BackgroundBridge.prototype = { - // Gaia URL base that is set from main auth script. - gaiaUrl_: null, - - // Whether auth flow has started. It is used as a signal of whether the - // injected script should scrape passwords. - authStarted_: false, - - passwordStore_: {}, - - channelMain_: null, - channelInjected_: null, +BackgroundBridgeManager.prototype = { + // Maps a tab id to its associated BackgroundBridge. + bridges_: {}, run: function() { chrome.runtime.onConnect.addListener(this.onConnect_.bind(this)); - // Workarounds for loading SAML page in an iframe. + chrome.webRequest.onBeforeRequest.addListener( + function(details) { + if (this.bridges_[details.tabId]) + return this.bridges_[details.tabId].onInsecureRequest(details.url); + }.bind(this), + {urls: ['http://*/*', 'file://*/*', 'ftp://*/*']}, + ['blocking']); + + chrome.webRequest.onBeforeSendHeaders.addListener( + function(details) { + if (this.bridges_[details.tabId]) + return this.bridges_[details.tabId].onBeforeSendHeaders(details); + else + return {requestHeaders: details.requestHeaders}; + }.bind(this), + {urls: ['*://*/*'], types: ['sub_frame']}, + ['blocking', 'requestHeaders']); + chrome.webRequest.onHeadersReceived.addListener( function(details) { - if (!this.authStarted_) - return; + if (this.bridges_[details.tabId]) + this.bridges_[details.tabId].onHeadersReceived(details); + }.bind(this), + {urls: ['*://*/*'], types: ['sub_frame']}, + ['responseHeaders']); - var headers = details.responseHeaders; - for (var i = 0; headers && i < headers.length; ++i) { - if (headers[i].name.toLowerCase() == 'x-frame-options') { - headers.splice(i, 1); - break; - } - } - return {responseHeaders: headers}; + chrome.webRequest.onCompleted.addListener( + function(details) { + if (this.bridges_[details.tabId]) + this.bridges_[details.tabId].onCompleted(details); }.bind(this), - {urls: ['<all_urls>'], types: ['sub_frame']}, - ['blocking', 'responseHeaders']); + {urls: ['*://*/*'], types: ['sub_frame']}, + ['responseHeaders']); }, onConnect_: function(port) { - if (port.name == 'authMain') - this.setupForAuthMain_(port); - else if (port.name == 'injected') - this.setupForInjected_(port); - else + var tabId = this.getTabIdFromPort_(port); + if (!this.bridges_[tabId]) + this.bridges_[tabId] = new BackgroundBridge(tabId); + if (port.name == 'authMain') { + this.bridges_[tabId].setupForAuthMain(port); + port.onDisconnect.addListener(function() { + delete this.bridges_[tabId]; + }.bind(this)); + } else if (port.name == 'injected') { + this.bridges_[tabId].setupForInjected(port); + } else { console.error('Unexpected connection, port.name=' + port.name); + } }, + getTabIdFromPort_: function(port) { + return port.sender.tab ? port.sender.tab.id : -1; + } +}; + +/** + * BackgroundBridge allows the main script and the injected script to + * collaborate. It forwards credentials API calls to the main script and + * maintains a list of scraped passwords. + * @param {string} tabId The associated tab ID. + */ +function BackgroundBridge(tabId) { + this.tabId_ = tabId; +} + +BackgroundBridge.prototype = { + // The associated tab ID. Only used for debugging now. + tabId: null, + + isDesktopFlow_: false, + + // Continue URL that is set from main auth script. + continueUrl_: null, + + // Whether the extension is loaded in a constrained window. + // Set from main auth script. + isConstrainedWindow_: null, + + // Email of the newly authenticated user based on the gaia response header + // 'google-accounts-signin'. + email_: null, + + // Session index of the newly authenticated user based on the gaia response + // header 'google-accounts-signin'. + sessionIndex_: null, + + // Gaia URL base that is set from main auth script. + gaiaUrl_: null, + + // Whether to abort the authentication flow and show an error messagen when + // content served over an unencrypted connection is detected. + blockInsecureContent_: false, + + // Whether auth flow has started. It is used as a signal of whether the + // injected script should scrape passwords. + authStarted_: false, + + passwordStore_: {}, + + channelMain_: null, + channelInjected_: null, + /** * Sets up the communication channel with the main script. */ - setupForAuthMain_: function(port) { + setupForAuthMain: function(port) { this.channelMain_ = new Channel(); this.channelMain_.init(port); + + // Registers for desktop related messages. + this.channelMain_.registerMessage( + 'initDesktopFlow', this.onInitDesktopFlow_.bind(this)); + + // Registers for SAML related messages. this.channelMain_.registerMessage( 'setGaiaUrl', this.onSetGaiaUrl_.bind(this)); this.channelMain_.registerMessage( + 'setBlockInsecureContent', this.onSetBlockInsecureContent_.bind(this)); + this.channelMain_.registerMessage( 'resetAuth', this.onResetAuth_.bind(this)); this.channelMain_.registerMessage( 'startAuth', this.onAuthStarted_.bind(this)); this.channelMain_.registerMessage( 'getScrapedPasswords', this.onGetScrapedPasswords_.bind(this)); + this.channelMain_.registerMessage( + 'apiResponse', this.onAPIResponse_.bind(this)); + + this.channelMain_.send({ + 'name': 'channelConnected' + }); }, /** * Sets up the communication channel with the injected script. */ - setupForInjected_: function(port) { + setupForInjected: function(port) { this.channelInjected_ = new Channel(); this.channelInjected_.init(port); + + this.channelInjected_.registerMessage( + 'apiCall', this.onAPICall_.bind(this)); this.channelInjected_.registerMessage( 'updatePassword', this.onUpdatePassword_.bind(this)); this.channelInjected_.registerMessage( @@ -95,22 +187,133 @@ BackgroundBridge.prototype = { }, /** + * Handler for 'initDesktopFlow' signal sent from the main script. + * Only called in desktop mode. + */ + onInitDesktopFlow_: function(msg) { + this.isDesktopFlow_ = true; + this.gaiaUrl_ = msg.gaiaUrl; + this.continueUrl_ = msg.continueUrl; + this.isConstrainedWindow_ = msg.isConstrainedWindow; + }, + + /** + * Handler for webRequest.onCompleted. It 1) detects loading of continue URL + * and notifies the main script of signin completion; 2) detects if the + * current page could be loaded in a constrained window and signals the main + * script of switching to full tab if necessary. + */ + onCompleted: function(details) { + // Only monitors requests in the gaia frame whose parent frame ID must be + // positive. + if (!this.isDesktopFlow_ || details.parentFrameId <= 0) + return; + + var msg = null; + if (this.continueUrl_ && + details.url.lastIndexOf(this.continueUrl_, 0) == 0) { + var skipForNow = false; + if (details.url.indexOf('ntp=1') >= 0) + skipForNow = true; + + // TOOD(guohui): Show password confirmation UI. + var passwords = this.onGetScrapedPasswords_(); + msg = { + 'name': 'completeLogin', + 'email': this.email_, + 'password': passwords[0], + 'sessionIndex': this.sessionIndex_, + 'skipForNow': skipForNow + }; + this.channelMain_.send(msg); + } else if (this.isConstrainedWindow_) { + // The header google-accounts-embedded is only set on gaia domain. + if (this.gaiaUrl_ && details.url.lastIndexOf(this.gaiaUrl_) == 0) { + var headers = details.responseHeaders; + for (var i = 0; headers && i < headers.length; ++i) { + if (headers[i].name.toLowerCase() == 'google-accounts-embedded') + return; + } + } + msg = { + 'name': 'switchToFullTab', + 'url': details.url + }; + this.channelMain_.send(msg); + } + }, + + /** + * 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 {string} url The URL that was blocked. + * @return {!Object} Decision whether to block the request. + */ + onInsecureRequest: function(url) { + if (!this.blockInsecureContent_) + return {}; + this.channelMain_.send({name: 'onInsecureContentBlocked', url: url}); + return {cancel: true}; + }, + + /** + * Handler or webRequest.onHeadersReceived. It reads the authenticated user + * email from google-accounts-signin-header. + */ + onHeadersReceived: function(details) { + if (!this.isDesktopFlow_ || + !this.gaiaUrl_ || + details.url.lastIndexOf(this.gaiaUrl_) != 0) { + // TODO(xiyuan, guohui): CrOS should reuse the logic below for reading the + // email for SAML users and cut off the /ListAccount call. + return; + } + + var headers = details.responseHeaders; + for (var i = 0; headers && i < headers.length; ++i) { + if (headers[i].name.toLowerCase() == 'google-accounts-signin') { + var headerValues = headers[i].value.toLowerCase().split(','); + var signinDetails = {}; + headerValues.forEach(function(e) { + var pair = e.split('='); + signinDetails[pair[0].trim()] = pair[1].trim(); + }); + // Remove "" around. + this.email_ = signinDetails['email'].slice(1, -1); + this.sessionIndex_ = signinDetails['sessionindex']; + return; + } + } + }, + + /** + * Handler for webRequest.onBeforeSendHeaders. + * @return {!Object} Modified request headers. + */ + onBeforeSendHeaders: function(details) { + if (!this.isDesktopFlow_ && this.gaiaUrl_ && + details.url.indexOf(this.gaiaUrl_) == 0) { + details.requestHeaders.push({ + name: 'X-Cros-Auth-Ext-Support', + value: 'SAML' + }); + } + return {requestHeaders: details.requestHeaders}; + }, + + /** * Handler for 'setGaiaUrl' signal sent from the main script. */ onSetGaiaUrl_: function(msg) { this.gaiaUrl_ = msg.gaiaUrl; + }, - // Set request header to let Gaia know that saml support is on. - chrome.webRequest.onBeforeSendHeaders.addListener( - function(details) { - details.requestHeaders.push({ - name: 'X-Cros-Auth-Ext-Support', - value: 'SAML' - }); - return {requestHeaders: details.requestHeaders}; - }, - {urls: [this.gaiaUrl_ + '*'], types: ['sub_frame']}, - ['blocking', 'requestHeaders']); + /** + * Handler for 'setBlockInsecureContent' signal sent from the main script. + */ + onSetBlockInsecureContent_: function(msg) { + this.blockInsecureContent_ = msg.blockInsecureContent; }, /** @@ -141,6 +344,18 @@ BackgroundBridge.prototype = { return Object.keys(passwords); }, + /** + * Handler for 'apiResponse' signal sent from the main script. Passes on the + * |msg| to the injected script. + */ + onAPIResponse_: function(msg) { + this.channelInjected_.send(msg); + }, + + onAPICall_: function(msg) { + this.channelMain_.send(msg); + }, + onUpdatePassword_: function(msg) { if (!this.authStarted_) return; @@ -149,9 +364,10 @@ BackgroundBridge.prototype = { }, onPageLoaded_: function(msg) { - this.channelMain_.send({name: 'onAuthPageLoaded', url: msg.url}); + if (this.channelMain_) + this.channelMain_.send({name: 'onAuthPageLoaded', url: msg.url}); } }; -var backgroundBridge = new BackgroundBridge(); -backgroundBridge.run(); +var backgroundBridgeManager = new BackgroundBridgeManager(); +backgroundBridgeManager.run(); diff --git a/chromium/chrome/browser/resources/gaia_auth/inline_injected.js b/chromium/chrome/browser/resources/gaia_auth/inline_injected.js deleted file mode 100644 index b0b84738160..00000000000 --- a/chromium/chrome/browser/resources/gaia_auth/inline_injected.js +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2013 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 Code injected into Gaia sign in page for inline sign in flow. - * On load stop, it receives a message from the embedder extension with a - * JavaScript reference to the embedder window. Then upon submit of the sign - * in form, it posts the username and password to the embedder window. - * Prototype Only. - */ - -(function() { - var extWindow; - - var $ = function(id) { return document.getElementById(id); }; - var gaiaLoginForm = $('gaia_loginform'); - - var onMessage = function(e) { - extWindow = e.source; - }; - window.addEventListener('message', onMessage); - - var onLoginSubmit = function(e) { - if (!extWindow) { - console.log('ERROR: no initial message received from the gaia ext'); - e.preventDefault(); - return; - } - - var checkboxElement = $('advanced-box'); - var chooseWhatToSync = checkboxElement && checkboxElement.checked; - var msg = {method: 'attemptLogin', - email: gaiaLoginForm['Email'].value, - password: gaiaLoginForm['Passwd'].value, - attemptToken: new Date().getTime(), - chooseWhatToSync: chooseWhatToSync}; - - extWindow.postMessage(msg, 'chrome://chrome-signin'); - console.log('Credentials sent'); - - return; - }; - // Overrides the submit handler for the gaia login form. - gaiaLoginForm.onsubmit = onLoginSubmit; -})(); diff --git a/chromium/chrome/browser/resources/gaia_auth/inline_main.html b/chromium/chrome/browser/resources/gaia_auth/inline_main.html deleted file mode 100644 index 00ed78b4982..00000000000 --- a/chromium/chrome/browser/resources/gaia_auth/inline_main.html +++ /dev/null @@ -1,13 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <link rel="stylesheet" href="main.css"> - <meta charset="utf-8"> - <script src="channel.js"></script> - <script src="util.js"></script> - <script src="main.js"></script> -</head> -<body> - <webview id="gaia-frame"></webview> -</body> -</html> diff --git a/chromium/chrome/browser/resources/gaia_auth/main.html b/chromium/chrome/browser/resources/gaia_auth/main.html index 944d3fffd9a..a53b3fe919c 100644 --- a/chromium/chrome/browser/resources/gaia_auth/main.html +++ b/chromium/chrome/browser/resources/gaia_auth/main.html @@ -8,6 +8,8 @@ <script src="main.js"></script> </head> <body> - <iframe id="gaia-frame" src="about:blank" frameborder="0"></iframe> + <iframe id="gaia-frame" name="gaia-frame" src="about:blank" frameborder="0" + sandbox="allow-same-origin allow-scripts allow-popups allow-forms + allow-pointer-lock"></iframe> </body> </html> diff --git a/chromium/chrome/browser/resources/gaia_auth/main.js b/chromium/chrome/browser/resources/gaia_auth/main.js index 7641a848546..41f5116fff6 100644 --- a/chromium/chrome/browser/resources/gaia_auth/main.js +++ b/chromium/chrome/browser/resources/gaia_auth/main.js @@ -16,6 +16,26 @@ Authenticator.THIS_EXTENSION_ORIGIN = 'chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik'; /** + * The lowest version of the credentials passing API supported. + * @type {number} + */ +Authenticator.MIN_API_VERSION_VERSION = 1; + +/** + * The highest version of the credentials passing API supported. + * @type {number} + */ +Authenticator.MAX_API_VERSION_VERSION = 1; + +/** + * The key types supported by the credentials passing API. + * @type {Array} Array of strings. + */ +Authenticator.API_KEY_TYPES = [ + 'KEY_TYPE_PASSWORD_PLAIN', +]; + +/** * Singleton getter of Authenticator. * @return {Object} The singleton instance of Authenticator. */ @@ -28,21 +48,30 @@ Authenticator.getInstance = function() { Authenticator.prototype = { email_: null, - password_: null, + + // Depending on the key type chosen, this will contain the plain text password + // or a credential derived from it along with the information required to + // repeat the derivation, such as a salt. The information will be encoded so + // that it contains printable ASCII characters only. The exact encoding is TBD + // when support for key types other than plain text password is added. + passwordBytes_: null, + attemptToken_: null, // Input params from extension initialization URL. inputLang_: undefined, intputEmail_: undefined, - samlPageLoaded_: false, - samlSupportChannel_: null, + isSAMLFlow_: false, + isSAMLEnabled_: false, + supportChannel_: null, GAIA_URL: 'https://accounts.google.com/', GAIA_PAGE_PATH: 'ServiceLogin?skipvpage=true&sarp=1&rm=hide', PARENT_PAGE: 'chrome://oobe/', SERVICE_ID: 'chromeoslogin', CONTINUE_URL: Authenticator.THIS_EXTENSION_ORIGIN + '/success.html', + CONSTRAINED_FLOW_SOURCE: 'chrome', initialize: function() { var params = getUrlSearchParams(location.search); @@ -53,16 +82,17 @@ Authenticator.prototype = { this.inputEmail_ = params.email; this.service_ = params.service || this.SERVICE_ID; this.continueUrl_ = params.continueUrl || this.CONTINUE_URL; - this.continueUrlWithoutParams_ = stripParams(this.continueUrl_); - this.inlineMode_ = params.inlineMode == '1'; - this.constrained_ = params.constrained == '1'; - this.partitionId_ = params.partitionId || ''; + this.desktopMode_ = params.desktopMode == '1'; + this.isConstrainedWindow_ = params.constrained == '1'; this.initialFrameUrl_ = params.frameUrl || this.constructInitialFrameUrl_(); this.initialFrameUrlWithoutParams_ = stripParams(this.initialFrameUrl_); - this.loaded_ = false; - document.addEventListener('DOMContentLoaded', this.onPageLoad.bind(this)); - document.addEventListener('enableSAML', this.onEnableSAML_.bind(this)); + document.addEventListener('DOMContentLoaded', this.onPageLoad_.bind(this)); + if (!this.desktopMode_) { + // SAML is always enabled in desktop mode, thus no need to listen for + // enableSAML event. + document.addEventListener('enableSAML', this.onEnableSAML_.bind(this)); + } }, isGaiaMessage_: function(msg) { @@ -88,116 +118,135 @@ Authenticator.prototype = { url = appendParam(url, 'hl', this.inputLang_); if (this.inputEmail_) url = appendParam(url, 'Email', this.inputEmail_); - + if (this.isConstrainedWindow_) + url = appendParam(url, 'source', this.CONSTRAINED_FLOW_SOURCE); return url; }, - /** Callback when all loads in the gaia webview is complete. */ - onWebviewLoadstop_: function(gaiaFrame) { - // Report the current state to the parent which will then update the - // browser history so that later it could respond properly to back/forward. - var msg = { - 'method': 'reportState', - 'src': gaiaFrame.src - }; - window.parent.postMessage(msg, this.parentPage_); + onPageLoad_: function() { + window.addEventListener('message', this.onMessage.bind(this), false); - if (gaiaFrame.src.lastIndexOf( - this.continueUrlWithoutParams_, 0) == 0) { - // Detect when login is finished by the load stop event of the continue - // URL. Cannot reuse the login complete flow in success.html, because - // webview does not support extension pages yet. - gaiaFrame.hidden = true; - msg = {'method': 'completeLogin'}; - window.parent.postMessage(msg, this.parentPage_); - return; - } + var gaiaFrame = $('gaia-frame'); + gaiaFrame.src = this.initialFrameUrl_; - if (gaiaFrame.src.lastIndexOf(this.gaiaUrl_, 0) == 0) { - gaiaFrame.executeScript({file: 'inline_injected.js'}, function() { - // Send an initial message to gaia so that it has an JavaScript - // reference to the embedder. - gaiaFrame.contentWindow.postMessage('', gaiaFrame.src); - }); - } + if (this.desktopMode_) { + var handler = function() { + this.onLoginUILoaded_(); + gaiaFrame.removeEventListener('load', handler); - this.loaded_ || this.onLoginUILoaded(); + this.initDesktopChannel_(); + }.bind(this); + gaiaFrame.addEventListener('load', handler); + } }, - /** - * Callback when the gaia webview attempts to open a new window. - */ - onWebviewNewWindow_: function(gaiaFrame, e) { - window.open(e.targetUrl, '_blank'); - e.window.discard(); - }, + initDesktopChannel_: function() { + this.supportChannel_ = new Channel(); + this.supportChannel_.connect('authMain'); - onWebviewRequestCompleted_: function(details) { - if (details.url.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) { - return; - } + var channelConnected = false; + this.supportChannel_.registerMessage('channelConnected', function() { + channelConnected = true; - var headers = details.responseHeaders; - for (var i = 0; headers && i < headers.length; ++i) { - if (headers[i].name.toLowerCase() == 'google-accounts-embedded') { - return; + this.supportChannel_.send({ + name: 'initDesktopFlow', + gaiaUrl: this.gaiaUrl_, + continueUrl: stripParams(this.continueUrl_), + isConstrainedWindow: this.isConstrainedWindow_ + }); + this.supportChannel_.registerMessage( + 'switchToFullTab', this.switchToFullTab_.bind(this)); + this.supportChannel_.registerMessage( + 'completeLogin', this.completeLogin_.bind(this)); + + this.onEnableSAML_(); + }.bind(this)); + + window.setTimeout(function() { + if (!channelConnected) { + // Re-initialize the channel if it is not connected properly, e.g. + // connect may be called before background script started running. + this.initDesktopChannel_(); } - } + }.bind(this), 200); + }, + + /** + * Invoked when the login UI is initialized or reset. + */ + onLoginUILoaded_: function() { var msg = { - 'method': 'switchToFullTab', - 'url': details.url + 'method': 'loginUILoaded' }; window.parent.postMessage(msg, this.parentPage_); }, - loadFrame_: function() { - var gaiaFrame = $('gaia-frame'); - gaiaFrame.partition = this.partitionId_; - gaiaFrame.src = this.initialFrameUrl_; - if (this.inlineMode_) { - gaiaFrame.addEventListener( - 'loadstop', this.onWebviewLoadstop_.bind(this, gaiaFrame)); - gaiaFrame.addEventListener( - 'newwindow', this.onWebviewNewWindow_.bind(this, gaiaFrame)); - } - if (this.constrained_) { - gaiaFrame.request.onCompleted.addListener( - this.onWebviewRequestCompleted_.bind(this), - {urls: ['<all_urls>'], types: ['main_frame']}, - ['responseHeaders']); - } + /** + * Invoked when the background script sends a message to indicate that the + * current content does not fit in a constrained window. + * @param {Object=} opt_extraMsg Optional extra info to send. + */ + switchToFullTab_: function(msg) { + var parentMsg = { + 'method': 'switchToFullTab', + 'url': msg.url + }; + window.parent.postMessage(parentMsg, this.parentPage_); }, - completeLogin: function(username, password) { + /** + * Invoked when the signin flow is complete. + * @param {Object=} opt_extraMsg Optional extra info to send. + */ + completeLogin_: function(opt_extraMsg) { var msg = { 'method': 'completeLogin', - 'email': username, - 'password': password + 'email': (opt_extraMsg && opt_extraMsg.email) || this.email_, + 'password': (opt_extraMsg && opt_extraMsg.password) || + this.passwordBytes_, + 'usingSAML': this.isSAMLFlow_, + 'chooseWhatToSync': this.chooseWhatToSync_ || false, + 'skipForNow': opt_extraMsg && opt_extraMsg.skipForNow, + 'sessionIndex': opt_extraMsg && opt_extraMsg.sessionIndex }; window.parent.postMessage(msg, this.parentPage_); - if (this.samlSupportChannel_) - this.samlSupportChannel_.send({name: 'resetAuth'}); - }, - - onPageLoad: function(e) { - window.addEventListener('message', this.onMessage.bind(this), false); - this.loadFrame_(); + if (this.isSAMLEnabled_) + this.supportChannel_.send({name: 'resetAuth'}); }, /** - * Invoked when 'enableSAML' event is received to initialize SAML support. + * Invoked when 'enableSAML' event is received to initialize SAML support on + * Chrome OS, or when initDesktopChannel_ is called on desktop. */ onEnableSAML_: function() { - this.samlPageLoaded_ = false; + this.isSAMLEnabled_ = true; + this.isSAMLFlow_ = false; - this.samlSupportChannel_ = new Channel(); - this.samlSupportChannel_.connect('authMain'); - this.samlSupportChannel_.registerMessage( + if (!this.supportChannel_) { + this.supportChannel_ = new Channel(); + this.supportChannel_.connect('authMain'); + } + + this.supportChannel_.registerMessage( 'onAuthPageLoaded', this.onAuthPageLoaded_.bind(this)); - this.samlSupportChannel_.send({ + this.supportChannel_.registerMessage( + 'onInsecureContentBlocked', this.onInsecureContentBlocked_.bind(this)); + this.supportChannel_.registerMessage( + 'apiCall', this.onAPICall_.bind(this)); + this.supportChannel_.send({ name: 'setGaiaUrl', gaiaUrl: this.gaiaUrl_ }); + if (!this.desktopMode_ && this.gaiaUrl_.indexOf('https://') == 0) { + // Abort the login flow when content served over an unencrypted connection + // is detected on Chrome OS. This does not apply to tests that explicitly + // set a non-https GAIA URL and want to perform all authentication over + // http. + this.supportChannel_.send({ + name: 'setBlockInsecureContent', + blockInsecureContent: true + }); + } }, /** @@ -205,52 +254,135 @@ Authenticator.prototype = { * @param {!Object} msg Details sent with the message. */ onAuthPageLoaded_: function(msg) { - this.samlPageLoaded_ = msg.url.indexOf(this.gaiaUrl_) != 0; + var isSAMLPage = msg.url.indexOf(this.gaiaUrl_) != 0; + + if (isSAMLPage && !this.isSAMLFlow_) { + // GAIA redirected to a SAML login page. The credentials provided to this + // page will determine what user gets logged in. The credentials obtained + // from the GAIA login form are no longer relevant and can be discarded. + this.isSAMLFlow_ = true; + this.email_ = null; + this.passwordBytes_ = null; + } + window.parent.postMessage({ 'method': 'authPageLoaded', - 'isSAML': this.samlPageLoaded_ + 'isSAML': this.isSAMLFlow_, + 'domain': extractDomain(msg.url) }, this.parentPage_); }, - onLoginUILoaded: function() { - var msg = { - 'method': 'loginUILoaded' - }; - window.parent.postMessage(msg, this.parentPage_); - if (this.inlineMode_) { - $('gaia-frame').focus(); + /** + * Invoked when the background page sends an 'onInsecureContentBlocked' + * message. + * @param {!Object} msg Details sent with the message. + */ + onInsecureContentBlocked_: function(msg) { + window.parent.postMessage({ + 'method': 'insecureContentBlocked', + 'url': stripParams(msg.url) + }, this.parentPage_); + }, + + /** + * Invoked when one of the credential passing API methods is called by a SAML + * provider. + * @param {!Object} msg Details of the API call. + */ + onAPICall_: function(msg) { + var call = msg.call; + if (call.method == 'initialize') { + if (!Number.isInteger(call.requestedVersion) || + call.requestedVersion < Authenticator.MIN_API_VERSION_VERSION) { + this.sendInitializationFailure_(); + return; + } + + this.apiVersion_ = Math.min(call.requestedVersion, + Authenticator.MAX_API_VERSION_VERSION); + this.initialized_ = true; + this.sendInitializationSuccess_(); + return; + } + + if (call.method == 'add') { + if (Authenticator.API_KEY_TYPES.indexOf(call.keyType) == -1) { + console.error('Authenticator.onAPICall_: unsupported key type'); + return; + } + this.apiToken_ = call.token; + this.email_ = call.user; + this.passwordBytes_ = call.passwordBytes; + } else if (call.method == 'confirm') { + if (call.token != this.apiToken_) + console.error('Authenticator.onAPICall_: token mismatch'); + } else { + console.error('Authenticator.onAPICall_: unknown message'); } - this.loaded_ = true; + }, + + sendInitializationSuccess_: function() { + this.supportChannel_.send({name: 'apiResponse', response: { + result: 'initialized', + version: this.apiVersion_, + keyTypes: Authenticator.API_KEY_TYPES + }}); + }, + + sendInitializationFailure_: function() { + this.supportChannel_.send({ + name: 'apiResponse', + response: {result: 'initialization_failed'} + }); }, onConfirmLogin_: function() { - if (!this.samlPageLoaded_) { - this.completeLogin(this.email_, this.password_); + if (!this.isSAMLFlow_) { + this.completeLogin_(); return; } - this.samlSupportChannel_.sendWithCallback( - {name: 'getScrapedPasswords'}, - function(passwords) { - if (passwords.length == 0) { - window.parent.postMessage( - {method: 'noPassword', email: this.email_}, - this.parentPage_); - } else { - window.parent.postMessage( - {method: 'confirmPassword', email: this.email_}, - this.parentPage_); - } - }.bind(this)); + var apiUsed = !!this.passwordBytes_; + + // Retrieve the e-mail address of the user who just authenticated from GAIA. + window.parent.postMessage({method: 'retrieveAuthenticatedUserEmail', + attemptToken: this.attemptToken_, + apiUsed: apiUsed}, + this.parentPage_); + + if (!apiUsed) { + this.supportChannel_.sendWithCallback( + {name: 'getScrapedPasswords'}, + function(passwords) { + if (passwords.length == 0) { + window.parent.postMessage( + {method: 'noPassword', email: this.email_}, + this.parentPage_); + } else { + window.parent.postMessage({method: 'confirmPassword', + email: this.email_, + passwordCount: passwords.length}, + this.parentPage_); + } + }.bind(this)); + } + }, + + maybeCompleteSAMLLogin_: function() { + // SAML login is complete when the user's e-mail address has been retrieved + // from GAIA and the user has successfully confirmed the password. + if (this.email_ !== null && this.passwordBytes_ !== null) + this.completeLogin_(); }, onVerifyConfirmedPassword_: function(password) { - this.samlSupportChannel_.sendWithCallback( + this.supportChannel_.sendWithCallback( {name: 'getScrapedPasswords'}, function(passwords) { for (var i = 0; i < passwords.length; ++i) { if (passwords[i] == password) { - this.completeLogin(this.email_, passwords[i]); + this.passwordBytes_ = passwords[i]; + this.maybeCompleteSAMLLogin_(); return; } } @@ -264,19 +396,26 @@ Authenticator.prototype = { var msg = e.data; if (msg.method == 'attemptLogin' && this.isGaiaMessage_(e)) { this.email_ = msg.email; - this.password_ = msg.password; + this.passwordBytes_ = msg.password; this.attemptToken_ = msg.attemptToken; - this.samlPageLoaded_ = false; - if (this.samlSupportChannel_) - this.samlSupportChannel_.send({name: 'startAuth'}); + this.chooseWhatToSync_ = msg.chooseWhatToSync; + this.isSAMLFlow_ = false; + if (this.isSAMLEnabled_) + this.supportChannel_.send({name: 'startAuth'}); } else if (msg.method == 'clearOldAttempts' && this.isGaiaMessage_(e)) { this.email_ = null; - this.password_ = null; + this.passwordBytes_ = null; this.attemptToken_ = null; - this.samlPageLoaded_ = false; - this.onLoginUILoaded(); - if (this.samlSupportChannel_) - this.samlSupportChannel_.send({name: 'resetAuth'}); + this.isSAMLFlow_ = false; + this.onLoginUILoaded_(); + if (this.isSAMLEnabled_) + this.supportChannel_.send({name: 'resetAuth'}); + } else if (msg.method == 'setAuthenticatedUserEmail' && + this.isParentMessage_(e)) { + if (this.attemptToken_ == msg.attemptToken) { + this.email_ = msg.email; + this.maybeCompleteSAMLLogin_(); + } } else if (msg.method == 'confirmLogin' && this.isInternalMessage_(e)) { if (this.attemptToken_ == msg.attemptToken) this.onConfirmLogin_(); @@ -285,11 +424,11 @@ Authenticator.prototype = { } else if (msg.method == 'verifyConfirmedPassword' && this.isParentMessage_(e)) { this.onVerifyConfirmedPassword_(msg.password); - } else if (msg.method == 'navigate' && + } else if (msg.method == 'redirectToSignin' && this.isParentMessage_(e)) { - $('gaia-frame').src = msg.src; + $('gaia-frame').src = this.constructInitialFrameUrl_(); } else { - console.error('Authenticator.onMessage: unknown message + origin!?'); + console.error('Authenticator.onMessage: unknown message + origin!?'); } } }; diff --git a/chromium/chrome/browser/resources/gaia_auth/manifest_inline.json b/chromium/chrome/browser/resources/gaia_auth/manifest_inline.json deleted file mode 100644 index 2040360615f..00000000000 --- a/chromium/chrome/browser/resources/gaia_auth/manifest_inline.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - // chrome-extension://mfffpogegjflfpflabcdkioaeobkgjik/ - "key": "MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQC4L17nAfeTd6Xhtx96WhQ6DSr8KdHeQmfzgCkieKLCgUkWdwB9G1DCuh0EPMDn1MdtSwUAT7xE36APEzi0X/UpKjOVyX8tCC3aQcLoRAE0aJAvCcGwK7qIaQaczHmHKvPC2lrRdzSoMMTC5esvHX+ZqIBMi123FOL0dGW6OPKzIwIBIw==", - "name": "GaiaAuthExtension", - "version": "0.0.1", - "manifest_version": 2, - "content_security_policy": "default-src 'self'; script-src 'self'; frame-src *; style-src 'self' 'unsafe-inline'", - "description": "GAIA Component Extension", - "web_accessible_resources": [ - "main.css", - "inline_main.html", - "main.js", - "offline.css", - "offline.html", - "offline.js", - "success.html", - "success.js", - "util.js" - ], - // <all_urls> for intercepting all URL requests in the main frame, and - // switching to a full tab if needed. - // cookies for getting hash passed back from GAIA on login success. - // tabs for calling current webui's login. This might not be needed once - // we have extension API. - // webview for interacting with the GAIA sign in page loaded in a webview. - "permissions": [ - "<all_urls>", - "cookies", - "tabs", - "webview" - ] -} diff --git a/chromium/chrome/browser/resources/gaia_auth/manifest_saml.json b/chromium/chrome/browser/resources/gaia_auth/manifest_saml.json index c30bdaa9f0f..afc5e93b92d 100644 --- a/chromium/chrome/browser/resources/gaia_auth/manifest_saml.json +++ b/chromium/chrome/browser/resources/gaia_auth/manifest_saml.json @@ -31,7 +31,6 @@ ], "permissions": [ "<all_urls>", - "background", "webRequest", "webRequestBlocking" ] diff --git a/chromium/chrome/browser/resources/gaia_auth/offline.css b/chromium/chrome/browser/resources/gaia_auth/offline.css index 6f5712b285a..b36d3c942b6 100644 --- a/chromium/chrome/browser/resources/gaia_auth/offline.css +++ b/chromium/chrome/browser/resources/gaia_auth/offline.css @@ -102,8 +102,7 @@ input[type=url][disabled=disabled]:hover { -webkit-transition: all 218ms; -webkit-user-select: none; background-color: #f5f5f5; - background-image: -webkit-linear-gradient(top,#f5f5f5,#f1f1f1); - background-image: linear-gradient(top,#f5f5f5,#f1f1f1); + background-image: linear-gradient(to bottom, #f5f5f5, #f1f1f1); border: 1px solid rgba(0,0,0,0.1); border-radius: 2px; color: #555; @@ -137,8 +136,7 @@ input[type=submit].g-button { -webkit-box-shadow: 0 1px 1px rgba(0,0,0,0.1); -webkit-transition: all 0; background-color: #f8f8f8; - background-image: -webkit-linear-gradient(top,#f8f8f8,#f1f1f1); - background-image: linear-gradient(top,#f8f8f8,#f1f1f1); + background-image: linear-gradient(to bottom, #f8f8f8, #f1f1f1); border: 1px solid #c6c6c6; box-shadow: 0 1px 1px rgba(0,0,0,0.1); color: #333; @@ -148,8 +146,7 @@ input[type=submit].g-button { .g-button:active { -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); background-color: #f6f6f6; - background-image: -webkit-linear-gradient(top,#f6f6f6,#f1f1f1); - background-image: linear-gradient(top,#f6f6f6,#f1f1f1); + background-image: linear-gradient(to bottom, #f6f6f6, #f1f1f1); box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); } .g-button:visited { @@ -157,19 +154,14 @@ input[type=submit].g-button { } .g-button-submit { background-color: #4d90fe; - background-image: -webkit-gradient(linear,left top,left bottom,from(#4d90fe),to(#4787ed)); - background-image: -webkit-linear-gradient(top,#4d90fe,#4787ed); - background-image: -ms-linear-gradient(top,#4d90fe,#4787ed); - background-image: -o-linear-gradient(top,#4d90fe,#4787ed); - background-image: linear-gradient(top,#4d90fe,#4787ed); + background-image: linear-gradient(to bottom, #4d90fe, #4787ed); border: 1px solid #3079ed; color: #fff; text-shadow: 0 1px rgba(0,0,0,0.1); } .g-button-submit:hover { background-color: #357ae8; - background-image: -webkit-linear-gradient(top,#4d90fe,#357ae8); - background-image: linear-gradient(top,#4d90fe,#357ae8); + background-image: linear-gradient(to bottom, #4d90fe, #357ae8); border: 1px solid #2f5bb7; color: #fff; text-shadow: 0 1px rgba(0,0,0,0.3); diff --git a/chromium/chrome/browser/resources/gaia_auth/saml_injected.js b/chromium/chrome/browser/resources/gaia_auth/saml_injected.js index d7fd1b8e37e..e41dfebec62 100644 --- a/chromium/chrome/browser/resources/gaia_auth/saml_injected.js +++ b/chromium/chrome/browser/resources/gaia_auth/saml_injected.js @@ -4,15 +4,60 @@ /** * @fileoverview - * Script to be injected into SAML provider pages that do not support the - * auth service provider postMessage API. It serves two main purposes: + * Script to be injected into SAML provider pages, serving three main purposes: * 1. Signal hosting extension that an external page is loaded so that the - * UI around it could be changed accordingly; - * 2. Scrape password and send it back to be used for encrypt user data and - * use for offline login; + * UI around it should be changed accordingly; + * 2. Provide an API via which the SAML provider can pass user credentials to + * Chrome OS, allowing the password to be used for encrypting user data and + * offline login. + * 3. Scrape password fields, making the password available to Chrome OS even if + * the SAML provider does not support the credential passing API. */ (function() { + function APICallForwarder() { + } + + /** + * The credential passing API is used by sending messages to the SAML page's + * |window| object. This class forwards API calls from the SAML page to a + * background script and API responses from the background script to the SAML + * page. Communication with the background script occurs via a |Channel|. + */ + APICallForwarder.prototype = { + // Channel to which API calls are forwarded. + channel_: null, + + /** + * Initialize the API call forwarder. + * @param {!Object} channel Channel to which API calls should be forwarded. + */ + init: function(channel) { + this.channel_ = channel; + this.channel_.registerMessage('apiResponse', + this.onAPIResponse_.bind(this)); + + window.addEventListener('message', this.onMessage_.bind(this)); + }, + + onMessage_: function(event) { + if (event.source != window || + typeof event.data != 'object' || + !event.data.hasOwnProperty('type') || + event.data.type != 'gaia_saml_api') { + return; + } + // Forward API calls to the background script. + this.channel_.send({name: 'apiCall', call: event.data.call}); + }, + + onAPIResponse_: function(msg) { + // Forward API responses to the SAML page. + window.postMessage({type: 'gaia_saml_api_reply', response: msg.response}, + '/'); + } + }; + /** * A class to scrape password from type=password input elements under a given * docRoot and send them back via a Channel. @@ -51,11 +96,7 @@ for (var i = 0; i < this.passwordFields_.length; ++i) { this.passwordFields_[i].addEventListener( - 'change', this.onPasswordChanged_.bind(this, i)); - // 'keydown' event is needed for the case that the form is submitted - // on enter key, in which case no 'change' event is dispatched. - this.passwordFields_[i].addEventListener( - 'keydown', this.onPasswordKeyDown_.bind(this, i)); + 'input', this.onPasswordChanged_.bind(this, i)); this.passwordValues_[i] = this.passwordFields_[i].value; } @@ -89,18 +130,6 @@ */ onPasswordChanged_: function(index) { this.maybeSendUpdatedPassword(index); - }, - - /** - * Handles 'keydown' event to trigger password change detection and - * updates on enter key. - * @param {number} index The index of the password fields in - * |passwordFields_|. - * @param {Event} e The keydown event. - */ - onPasswordKeyDown_: function(index, e) { - if (e.keyIdentifier == 'Enter') - this.maybeSendUpdatedPassword(index); } }; @@ -141,6 +170,9 @@ channel.connect('injected'); channel.send({name: 'pageLoaded', url: pageURL}); + apiCallForwarder = new APICallForwarder(); + apiCallForwarder.init(channel); + passwordScraper = new PasswordInputScraper(); passwordScraper.init(channel, pageURL, document.documentElement); } diff --git a/chromium/chrome/browser/resources/gaia_auth/util.js b/chromium/chrome/browser/resources/gaia_auth/util.js index 72d13c2f2f6..9d8ebd7237d 100644 --- a/chromium/chrome/browser/resources/gaia_auth/util.js +++ b/chromium/chrome/browser/resources/gaia_auth/util.js @@ -2,10 +2,20 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +/** + * Alias for document.getElementById. + * @param {string} id The ID of the element to find. + * @return {HTMLElement} The found element or null if not found. + */ function $(id) { return document.getElementById(id); } +/** + * Extract query params from given search string of an URL. + * @param {string} search The search portion of an URL to extract params. + * @return {Object} The key value pairs of the extracted params. + */ function getUrlSearchParams(search) { var params = {}; @@ -51,3 +61,14 @@ function appendParam(url, key, value) { 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; +} |