Skip to content

Commit 38d4407

Browse files
authored
🐛 The browser just appears white (#274)
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
1 parent f172110 commit 38d4407

File tree

4 files changed

+158
-1
lines changed

4 files changed

+158
-1
lines changed

src/main/java/com/okta/tools/authentication/BrowserAuthentication.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.okta.tools.OktaAwsCliEnvironment;
44
import com.okta.tools.helpers.CookieHelper;
5+
import com.okta.tools.io.SubresourceIntegrityStrippingHack;
56
import com.okta.tools.util.NodeListIterable;
67
import com.sun.javafx.webkit.WebConsoleListener;
78
import javafx.application.Application;
@@ -61,6 +62,7 @@ public void start(final Stage stage) throws IOException {
6162
URI uri = URI.create(ENVIRONMENT.oktaAwsAppUrl);
6263
initializeCookies(uri);
6364

65+
SubresourceIntegrityStrippingHack.overrideHttpsProtocolHandler(ENVIRONMENT);
6466
webEngine.getLoadWorker().stateProperty()
6567
.addListener((ov, oldState, newState) -> {
6668
if (webEngine.getDocument() != null) {
@@ -75,7 +77,7 @@ public void start(final Stage stage) throws IOException {
7577
});
7678

7779
WebConsoleListener.setDefaultListener((webView, message, lineNumber, sourceId) -> {
78-
System.out.println("WebConsoleListener: " + message + "[at " + lineNumber + "]");
80+
System.out.println("WebConsoleListener: " + message + "[" + webEngine.getLocation() + ":" + lineNumber + "]");
7981
});
8082

8183
webEngine.load(uri.toASCIIString());
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.okta.tools.io;
2+
3+
import com.okta.tools.OktaAwsCliEnvironment;
4+
5+
import java.io.IOException;
6+
import java.net.Proxy;
7+
import java.net.URI;
8+
import java.net.URL;
9+
import java.net.URLConnection;
10+
import java.util.Arrays;
11+
import java.util.function.BiFunction;
12+
import java.util.logging.Logger;
13+
14+
final class LoginPageInterceptingProtocolHandler extends sun.net.www.protocol.https.Handler {
15+
private static final Logger LOGGER = Logger.getLogger(LoginPageInterceptingProtocolHandler.class.getName());
16+
private final OktaAwsCliEnvironment environment;
17+
private final BiFunction<URL, URLConnection, URLConnection> filteringUrlConnectionFactory;
18+
19+
LoginPageInterceptingProtocolHandler(OktaAwsCliEnvironment environment, BiFunction<URL, URLConnection, URLConnection> filteringUrlConnectionFactory) {
20+
this.environment = environment;
21+
this.filteringUrlConnectionFactory = filteringUrlConnectionFactory;
22+
}
23+
24+
@Override
25+
protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
26+
URLConnection urlConnection = super.openConnection(url, proxy);
27+
if (environment.oktaOrg.equals(url.getHost()) &&
28+
Arrays.asList(
29+
URI.create(environment.oktaAwsAppUrl).getPath(),
30+
"/login/login.htm",
31+
"/auth/services/devicefingerprint"
32+
).contains(url.getPath())
33+
) {
34+
LOGGER.finest(() -> String.format("[%s] Using filtering URLConnection", url));
35+
return filteringUrlConnectionFactory.apply(url, urlConnection);
36+
} else {
37+
LOGGER.finest(() -> String.format("[%s] Using unmodified URLConnection", url));
38+
return urlConnection;
39+
}
40+
}
41+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.okta.tools.io;
2+
3+
import com.okta.tools.OktaAwsCliEnvironment;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.PrintWriter;
7+
import java.net.URL;
8+
import java.util.logging.Logger;
9+
10+
public class SubresourceIntegrityStrippingHack {
11+
private static final Logger LOGGER = Logger.getLogger(SubresourceIntegrityStrippingHack.class.getName());
12+
13+
private SubresourceIntegrityStrippingHack() {}
14+
15+
public static void overrideHttpsProtocolHandler(OktaAwsCliEnvironment environment) {
16+
try {
17+
URL.setURLStreamHandlerFactory(protocol -> "https".equals(protocol) ?
18+
new LoginPageInterceptingProtocolHandler(environment,
19+
SubresourceIntegrityStrippingURLConnection::new) :
20+
null
21+
);
22+
LOGGER.finest("Successfully registered custom protocol handler");
23+
} catch (Exception e) {
24+
LOGGER.warning(() -> {
25+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
26+
e.printStackTrace(new PrintWriter(outputStream));
27+
return String.format("Unable to register custom protocol handler:%n%s", outputStream.toString());
28+
});
29+
}
30+
}
31+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.okta.tools.io;
2+
3+
import org.jsoup.Jsoup;
4+
import org.jsoup.nodes.DataNode;
5+
import org.jsoup.nodes.Document;
6+
import org.jsoup.nodes.Element;
7+
import org.jsoup.nodes.Node;
8+
import org.jsoup.select.Elements;
9+
10+
import java.io.ByteArrayInputStream;
11+
import java.io.IOException;
12+
import java.io.InputStream;
13+
import java.io.OutputStream;
14+
import java.net.URISyntaxException;
15+
import java.net.URL;
16+
import java.net.URLConnection;
17+
import java.nio.charset.StandardCharsets;
18+
import java.util.logging.Logger;
19+
20+
/**
21+
* <p>
22+
* Inspired by a find/replace workaround:
23+
* https://stackoverflow.com/questions/52572853/failed-integrity-metadata-check-in-javafx-webview-ignores-systemprop
24+
* </p>
25+
* <p>
26+
* {@literal @bogeylnj} built the original version of this fix and had this comment:
27+
* <blockquote>
28+
* "javaFX.WebEngine with >1.8.0._162 cannot handle "integrity=" (attribute &lt;link&gt; or &lt;script&gt;) checks on files retrievals properly.
29+
* This custom stream handler will disable the integrity checks by replacing "integrity=" and "integrity =" with a "integrity.disabled" counterpart
30+
* 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"
31+
* </blockquote>
32+
* </p>
33+
* <p>
34+
* The current fix expands on the find/replace solution by using JSoup to do a robust HTML5 parse to find and disable
35+
* the integrity assertions within the DOM and JavaScript content. If I was feeling particularly bold, I'd parse the
36+
* JavaScript with a JavaScript parser, but I like sleep and people using broken software like timely fixes.
37+
* </p>
38+
*/
39+
final class SubresourceIntegrityStrippingURLConnection extends URLConnection {
40+
private static final Logger LOGGER = Logger.getLogger(SubresourceIntegrityStrippingURLConnection.class.getName());
41+
private final URLConnection httpsURLConnection;
42+
43+
SubresourceIntegrityStrippingURLConnection(URL url, URLConnection httpsURLConnection) {
44+
super(url);
45+
this.httpsURLConnection = httpsURLConnection;
46+
}
47+
48+
@Override
49+
public void connect() throws IOException {
50+
httpsURLConnection.connect();
51+
}
52+
53+
@Override
54+
public InputStream getInputStream() throws IOException {
55+
try {
56+
Document document = Jsoup.parse(
57+
httpsURLConnection.getInputStream(),
58+
StandardCharsets.UTF_8.name(),
59+
httpsURLConnection.getURL().toURI().toASCIIString()
60+
);
61+
LOGGER.finest(document::toString);
62+
Elements scriptsAssertingIntegrity = document.select("script:containsData(integrity)");
63+
for (Element scriptAssertingIntegrity : scriptsAssertingIntegrity) {
64+
String scriptWithSuppressedIntegrity = scriptAssertingIntegrity.data()
65+
.replace("integrity", "integrityDisabled");
66+
for (Node dataNode : scriptAssertingIntegrity.dataNodes()) {
67+
dataNode.remove();
68+
}
69+
scriptAssertingIntegrity.appendChild(new DataNode(scriptWithSuppressedIntegrity));
70+
}
71+
document.select("script[integrity^=sha]").removeAttr("integrity");
72+
LOGGER.finest(document::toString);
73+
return new ByteArrayInputStream(document.toString().getBytes(StandardCharsets.UTF_8));
74+
} catch (URISyntaxException e) {
75+
throw new IOException(e);
76+
}
77+
}
78+
79+
@Override
80+
public OutputStream getOutputStream() throws IOException {
81+
return httpsURLConnection.getOutputStream();
82+
}
83+
}

0 commit comments

Comments
 (0)