Implement signin using webview
This CL implements the Chrome signin on desktop using webview. Since the webview approach is
significantly different than the current iframe one, thus I created a fork of some files
instead of adding more logic to the existing over-complicated code. The feature is enabled
under a flag --enable-webview-based-signin. Once the feature is fully launched on both dekstop
and ChromeOS, the old files should be deleted.
There are a few known issues.
1. Gaia needs to update their script to post credentials directly to chrome://chrome-signin
instead of the gaia auth extension. Before the gaia code is updated, webview signin cannot scrape
password and the checkbox value for choosing what to sync.
2. Webview currently only loads in a full tab, need to investigate why it doesn't work when
embedded in the avatar menu.
3. Some webview apis are broken in an extension-less webui context.
https://codereview.chromium.org/670173002/ fixes a few that are required for a minimum desktop
signin flow. One important missing piece is the extension messaging api, and as a result Gnubby
does not work with a webview, tracked in crbug/426016.
4. Some standard WebUI features are missing, such as zoom, find, print preview, need to
decide whether it is worth to support them.
5. Webview is not fully accessibility proof. According to webview team, most accessibility bugs
have been fixed by the accessibility team (crbug/330307, crbug/368298), need to confirm.
6. This CL only implements the desktop signin flow. The ChromeOS signin flow is a superset
of the desktop one, and ChromeOS team needs to add extra logic to complete it for ChromeOS.
BUG=364432
Review URL: https://codereview.chromium.org/646983008
Cr-Commit-Position: refs/heads/master@{#301130}
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index dd878a3f..7ee392f 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -1467,6 +1467,13 @@
SINGLE_VALUE_TYPE(switches::kEnableWebBasedSignin)
},
{
+ "enable-webview-based-signin",
+ IDS_FLAGS_ENABLE_WEBVIEW_BASED_SIGNIN_NAME,
+ IDS_FLAGS_ENABLE_WEBVIEW_BASED_SIGNIN_DESCRIPTION,
+ kOsMac | kOsWin | kOsLinux,
+ SINGLE_VALUE_TYPE(switches::kEnableWebviewBasedSignin)
+ },
+ {
"enable-google-profile-info",
IDS_FLAGS_ENABLE_GOOGLE_PROFILE_INFO_NAME,
IDS_FLAGS_ENABLE_GOOGLE_PROFILE_INFO_DESCRIPTION,
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index 1613d0f..17800d05 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -169,9 +169,12 @@
<include name="IDR_HISTORY_JS" file="resources\history\history.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_OTHER_DEVICES_JS" file="resources\history\other_devices.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_IDENTITY_API_SCOPE_APPROVAL_MANIFEST" file="resources\identity_scope_approval_dialog\manifest.json" type="BINDATA" />
+ <include name="IDR_NEW_INLINE_LOGIN_HTML" file="resources\inline_login\new_inline_login.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" />
<include name="IDR_INLINE_LOGIN_HTML" file="resources\inline_login\inline_login.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" />
<include name="IDR_INLINE_LOGIN_CSS" file="resources\inline_login\inline_login.css" flattenhtml="true" type="BINDATA" />
<include name="IDR_INLINE_LOGIN_JS" file="resources\inline_login\inline_login.js" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_GAIA_AUTH_AUTHENTICATOR_JS" file="resources\gaia_auth_host\authenticator.js" flattenhtml="true" type="BINDATA" />
+ <include name="IDR_GAIA_AUTH_HOST_JS" file="resources\gaia_auth_host\gaia_auth_host.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_INSPECT_CSS" file="resources\inspect\inspect.css" flattenhtml="true" type="BINDATA" />
<include name="IDR_INSPECT_HTML" file="resources\inspect\inspect.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" />
<include name="IDR_INSPECT_JS" file="resources\inspect\inspect.js" flattenhtml="true" type="BINDATA" />
diff --git a/chrome/browser/chrome_content_browser_client.cc b/chrome/browser/chrome_content_browser_client.cc
index 0ab93b3..23ca026 100644
--- a/chrome/browser/chrome_content_browser_client.cc
+++ b/chrome/browser/chrome_content_browser_client.cc
@@ -712,7 +712,8 @@
// SiteInstance URL - "chrome-guest://app_id/persist?partition".
if (site.SchemeIs(content::kGuestScheme)) {
partition_id = site.spec();
- } else if (site.GetOrigin().spec() == kChromeUIChromeSigninURL) {
+ } else if (site.GetOrigin().spec() == kChromeUIChromeSigninURL &&
+ !switches::IsEnableWebviewBasedSignin()) {
// Chrome signin page has an embedded iframe of extension and web content,
// thus it must be isolated from other webUI pages.
partition_id = site.GetOrigin().spec();
@@ -776,7 +777,8 @@
}
#endif
- if (!success && (site.GetOrigin().spec() == kChromeUIChromeSigninURL)) {
+ if (!success && (site.GetOrigin().spec() == kChromeUIChromeSigninURL) &&
+ !switches::IsEnableWebviewBasedSignin()) {
// Chrome signin page has an embedded iframe of extension and web content,
// thus it must be isolated from other webUI pages.
*partition_domain = chrome::kChromeUIChromeSigninHost;
diff --git a/chrome/browser/resources/gaia_auth_host/authenticator.js b/chrome/browser/resources/gaia_auth_host/authenticator.js
new file mode 100644
index 0000000..4a1bf5a
--- /dev/null
+++ b/chrome/browser/resources/gaia_auth_host/authenticator.js
@@ -0,0 +1,315 @@
+// Copyright 2014 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 An UI component to authenciate to Chrome. The component hosts
+ * IdP web pages in a webview. A client who is interested in monitoring
+ * authentication events should pass a listener object of type
+ * 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';
+
+ 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';
+
+ /**
+ * The source URL parameter for the constrained signin flow.
+ */
+ var CONSTRAINED_FLOW_SOURCE = 'chrome';
+
+ /**
+ * Enum for the authorization mode, must match AuthMode defined in
+ * chrome/browser/ui/webui/inline_login_ui.cc.
+ * @enum {number}
+ */
+ var AuthMode = {
+ DEFAULT: 0,
+ OFFLINE: 1,
+ DESKTOP: 2
+ };
+
+ /**
+ * Enum for the authorization type.
+ * @enum {number}
+ */
+ var AuthFlow = {
+ DEFAULT: 0,
+ SAML: 1
+ };
+
+ /**
+ * 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) {
+ this.webview_ = typeof webview == 'string' ? $(webview) : webview;
+ assert(this.webview_);
+
+ this.listener_ = opt_listener || null;
+
+ this.email_ = null;
+ this.password_ = null;
+ this.sessionIndex_ = null;
+ this.chooseWhatToSync_ = false;
+ this.skipForNow_ = false;
+ this.authFlow_ = AuthFlow.DEFAULT;
+ this.loaded_ = false;
+ this.idpOrigin_ = null;
+ this.continueUrl_ = null;
+ this.continueUrlWithoutParams_ = null;
+ this.initialFrameUrl_ = null;
+ this.reloadUrl_ = null;
+ }
+
+ // 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: '[email protected]',
+ * 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.
+ */
+ Authenticator.Listener.prototype.onNewWindow = function(e) {};
+
+ /**
+ * Loads the authenticator component with the given parameters.
+ * @param {AuthMode} authMode Authorization mode.
+ * @param {Object} data Parameters for the authorization flow.
+ */
+ Authenticator.prototype.load = function(authMode, data) {
+ 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.initialFrameUrl_ = this.constructInitialFrameUrl_(data);
+ this.reloadUrl_ = data.frameUrl || this.initialFrameUrl_;
+ this.authFlow_ = AuthFlow.DEFAULT;
+
+ 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.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);
+ if (data.hl)
+ url = appendParam(url, 'hl', data.hl);
+ if (data.email)
+ url = appendParam(url, 'Email', data.email);
+ if (this.isConstrainedWindow_)
+ url = appendParam(url, 'source', CONSTRAINED_FLOW_SOURCE);
+ return url;
+ };
+
+ /**
+ * Invoked when a main frame request in the webview has completed.
+ * @private
+ */
+ Authenticator.prototype.onRequestCompleted_ = function(details) {
+ var currentUrl = details.url;
+ if (currentUrl.lastIndexOf(this.continueUrlWithoutParams_, 0) == 0) {
+ if (currentUrl.indexOf('ntp=1') >= 0) {
+ this.skipForNow_ = true;
+ }
+ this.onAuthCompleted_();
+ return;
+ }
+
+ if (this.isConstrainedWindow_) {
+ var isEmbeddedPage = false;
+ if (this.idpOrigin_ && currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
+ var headers = details.responseHeaders;
+ for (var i = 0; headers && i < headers.length; ++i) {
+ if (headers[i].name.toLowerCase() == EMBEDDED_FORM_HEADER) {
+ isEmbeddedPage = true;
+ break;
+ }
+ }
+ }
+ if (!isEmbeddedPage && this.listener_) {
+ this.listener_.onResize(currentUrl);
+ return;
+ }
+ }
+
+ if (currentUrl.lastIndexOf(this.idpOrigin_) == 0) {
+ this.webview_.contentWindow.postMessage({}, currentUrl);
+ }
+
+ if (!this.loaded_) {
+ this.loaded_ = true;
+ if (this.listener_) {
+ this.listener_.onReady();
+ }
+ }
+ };
+
+ /**
+ * Invoked when headers are received in the main frame of the webview. It
+ * 1) reads the authenticated user info from a signin header,
+ * 2) signals the start of a saml flow upon receiving a saml header.
+ * @return {!Object} Modified request headers.
+ * @private
+ */
+ Authenticator.prototype.onHeadersReceived_ = function(details) {
+ var headers = details.responseHeaders;
+ for (var i = 0; headers && i < headers.length; ++i) {
+ var header = headers[i];
+ var headerName = header.name.toLowerCase();
+ if (headerName == SIGN_IN_HEADER) {
+ var headerValues = header.value.toLowerCase().split(',');
+ var signinDetails = {};
+ headerValues.forEach(function(e) {
+ var pair = e.split('=');
+ 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.sessionIndex_ = signinDetails['sessionindex'];
+ } else if (headerName == SAML_HEADER) {
+ this.authFlow_ = AuthFlow.SAML;
+ }
+ }
+ };
+
+ /**
+ * Invoked when an HTML5 message is received.
+ * @param {object} e Payload of the received HTML5 message.
+ * @private
+ */
+ Authenticator.prototype.onMessage_ = function(e) {
+ if (e.origin != this.idpOrigin_) {
+ return;
+ }
+
+ var msg = e.data;
+
+ if (msg.method == 'attemptLogin') {
+ this.email_ = msg.email;
+ this.password_ = msg.password;
+ this.chooseWhatToSync_ = msg.chooseWhatToSync;
+ }
+ };
+
+ /**
+ * Invoked to process authentication completion.
+ * @private
+ */
+ Authenticator.prototype.onAuthCompleted_ = function() {
+ if (!this.listener_) {
+ return;
+ }
+
+ if (!this.email_ && !this.skipForNow_) {
+ 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_ || ''});
+ };
+
+ /**
+ * Invoked when the webview attempts to open a new window.
+ * @private
+ */
+ Authenticator.prototype.onNewWindow_ = function(e) {
+ if (!this.listener_) {
+ return;
+ }
+
+ this.listener_.onNewWindow(e);
+ };
+
+ Authenticator.AuthFlow = AuthFlow;
+ Authenticator.AuthMode = AuthMode;
+
+ return {
+ // TODO(guohui, xiyuan): Rename GaiaAuthHost to Authenticator once the old
+ // iframe-based flow is deprecated.
+ GaiaAuthHost: Authenticator
+ };
+});
diff --git a/chrome/browser/resources/inline_login/inline_login.html b/chrome/browser/resources/inline_login/inline_login.html
index 826780e1..71430c61e 100644
--- a/chrome/browser/resources/inline_login/inline_login.html
+++ b/chrome/browser/resources/inline_login/inline_login.html
@@ -8,6 +8,7 @@
<script src="chrome://resources/js/cr/event_target.js"></script>
<script src="chrome://resources/js/load_time_data.js"></script>
<script src="chrome://resources/js/util.js"></script>
+ <script src="chrome://chrome-signin/gaia_auth_host.js"></script>
<script src="chrome://chrome-signin/inline_login.js"></script>
<script src="chrome://chrome-signin/strings.js"></script>
</head>
diff --git a/chrome/browser/resources/inline_login/inline_login.js b/chrome/browser/resources/inline_login/inline_login.js
index 82ed092..fe41e0b 100644
--- a/chrome/browser/resources/inline_login/inline_login.js
+++ b/chrome/browser/resources/inline_login/inline_login.js
@@ -6,14 +6,12 @@
* @fileoverview Inline login UI.
*/
-<include src="../gaia_auth_host/gaia_auth_host.js">
-
cr.define('inline.login', function() {
'use strict';
/**
* The auth extension host instance.
- * @type {Object}
+ * @type {cr.login.GaiaAuthHost}
*/
var authExtHost;
@@ -23,6 +21,34 @@
var authReadyFired;
/**
+ * A listener class for authentication events from GaiaAuthHost.
+ * @constructor
+ * @implements {cr.login.GaiaAuthHost.Listener}
+ */
+ function GaiaAuthHostListener() {}
+
+ /** @override */
+ GaiaAuthHostListener.prototype.onSuccess = function(credentials) {
+ onAuthCompleted(credentials);
+ };
+
+ /** @override */
+ GaiaAuthHostListener.prototype.onReady = function(e) {
+ onAuthReady();
+ };
+
+ /** @override */
+ GaiaAuthHostListener.prototype.onResize = function(url) {
+ chrome.send('switchToFullTab', url);
+ };
+
+ /** @override */
+ GaiaAuthHostListener.prototype.onNewWindow = function(e) {
+ window.open(e.targetUrl, '_blank');
+ e.window.discard();
+ };
+
+ /**
* Handler of auth host 'ready' event.
*/
function onAuthReady() {
@@ -43,7 +69,8 @@
* Initialize the UI.
*/
function initialize() {
- authExtHost = new cr.login.GaiaAuthHost('signin-frame');
+ authExtHost = new cr.login.GaiaAuthHost(
+ 'signin-frame', new GaiaAuthHostListener());
authExtHost.addEventListener('ready', onAuthReady);
chrome.send('initialize');
diff --git a/chrome/browser/resources/inline_login/new_inline_login.html b/chrome/browser/resources/inline_login/new_inline_login.html
new file mode 100644
index 0000000..68da6c2
--- /dev/null
+++ b/chrome/browser/resources/inline_login/new_inline_login.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html i18n-values="dir:textdirection">
+<head>
+ <title i18n-content="title"></title>
+ <link rel="stylesheet" href="chrome://resources/css/spinner.css">
+ <link rel="stylesheet" href="chrome://chrome-signin/inline_login.css">
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="chrome://resources/js/cr/event_target.js"></script>
+ <script src="chrome://resources/js/load_time_data.js"></script>
+ <script src="chrome://resources/js/util.js"></script>
+ <script src="chrome://chrome-signin/gaia_auth_host.js"></script>
+ <script src="chrome://chrome-signin/inline_login.js"></script>
+ <script src="chrome://chrome-signin/strings.js"></script>
+</head>
+<body i18n-values=".style.fontFamily:fontfamily;.style.fontSize:fontsize">
+ <div id="contents" class="loading">
+ <webview id="signin-frame" name="signin-frame"></webview>
+ <div id="spinner-container">
+ <div class="spinner"></div>
+ </div>
+ </div>
+ <script src="chrome://resources/js/i18n_template2.js"></script>
+</body>
+</html>
diff --git a/chrome/browser/ui/webui/signin/inline_login_handler_impl.cc b/chrome/browser/ui/webui/signin/inline_login_handler_impl.cc
index 13c59ca..b54ac9d 100644
--- a/chrome/browser/ui/webui/signin/inline_login_handler_impl.cc
+++ b/chrome/browser/ui/webui/signin/inline_login_handler_impl.cc
@@ -384,10 +384,12 @@
about_signin_internals->OnAuthenticationResultReceived(
"GAIA Auth Successful");
+ GURL partition_url(switches::IsEnableWebviewBasedSignin() ?
+ "chrome-guest://chrome-signin/?" :
+ chrome::kChromeUIChromeSigninURL);
content::StoragePartition* partition =
content::BrowserContext::GetStoragePartitionForSite(
- contents->GetBrowserContext(),
- GURL(chrome::kChromeUIChromeSigninURL));
+ contents->GetBrowserContext(), partition_url);
SigninClient* signin_client =
ChromeSigninClientFactory::GetForProfile(Profile::FromWebUI(web_ui()));
diff --git a/chrome/browser/ui/webui/signin/inline_login_ui.cc b/chrome/browser/ui/webui/signin/inline_login_ui.cc
index cf3ec66..9fd5b1e 100644
--- a/chrome/browser/ui/webui/signin/inline_login_ui.cc
+++ b/chrome/browser/ui/webui/signin/inline_login_ui.cc
@@ -9,6 +9,7 @@
#include "chrome/browser/sessions/session_tab_helper.h"
#include "chrome/common/url_constants.h"
#include "chrome/grit/chromium_strings.h"
+#include "components/signin/core/common/profile_management_switches.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_ui.h"
#include "content/public/browser/web_ui_data_source.h"
@@ -28,9 +29,13 @@
source->OverrideContentSecurityPolicyObjectSrc("object-src *;");
source->SetJsonPath("strings.js");
- source->SetDefaultResource(IDR_INLINE_LOGIN_HTML);
+ bool is_webview_signin_enabled = switches::IsEnableWebviewBasedSignin();
+ source->SetDefaultResource(is_webview_signin_enabled ?
+ IDR_NEW_INLINE_LOGIN_HTML : IDR_INLINE_LOGIN_HTML);
source->AddResourcePath("inline_login.css", IDR_INLINE_LOGIN_CSS);
source->AddResourcePath("inline_login.js", IDR_INLINE_LOGIN_JS);
+ source->AddResourcePath("gaia_auth_host.js", is_webview_signin_enabled ?
+ IDR_GAIA_AUTH_AUTHENTICATOR_JS : IDR_GAIA_AUTH_HOST_JS);
source->AddLocalizedString("title", IDS_CHROME_SIGNIN_TITLE);
return source;