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(); + } +}