Skip to content

Commit

Permalink
🐛 The browser just appears white
Browse files Browse the repository at this point in the history
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 oktadev#272
  • Loading branch information
AlainODea committed Feb 25, 2019
1 parent f172110 commit f23d655
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<URL, URLConnection, URLConnection> filteringUrlConnectionFactory;

LoginPageInterceptingProtocolHandler(OktaAwsCliEnvironment environment, BiFunction<URL, URLConnection, URLConnection> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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());
});
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>
* Inspired by a find/replace workaround:
* https://stackoverflow.com/questions/52572853/failed-integrity-metadata-check-in-javafx-webview-ignores-systemprop
* </p>
* <p>
* {@literal @bogeylnj} built the original version of this fix and had this comment:
* <blockquote>
* "javaFX.WebEngine with >1.8.0._162 cannot handle "integrity=" (attribute &lt;link&gt; or &lt;script&gt;) 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"
* </blockquote>
* </p>
* <p>
* 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.
* </p>
*/
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();
}
}

0 comments on commit f23d655

Please sign in to comment.