Commit 6d00e32f authored by A Olsen's avatar A Olsen Committed by Commit Bot

PW change success detection for Ping

Ping does not have a particular success URL -
it just redirects back to the "returnurl" on success.

We can detect this if we listen for a POSTed request
to the password change URL, and then for a response that
redirects us to the returnurl, and not back to the
password change URL again.

This involves listening for redirects, not just URLs -
since visiting the returnurl normally would not mean
success.

I have modified all such success detection attempts to
listen for redirects - even when we have a success URL
to listen for eg "?status=0" - it is a bit more certain
to listen for a redirect *from* the password change URL
*to* the success URL, instead of just listening for a
visit to the success URL by itself.

Bug: 930109
Change-Id: Idfd081a603ca402367de774f6253e4af9b84bbdf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1878489Reviewed-by: default avatarRoman Sorokin [CET] <rsorokin@chromium.org>
Commit-Queue: A Olsen <olsen@chromium.org>
Cr-Commit-Position: refs/heads/master@{#711235}
parent 64af02ca
...@@ -76,12 +76,12 @@ cr.define('cr.samlPasswordChange', function() { ...@@ -76,12 +76,12 @@ cr.define('cr.samlPasswordChange', function() {
} }
/** /**
* @param {Object} details The web-request details. * @param {URL?} postUrl Where the password change request was POSTed.
* @param {URL?} redirectUrl Where the response redirected the browser.
* @return {boolean} True if we detect that a password change was successful. * @return {boolean} True if we detect that a password change was successful.
*/ */
function detectPasswordChangeSuccess(details) { function detectPasswordChangeSuccess(postUrl, redirectUrl) {
const url = safeParseUrl_(details.url); if (!postUrl || !redirectUrl) {
if (!url) {
return false; return false;
} }
...@@ -90,20 +90,26 @@ cr.define('cr.samlPasswordChange', function() { ...@@ -90,20 +90,26 @@ cr.define('cr.samlPasswordChange', function() {
// that an otherwise unsupported IdP can also send it as a success message. // that an otherwise unsupported IdP can also send it as a success message.
// TODO(https://crbug.com/930109): Consider removing this entirely, or, // TODO(https://crbug.com/930109): Consider removing this entirely, or,
// using a more self-documenting parameter like 'passwordChanged=1'. // using a more self-documenting parameter like 'passwordChanged=1'.
if (url.searchParams.get('status') == '0') { if (redirectUrl.searchParams.get('status') == '0') {
return true; return true;
} }
const pageProvider = detectProvider_(url); const pageProvider = detectProvider_(postUrl);
// These heuristics work for the following SAML IdPs: // These heuristics work for the following SAML IdPs:
if (pageProvider == PasswordChangePageProvider.ADFS) { if (pageProvider == PasswordChangePageProvider.ADFS) {
return url.searchParams.get('status') == '0'; return redirectUrl.searchParams.get('status') == '0';
} }
if (pageProvider == PasswordChangePageProvider.AZURE) { if (pageProvider == PasswordChangePageProvider.AZURE) {
return url.searchParams.get('ReturnCode') == '0'; return redirectUrl.searchParams.get('ReturnCode') == '0';
}
if (pageProvider == PasswordChangePageProvider.PING) {
// The returnurl is always preserved until password change succeeds - then
// it is no longer needed.
return !!postUrl.searchParams.get('returnurl') &&
!redirectUrl.searchParams.get('returnurl');
} }
// We can't currently detect success for Okta or Ping just by inspecting the // We can't currently detect success for Okta just by inspecting the
// URL or even response headers. To inspect the response body, we need // URL or even response headers. To inspect the response body, we need
// to inject scripts onto their page (see okta_detect_success_injected.js). // to inject scripts onto their page (see okta_detect_success_injected.js).
...@@ -163,11 +169,12 @@ cr.define('cr.samlPasswordChange', function() { ...@@ -163,11 +169,12 @@ cr.define('cr.samlPasswordChange', function() {
this.samlHandler_, 'authPageLoaded', this.samlHandler_, 'authPageLoaded',
this.onAuthPageLoaded_.bind(this)); this.onAuthPageLoaded_.bind(this));
// Listen for completed main-frame requests to check for password-change // Listen for main-frame redirects to check for success - we can mostly
// success. // detect success by detecting we POSTed something to the password-change
// URL, and the response redirected us to a particular success URL.
this.webviewEventManager_.addWebRequestEventListener( this.webviewEventManager_.addWebRequestEventListener(
this.webview_.request.onCompleted, this.webview_.request.onBeforeRedirect,
this.onCompleted_.bind(this), this.onBeforeRedirect_.bind(this),
{urls: ['*://*/*'], types: ['main_frame']}, {urls: ['*://*/*'], types: ['main_frame']},
); );
...@@ -180,6 +187,13 @@ cr.define('cr.samlPasswordChange', function() { ...@@ -180,6 +187,13 @@ cr.define('cr.samlPasswordChange', function() {
run_at: 'document_start' run_at: 'document_start'
}]); }]);
// Connect to the script running in Okta web pages once it loads.
this.webviewEventManager_.addWebRequestEventListener(
this.webview_.request.onCompleted,
this.onOktaCompleted_.bind(this),
{urls: ['*://*.okta.com/*'], types: ['main_frame']},
);
// Okta-detect-success-inject script signals success by posting a message // Okta-detect-success-inject script signals success by posting a message
// that says "passwordChangeSuccess", which we listen for: // that says "passwordChangeSuccess", which we listen for:
this.webviewEventManager_.addEventListener( this.webviewEventManager_.addEventListener(
...@@ -265,25 +279,32 @@ cr.define('cr.samlPasswordChange', function() { ...@@ -265,25 +279,32 @@ cr.define('cr.samlPasswordChange', function() {
this.webview_.focus(); this.webview_.focus();
} }
/** /**
* Invoked when a new document loading completes. * Invoked when a new document loading completes.
* @param {Object} details The web-request details. * @param {Object} details The web-request details.
* @private * @private
*/ */
onCompleted_(details) { onBeforeRedirect_(details) {
if (detectPasswordChangeSuccess(details)) { if (details.method == 'POST' &&
detectPasswordChangeSuccess(
safeParseUrl_(details.url), safeParseUrl_(details.redirectUrl))) {
this.onPasswordChangeSuccess_(); this.onPasswordChangeSuccess_();
} }
}
/**
* Invoked when loading completes on an Okta page.
* @param {Object} details The web-request details.
* @private
*/
onOktaCompleted_(details) {
// Okta_detect_success_injected.js needs to be contacted by the parent, // Okta_detect_success_injected.js needs to be contacted by the parent,
// so that it can send messages back to the parent. // so that it can send messages back to the parent.
const pageProvider = detectProvider_(safeParseUrl_(details.url)); // Using setTimeout gives the page time to finish initializing.
if (pageProvider == PasswordChangePageProvider.OKTA) { setTimeout(() => {
// Using setTimeout gives the page time to finish initializing. this.webview_.contentWindow.postMessage('connect', details.url);
setTimeout(() => { }, 1000);
this.webview_.contentWindow.postMessage('connect', details.url);
}, 1000);
}
} }
/** /**
......
...@@ -33,44 +33,67 @@ PasswordChangeAuthenticatorUnitTest = class extends testing.Test { ...@@ -33,44 +33,67 @@ PasswordChangeAuthenticatorUnitTest = class extends testing.Test {
]; ];
} }
assertSuccess(details) { assertSuccess(postUrl, redirectUrl) {
assertTrue(this.detectSuccess(details)); assertTrue(this.detectSuccess(postUrl, redirectUrl));
} }
assertNotSuccess(details, responseData) { assertNotSuccess(postUrl, redirectUrl) {
assertFalse(this.detectSuccess(details)); assertFalse(this.detectSuccess(postUrl, redirectUrl));
} }
detectSuccess(details) { detectSuccess(postUrl, redirectUrl) {
if (typeof details == 'string') { postUrl = (typeof postUrl == 'string') ? new URL(postUrl) : postUrl;
details = {'url': details}; redirectUrl =
} (typeof redirectUrl == 'string') ? new URL(redirectUrl) : redirectUrl;
return cr.samlPasswordChange.detectPasswordChangeSuccess(details); return cr.samlPasswordChange.detectPasswordChangeSuccess(postUrl,
redirectUrl);
} }
} }
TEST_F('PasswordChangeAuthenticatorUnitTest', 'DetectAdfsSuccess', function() { TEST_F('PasswordChangeAuthenticatorUnitTest', 'DetectAdfsSuccess', function() {
const endpointUrl = EXAMPLE_ADFS_ENDPOINT; const endpointUrl = EXAMPLE_ADFS_ENDPOINT;
this.assertNotSuccess(endpointUrl); this.assertNotSuccess(endpointUrl, endpointUrl);
this.assertNotSuccess(endpointUrl + '?status=1'); this.assertNotSuccess(endpointUrl, endpointUrl + '?status=1');
this.assertSuccess(endpointUrl + '?status=0');
this.assertSuccess(endpointUrl, endpointUrl + '?status=0');
this.assertSuccess(endpointUrl + '?status=1', endpointUrl + '?status=0');
// We allow "status=0" to count as success everywhere right now, but this // 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. // should be narrowed down to ADFS - see the TODO in the code.
this.assertSuccess(EXAMPLE_AZURE_ENDPOINT + '?status=0'); this.assertSuccess(EXAMPLE_AZURE_ENDPOINT,
EXAMPLE_AZURE_ENDPOINT + '?status=0');
}); });
TEST_F('PasswordChangeAuthenticatorUnitTest', 'DetectAzureSuccess', function() { TEST_F('PasswordChangeAuthenticatorUnitTest', 'DetectAzureSuccess', function() {
const endpointUrl = EXAMPLE_AZURE_ENDPOINT; const endpointUrl = EXAMPLE_AZURE_ENDPOINT;
const extraParam = 'BrandContextID=O123'; const extraParam = 'BrandContextID=O123';
this.assertNotSuccess(endpointUrl); this.assertNotSuccess(endpointUrl, endpointUrl);
this.assertNotSuccess(endpointUrl + '?' + extraParam); this.assertNotSuccess(endpointUrl, endpointUrl + '?' + extraParam);
this.assertNotSuccess(endpointUrl + '?ReturnCode=1&' + extraParam); this.assertNotSuccess(endpointUrl, endpointUrl + '?ReturnCode=1&' + extraParam);
this.assertNotSuccess(endpointUrl + '?' + extraParam + '&ReturnCode=1'); this.assertNotSuccess(endpointUrl,
this.assertNotSuccess(EXAMPLE_PING_ENDPOINT + '?ReturnCode=0'); endpointUrl + '?' + extraParam + '&ReturnCode=1');
this.assertNotSuccess(EXAMPLE_PING_ENDPOINT,
endpointUrl + '?ReturnCode=0');
this.assertSuccess(endpointUrl,
endpointUrl + '?ReturnCode=0');
this.assertSuccess(endpointUrl + '?' + extraParam,
endpointUrl + '?ReturnCode=0&' + extraParam);
this.assertSuccess(endpointUrl + '?' + extraParam,
endpointUrl + '?' + extraParam + '&ReturnCode=0');
});
TEST_F('PasswordChangeAuthenticatorUnitTest', 'DetectPingSuccess', function() {
const endpointUrl = EXAMPLE_PING_ENDPOINT;
this.assertNotSuccess(endpointUrl, endpointUrl);
this.assertNotSuccess(endpointUrl + '?returnurl=https://desktop.pingone.com',
endpointUrl + '?returnurl=https://desktop.pingone.com');
this.assertNotSuccess(endpointUrl,
endpointUrl + '?returnurl=https://desktop.pingone.com');
this.assertSuccess(endpointUrl + '?ReturnCode=0&' + extraParam); this.assertSuccess(endpointUrl + '?returnurl=https://desktop.pingone.com',
this.assertSuccess(endpointUrl + '?' + extraParam + '&ReturnCode=0'); 'https://desktop.pingone.com/Selection?cmd=selection');
}); });
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment