summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources/gaia_auth_host
diff options
context:
space:
mode:
Diffstat (limited to 'chromium/chrome/browser/resources/gaia_auth_host')
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/authenticator.js12
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/okta_detect_success_injected.js58
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator.js159
-rw-r--r--chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator_test.unitjs76
4 files changed, 293 insertions, 12 deletions
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/authenticator.js b/chromium/chrome/browser/resources/gaia_auth_host/authenticator.js
index 7161b0893c1..6881172f6dc 100644
--- a/chromium/chrome/browser/resources/gaia_auth_host/authenticator.js
+++ b/chromium/chrome/browser/resources/gaia_auth_host/authenticator.js
@@ -241,6 +241,7 @@ cr.define('cr.login', function() {
this.confirmPasswordCallback = null;
this.noPasswordCallback = null;
+ this.onePasswordCallback = null;
this.insecureContentBlockedCallback = null;
this.samlApiUsedCallback = null;
this.missingGaiaInfoCallback = null;
@@ -848,7 +849,9 @@ cr.define('cr.login', function() {
if (this.samlHandler_.samlApiUsed) {
if (this.samlApiUsedCallback) {
- this.samlApiUsedCallback();
+ // Makes distinction between Gaia and Chrome Credentials Passing API
+ // login to properly fill ChromeOS.SAML.ApiLogin metrics.
+ this.samlApiUsedCallback(this.authFlow == AuthFlow.SAML);
}
this.password_ = this.samlHandler_.apiPasswordBytes;
this.onAuthCompleted_();
@@ -870,6 +873,9 @@ cr.define('cr.login', function() {
// If we scraped exactly one password, we complete the
// authentication right away.
this.password_ = this.samlHandler_.firstScrapedPassword;
+ if (this.onePasswordCallback) {
+ this.onePasswordCallback();
+ }
this.onAuthCompleted_();
return;
}
@@ -888,8 +894,8 @@ cr.define('cr.login', function() {
/**
* Invoked to complete the authentication using the password the user
- * enters manually for non-principals API SAML IdPs that we couldn't
- * scrape their password input.
+ * enters manually for SAML IdPs that do not use Chrome Credentials Passing
+ * API and we couldn't scrape their password input.
*/
completeAuthWithManualPassword(password) {
this.password_ = password;
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/okta_detect_success_injected.js b/chromium/chrome/browser/resources/gaia_auth_host/okta_detect_success_injected.js
new file mode 100644
index 00000000000..d2b13e7c2bc
--- /dev/null
+++ b/chromium/chrome/browser/resources/gaia_auth_host/okta_detect_success_injected.js
@@ -0,0 +1,58 @@
+// Copyright 2019 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.
+
+/**
+ * Intercept Ajax responses, detect responses to the password-change endpoint
+ * that don't contain any errors.
+ */
+(function() {
+function oktaDetectSuccess() {
+ const PARENT_ORIGIN = 'chrome://password-change';
+
+ let messageFromParent;
+ function onMessageReceived(event) {
+ if (event.origin == PARENT_ORIGIN) {
+ messageFromParent = event;
+ }
+ }
+ window.addEventListener('message', onMessageReceived, false);
+
+ function checkResponse(responseUrl, responseData) {
+ if (responseUrl.includes('/internal_login/password') &&
+ !responseData.match(/"has[A-Za-z]*Errors":true/)) {
+ console.info('passwordChangeSuccess');
+ messageFromParent.source.postMessage(
+ 'passwordChangeSuccess', PARENT_ORIGIN);
+ }
+ }
+
+ const proxied = window.XMLHttpRequest.prototype.send;
+
+ window.XMLHttpRequest.prototype.send = function() {
+ this.addEventListener('load', function() {
+ checkResponse(this.responseURL, this.response);
+ });
+ return proxied.apply(this, arguments);
+ };
+}
+
+/** Run a script in the window context - not isolated as a content-script. */
+function runInPageContext(jsFn) {
+ const script = document.createElement('script');
+ script.type = 'text/javascript';
+ script.innerHTML = '(' + jsFn + ')();';
+ document.head.prepend(script);
+}
+
+/** Wait until DOM is loaded, then run oktaDetectSuccess script. */
+function initialize() {
+ if (document.body && document.head) {
+ console.info('initialize');
+ runInPageContext(oktaDetectSuccess);
+ } else {
+ requestIdleCallback(initialize);
+ }
+}
+requestIdleCallback(initialize);
+})();
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator.js b/chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator.js
index 0f5fa464c5f..7854c1b20cb 100644
--- a/chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator.js
+++ b/chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator.js
@@ -12,9 +12,105 @@
cr.define('cr.samlPasswordChange', function() {
'use strict';
+ /** @const */
+ const oktaInjectedScriptName = 'oktaInjected';
+
+ /**
+ * The script to inject into Okta user settings page.
+ * @type {string}
+ */
+ const oktaInjectedJs = String.raw`
+ // <include src="okta_detect_success_injected.js">
+ `;
+
const BLANK_PAGE_URL = 'about:blank';
/**
+ * The different providers of password-change pages that we support, or are
+ * working on supporting.
+ * @enum {number}
+ */
+ const PasswordChangePageProvider = {
+ UNKNOWN: 0,
+ ADFS: 1,
+ AZURE: 2,
+ OKTA: 3,
+ PING: 4,
+ };
+
+ /**
+ * @param {URL?} url The url of the webpage that is being interacted with.
+ * @return {PasswordChangePageProvider} The provider of the password change
+ * page, as detected based on the URL.
+ */
+ function detectProvider_(url) {
+ if (!url) {
+ return null;
+ }
+ if (url.pathname.match(/\/updatepassword\/?$/)) {
+ return PasswordChangePageProvider.ADFS;
+ }
+ if (url.pathname.endsWith('/ChangePassword.aspx')) {
+ return PasswordChangePageProvider.AZURE;
+ }
+ if (url.host.match(/\.okta\.com$/)) {
+ return PasswordChangePageProvider.OKTA;
+ }
+ if (url.pathname.match('/password/chg/')) {
+ return PasswordChangePageProvider.PING;
+ }
+ return PasswordChangePageProvider.UNKNOWN;
+ }
+
+ /**
+ * @param {string?} str A string that should be a valid URL.
+ * @return {URL?} A valid URL object, or null.
+ */
+ function safeParseUrl_(str) {
+ try {
+ return new URL(str);
+ } catch (error) {
+ console.error('Invalid url: ' + str);
+ return null;
+ }
+ }
+
+ /**
+ * @param {Object} details The web-request details.
+ * @return {boolean} True if we detect that a password change was successful.
+ */
+ function detectPasswordChangeSuccess(details) {
+ const url = safeParseUrl_(details.url);
+ if (!url) {
+ return false;
+ }
+
+ // We count it as a success whenever "status=0" is in the query params.
+ // This is what we use for ADFS, but for now, we allow it for every IdP, so
+ // that an otherwise unsupported IdP can also send it as a success message.
+ // TODO(https://crbug.com/930109): Consider removing this entirely, or,
+ // using a more self-documenting parameter like 'passwordChanged=1'.
+ if (url.searchParams.get('status') == '0') {
+ return true;
+ }
+
+ const pageProvider = detectProvider_(url);
+ // These heuristics work for the following SAML IdPs:
+ if (pageProvider == PasswordChangePageProvider.ADFS) {
+ return url.searchParams.get('status') == '0';
+ }
+ if (pageProvider == PasswordChangePageProvider.AZURE) {
+ return url.searchParams.get('ReturnCode') == '0';
+ }
+
+ // We can't currently detect success for Okta or Ping just by inspecting the
+ // URL or even response headers. To inspect the response body, we need
+ // to inject scripts onto their page (see okta_detect_success_injected.js).
+
+ return false;
+ }
+
+ /**
* Initializes the authenticator component.
*/
class Authenticator extends cr.EventTarget {
@@ -67,8 +163,27 @@ cr.define('cr.samlPasswordChange', function() {
this.samlHandler_, 'authPageLoaded',
this.onAuthPageLoaded_.bind(this));
+ // Listen for completed main-frame requests to check for password-change
+ // success.
+ this.webviewEventManager_.addWebRequestEventListener(
+ this.webview_.request.onCompleted,
+ this.onCompleted_.bind(this),
+ {urls: ['*://*/*'], types: ['main_frame']},
+ );
+
+ // Inject a custom script for detecting password change success in Okta.
+ this.webview_.addContentScripts([{
+ name: oktaInjectedScriptName,
+ matches: ['*://*.okta.com/*'],
+ js: {code: oktaInjectedJs},
+ all_frames: true,
+ run_at: 'document_start'
+ }]);
+
+ // Okta-detect-success-inject script signals success by posting a message
+ // that says "passwordChangeSuccess", which we listen for:
this.webviewEventManager_.addEventListener(
- this.webview_, 'contentload', this.onContentLoad_.bind(this));
+ window, 'message', this.onMessageReceived_.bind(this));
}
/**
@@ -129,7 +244,7 @@ cr.define('cr.samlPasswordChange', function() {
* Sends scraped password and resets the state.
* @private
*/
- completeAuth_() {
+ onPasswordChangeSuccess_() {
const passwordsOnce = this.samlHandler_.getPasswordsScrapedTimes(1);
const passwordsTwice = this.samlHandler_.getPasswordsScrapedTimes(2);
@@ -151,17 +266,43 @@ cr.define('cr.samlPasswordChange', function() {
}
/**
- * Invoked when a new document is loaded.
+ * Invoked when a new document loading completes.
+ * @param {Object} details The web-request details.
* @private
*/
- onContentLoad_(e) {
- const currentUrl = this.webview_.src;
- // TODO(rsorokin): Implement more robust check.
- if (currentUrl.lastIndexOf('status=0') != -1) {
- this.completeAuth_();
+ onCompleted_(details) {
+ if (detectPasswordChangeSuccess(details)) {
+ this.onPasswordChangeSuccess_();
+ }
+
+ // Okta_detect_success_injected.js needs to be contacted by the parent,
+ // so that it can send messages back to the parent.
+ const pageProvider = detectProvider_(safeParseUrl_(details.url));
+ if (pageProvider == PasswordChangePageProvider.OKTA) {
+ // Using setTimeout gives the page time to finish initializing.
+ setTimeout(() => {
+ this.webview_.contentWindow.postMessage('connect', details.url);
+ }, 1000);
+ }
+ }
+
+ /**
+ * Invoked when the webview posts a message.
+ * @param {Object} event The message event.
+ * @private
+ */
+ onMessageReceived_(event) {
+ if (event.data == 'passwordChangeSuccess') {
+ const pageProvider = detectProvider_(safeParseUrl_(event.origin));
+ if (pageProvider == PasswordChangePageProvider.OKTA) {
+ this.onPasswordChangeSuccess_();
+ }
}
}
}
- return {Authenticator: Authenticator};
+ return {
+ Authenticator: Authenticator,
+ detectPasswordChangeSuccess: detectPasswordChangeSuccess,
+ };
});
diff --git a/chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator_test.unitjs b/chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator_test.unitjs
new file mode 100644
index 00000000000..617677b55ee
--- /dev/null
+++ b/chromium/chrome/browser/resources/gaia_auth_host/password_change_authenticator_test.unitjs
@@ -0,0 +1,76 @@
+// Copyright 2019 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.
+
+GEN_INCLUDE(['//ui/webui/resources/js/cr.js']);
+
+const EXAMPLE_ADFS_ENDPOINT =
+ 'https://example.com/adfs/portal/updatepassword/';
+
+const EXAMPLE_AZURE_ENDPOINT =
+ 'https://example.windowsazure.com/ChangePassword.aspx';
+
+const EXAMPLE_OKTA_ENDPOINT =
+ 'https://example.okta.com/user/profile/internal_login/password';
+
+const EXAMPLE_PING_ENDPOINT =
+ 'https://login.pingone.com/idp/directory/a/12345/password/chg/67890';
+
+PasswordChangeAuthenticatorUnitTest = class extends testing.Test {
+ get browsePreload() {
+ return DUMMY_URL;
+ }
+
+ // No need to run these checks - see comment in SamlPasswordAttributesTest.
+ get runAccessibilityChecks() {
+ return false;
+ }
+
+ get extraLibraries() {
+ return [
+ '//ui/webui/resources/js/cr/event_target.js',
+ 'password_change_authenticator.js',
+ ];
+ }
+
+ assertSuccess(details) {
+ assertTrue(this.detectSuccess(details));
+ }
+
+ assertNotSuccess(details, responseData) {
+ assertFalse(this.detectSuccess(details));
+ }
+
+ detectSuccess(details) {
+ if (typeof details == 'string') {
+ details = {'url': details};
+ }
+ return cr.samlPasswordChange.detectPasswordChangeSuccess(details);
+ }
+}
+
+TEST_F('PasswordChangeAuthenticatorUnitTest', 'DetectAdfsSuccess', function() {
+ const endpointUrl = EXAMPLE_ADFS_ENDPOINT;
+
+ this.assertNotSuccess(endpointUrl);
+ this.assertNotSuccess(endpointUrl + '?status=1');
+ this.assertSuccess(endpointUrl + '?status=0');
+
+ // We allow "status=0" to count as success everywhere right now, but this
+ // should be narrowed down to ADFS - see the TODO in the code.
+ this.assertSuccess(EXAMPLE_AZURE_ENDPOINT + '?status=0');
+});
+
+TEST_F('PasswordChangeAuthenticatorUnitTest', 'DetectAzureSuccess', function() {
+ const endpointUrl = EXAMPLE_AZURE_ENDPOINT;
+ const extraParam = 'BrandContextID=O123';
+
+ this.assertNotSuccess(endpointUrl);
+ this.assertNotSuccess(endpointUrl + '?' + extraParam);
+ this.assertNotSuccess(endpointUrl + '?ReturnCode=1&' + extraParam);
+ this.assertNotSuccess(endpointUrl + '?' + extraParam + '&ReturnCode=1');
+ this.assertNotSuccess(EXAMPLE_PING_ENDPOINT + '?ReturnCode=0');
+
+ this.assertSuccess(endpointUrl + '?ReturnCode=0&' + extraParam);
+ this.assertSuccess(endpointUrl + '?' + extraParam + '&ReturnCode=0');
+}); \ No newline at end of file