diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-01-23 17:21:03 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-01-23 16:25:15 +0000 |
commit | c551f43206405019121bd2b2c93714319a0a3300 (patch) | |
tree | 1f48c30631c421fd4bbb3c36da20183c8a2ed7d7 /chromium/chrome/browser/resources/gaia_auth_host | |
parent | 7961cea6d1041e3e454dae6a1da660b453efd238 (diff) |
BASELINE: Update Chromium to 79.0.3945.139
Change-Id: I336b7182fab9bca80b709682489c07db112eaca5
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/chrome/browser/resources/gaia_auth_host')
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 |