From f23d655fc727c02cda809a188cea0a5da9dbb607 Mon Sep 17 00:00:00 2001
From: Alain O'Dea
Date: Sat, 23 Feb 2019 21:21:01 -0330
Subject: [PATCH] :bug: The browser just appears white
The Okta login page is not rendered when OKTA_BROWSER_AUTH=true. This
started happening after Okta released 2019.02.0, which introduced
subresource integrity checks on certain JavaScript resources.
This bug appears to be Windows specific. I could not reproduce it on
any macOS Mojave configuration regardless of JDK 1.8 or JDK 11 version
I tried.
The root cause is that JavaFX WebEngine on Java 1.8.0_162 or later on
Windows does not correctly handle all subresource integrity checks and
will refuse to load certain referenced resources like CSS and
JavaScript.
- Strip subresource integrity directives from DOM before it gets to
the JavaFX WebView (this is a hack, and an ugly one)
Resolves #272
---
.../authentication/BrowserAuthentication.java | 4 +-
.../LoginPageInterceptingProtocolHandler.java | 41 +++++++++
.../io/SubresourceIntegrityStrippingHack.java | 31 +++++++
...sourceIntegrityStrippingURLConnection.java | 83 +++++++++++++++++++
4 files changed, 158 insertions(+), 1 deletion(-)
create mode 100644 src/main/java/com/okta/tools/io/LoginPageInterceptingProtocolHandler.java
create mode 100644 src/main/java/com/okta/tools/io/SubresourceIntegrityStrippingHack.java
create mode 100644 src/main/java/com/okta/tools/io/SubresourceIntegrityStrippingURLConnection.java
diff --git a/src/main/java/com/okta/tools/authentication/BrowserAuthentication.java b/src/main/java/com/okta/tools/authentication/BrowserAuthentication.java
index d1c6d24..fe1765c 100644
--- a/src/main/java/com/okta/tools/authentication/BrowserAuthentication.java
+++ b/src/main/java/com/okta/tools/authentication/BrowserAuthentication.java
@@ -2,6 +2,7 @@
import com.okta.tools.OktaAwsCliEnvironment;
import com.okta.tools.helpers.CookieHelper;
+import com.okta.tools.io.SubresourceIntegrityStrippingHack;
import com.okta.tools.util.NodeListIterable;
import com.sun.javafx.webkit.WebConsoleListener;
import javafx.application.Application;
@@ -61,6 +62,7 @@ public void start(final Stage stage) throws IOException {
URI uri = URI.create(ENVIRONMENT.oktaAwsAppUrl);
initializeCookies(uri);
+ SubresourceIntegrityStrippingHack.overrideHttpsProtocolHandler(ENVIRONMENT);
webEngine.getLoadWorker().stateProperty()
.addListener((ov, oldState, newState) -> {
if (webEngine.getDocument() != null) {
@@ -75,7 +77,7 @@ public void start(final Stage stage) throws IOException {
});
WebConsoleListener.setDefaultListener((webView, message, lineNumber, sourceId) -> {
- System.out.println("WebConsoleListener: " + message + "[at " + lineNumber + "]");
+ System.out.println("WebConsoleListener: " + message + "[" + webEngine.getLocation() + ":" + lineNumber + "]");
});
webEngine.load(uri.toASCIIString());
diff --git a/src/main/java/com/okta/tools/io/LoginPageInterceptingProtocolHandler.java b/src/main/java/com/okta/tools/io/LoginPageInterceptingProtocolHandler.java
new file mode 100644
index 0000000..ef2bb05
--- /dev/null
+++ b/src/main/java/com/okta/tools/io/LoginPageInterceptingProtocolHandler.java
@@ -0,0 +1,41 @@
+package com.okta.tools.io;
+
+import com.okta.tools.OktaAwsCliEnvironment;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.Arrays;
+import java.util.function.BiFunction;
+import java.util.logging.Logger;
+
+final class LoginPageInterceptingProtocolHandler extends sun.net.www.protocol.https.Handler {
+ private static final Logger LOGGER = Logger.getLogger(LoginPageInterceptingProtocolHandler.class.getName());
+ private final OktaAwsCliEnvironment environment;
+ private final BiFunction filteringUrlConnectionFactory;
+
+ LoginPageInterceptingProtocolHandler(OktaAwsCliEnvironment environment, BiFunction filteringUrlConnectionFactory) {
+ this.environment = environment;
+ this.filteringUrlConnectionFactory = filteringUrlConnectionFactory;
+ }
+
+ @Override
+ protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
+ URLConnection urlConnection = super.openConnection(url, proxy);
+ if (environment.oktaOrg.equals(url.getHost()) &&
+ Arrays.asList(
+ URI.create(environment.oktaAwsAppUrl).getPath(),
+ "/login/login.htm",
+ "/auth/services/devicefingerprint"
+ ).contains(url.getPath())
+ ) {
+ LOGGER.finest(() -> String.format("[%s] Using filtering URLConnection", url));
+ return filteringUrlConnectionFactory.apply(url, urlConnection);
+ } else {
+ LOGGER.finest(() -> String.format("[%s] Using unmodified URLConnection", url));
+ return urlConnection;
+ }
+ }
+}
diff --git a/src/main/java/com/okta/tools/io/SubresourceIntegrityStrippingHack.java b/src/main/java/com/okta/tools/io/SubresourceIntegrityStrippingHack.java
new file mode 100644
index 0000000..8162869
--- /dev/null
+++ b/src/main/java/com/okta/tools/io/SubresourceIntegrityStrippingHack.java
@@ -0,0 +1,31 @@
+package com.okta.tools.io;
+
+import com.okta.tools.OktaAwsCliEnvironment;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.net.URL;
+import java.util.logging.Logger;
+
+public class SubresourceIntegrityStrippingHack {
+ private static final Logger LOGGER = Logger.getLogger(SubresourceIntegrityStrippingHack.class.getName());
+
+ private SubresourceIntegrityStrippingHack() {}
+
+ public static void overrideHttpsProtocolHandler(OktaAwsCliEnvironment environment) {
+ try {
+ URL.setURLStreamHandlerFactory(protocol -> "https".equals(protocol) ?
+ new LoginPageInterceptingProtocolHandler(environment,
+ SubresourceIntegrityStrippingURLConnection::new) :
+ null
+ );
+ LOGGER.finest("Successfully registered custom protocol handler");
+ } catch (Exception e) {
+ LOGGER.warning(() -> {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ e.printStackTrace(new PrintWriter(outputStream));
+ return String.format("Unable to register custom protocol handler:%n%s", outputStream.toString());
+ });
+ }
+ }
+}
diff --git a/src/main/java/com/okta/tools/io/SubresourceIntegrityStrippingURLConnection.java b/src/main/java/com/okta/tools/io/SubresourceIntegrityStrippingURLConnection.java
new file mode 100644
index 0000000..a06de4f
--- /dev/null
+++ b/src/main/java/com/okta/tools/io/SubresourceIntegrityStrippingURLConnection.java
@@ -0,0 +1,83 @@
+package com.okta.tools.io;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.DataNode;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.select.Elements;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.logging.Logger;
+
+/**
+ *
+ * Inspired by a find/replace workaround:
+ * https://stackoverflow.com/questions/52572853/failed-integrity-metadata-check-in-javafx-webview-ignores-systemprop
+ *
+ *
+ * {@literal @bogeylnj} built the original version of this fix and had this comment:
+ *
+ * "javaFX.WebEngine with >1.8.0._162 cannot handle "integrity=" (attribute <link> or <script>) checks on files retrievals properly.
+ * This custom stream handler will disable the integrity checks by replacing "integrity=" and "integrity =" with a "integrity.disabled" counterpart
+ * This is very susceptible to breaking if Okta changes the response body again as we are making changes based on the format of the characters in their response"
+ *
+ *
+ *
+ * The current fix expands on the find/replace solution by using JSoup to do a robust HTML5 parse to find and disable
+ * the integrity assertions within the DOM and JavaScript content. If I was feeling particularly bold, I'd parse the
+ * JavaScript with a JavaScript parser, but I like sleep and people using broken software like timely fixes.
+ *
+ */
+final class SubresourceIntegrityStrippingURLConnection extends URLConnection {
+ private static final Logger LOGGER = Logger.getLogger(SubresourceIntegrityStrippingURLConnection.class.getName());
+ private final URLConnection httpsURLConnection;
+
+ SubresourceIntegrityStrippingURLConnection(URL url, URLConnection httpsURLConnection) {
+ super(url);
+ this.httpsURLConnection = httpsURLConnection;
+ }
+
+ @Override
+ public void connect() throws IOException {
+ httpsURLConnection.connect();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ try {
+ Document document = Jsoup.parse(
+ httpsURLConnection.getInputStream(),
+ StandardCharsets.UTF_8.name(),
+ httpsURLConnection.getURL().toURI().toASCIIString()
+ );
+ LOGGER.finest(document::toString);
+ Elements scriptsAssertingIntegrity = document.select("script:containsData(integrity)");
+ for (Element scriptAssertingIntegrity : scriptsAssertingIntegrity) {
+ String scriptWithSuppressedIntegrity = scriptAssertingIntegrity.data()
+ .replace("integrity", "integrityDisabled");
+ for (Node dataNode : scriptAssertingIntegrity.dataNodes()) {
+ dataNode.remove();
+ }
+ scriptAssertingIntegrity.appendChild(new DataNode(scriptWithSuppressedIntegrity));
+ }
+ document.select("script[integrity^=sha]").removeAttr("integrity");
+ LOGGER.finest(document::toString);
+ return new ByteArrayInputStream(document.toString().getBytes(StandardCharsets.UTF_8));
+ } catch (URISyntaxException e) {
+ throw new IOException(e);
+ }
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ return httpsURLConnection.getOutputStream();
+ }
+}