com.auth0
java-jwt
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index ae2d4c320..7138bcf09 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -28,11 +28,9 @@ module org.cryptomator.desktop {
requires com.nulabinc.zxcvbn;
requires com.tobiasdiez.easybind;
requires dagger;
+ requires io.github.coffeelibs.tinyoauth2client;
requires org.slf4j;
requires org.apache.commons.lang3;
- requires org.eclipse.jetty.server;
- requires org.eclipse.jetty.webapp;
- requires org.eclipse.jetty.servlets;
/* TODO: filename-based modules: */
requires static javax.inject; /* ugly dagger/guava crap */
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java
deleted file mode 100644
index 80bad9c6e..000000000
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java
+++ /dev/null
@@ -1,186 +0,0 @@
-package org.cryptomator.ui.keyloading.hub;
-
-import com.google.common.base.Splitter;
-import com.google.common.base.Strings;
-import com.google.common.collect.Streams;
-import com.google.common.escape.Escaper;
-import com.google.common.io.BaseEncoding;
-import com.google.common.net.PercentEscaper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.charset.StandardCharsets;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Map;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * Simple OAuth 2.0 Authentication Code Flow with {@link PKCE}.
- *
- * @see RFC 8252
- * @see RFC 6749
- * @see RFC 7636
- */
-class AuthFlow implements AutoCloseable {
-
- private static final Logger LOG = LoggerFactory.getLogger(AuthFlow.class);
- private static final SecureRandom CSPRNG = new SecureRandom();
- private static final BaseEncoding BASE64URL = BaseEncoding.base64Url().omitPadding();
- public static final Escaper QUERY_STRING_ESCAPER = new PercentEscaper("-_.!~*'()@:$,;/?", false);
-
- private final AuthFlowReceiver receiver;
- private final URI authEndpoint; // see https://datatracker.ietf.org/doc/html/rfc6749#section-3.1
- private final URI tokenEndpoint; // see https://datatracker.ietf.org/doc/html/rfc6749#section-3.2
- private final String clientId; // see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
-
- private AuthFlow(AuthFlowReceiver receiver, HubConfig hubConfig) {
- this.receiver = receiver;
- this.authEndpoint = URI.create(hubConfig.authEndpoint);
- this.tokenEndpoint = URI.create(hubConfig.tokenEndpoint);
- this.clientId = hubConfig.clientId;
- }
-
- /**
- * Prepares an Authorization Code Flow with PKCE.
- *
- * This will start a loopback server, so make sure to {@link #close()} this resource.
- *
- * @param hubConfig A hub config object containing parameters required for this auth flow
- * @return An authorization flow
- * @throws Exception In case of any problems starting the server
- */
- public static AuthFlow init(HubConfig hubConfig, AuthFlowContext authFlowContext) throws Exception {
- var receiver = AuthFlowReceiver.start(hubConfig, authFlowContext);
- return new AuthFlow(receiver, hubConfig);
- }
-
- /**
- * Runs this Authorization Code Flow. This will take a long time and should be done in a background thread.
- *
- * @param browser A callback that will open the auth URI in a browser
- * @return The access token
- * @throws IOException In case of any errors, including failed authentication.
- * @throws InterruptedException If this method is interrupted while waiting for responses from the authorization server
- */
- public String run(Consumer browser) throws IOException, InterruptedException {
- var pkce = new PKCE();
- var authCode = auth(pkce, browser);
- return token(pkce, authCode);
- }
-
- private String auth(PKCE pkce, Consumer browser) throws IOException, InterruptedException {
- var state = BASE64URL.encode(randomBytes(16));
- var params = Map.of("response_type", "code", //
- "client_id", clientId, //
- "redirect_uri", receiver.getRedirectUri(), //
- "state", state, //
- "code_challenge", pkce.challenge, //
- "code_challenge_method", PKCE.METHOD //
- );
- var uri = appendQueryParams(this.authEndpoint, params);
-
- // open browser and wait for response
- LOG.debug("waiting for user to log into {}", uri);
- browser.accept(uri);
- var callback = receiver.receive();
-
- if (!state.equals(callback.state())) {
- throw new IOException("Invalid CSRF Token");
- } else if (callback.error() != null) {
- throw new IOException("Authentication failed " + callback.error());
- } else if (callback.code() == null) {
- throw new IOException("Received neither authentication code nor error");
- }
- return callback.code();
- }
-
- private String token(PKCE pkce, String authCode) throws IOException, InterruptedException {
- var params = Map.of("grant_type", "authorization_code", //
- "client_id", clientId, //
- "redirect_uri", receiver.getRedirectUri(), //
- "code", authCode, //
- "code_verifier", pkce.verifier //
- );
- var paramStr = paramString(params).collect(Collectors.joining("&"));
- var request = HttpRequest.newBuilder(this.tokenEndpoint) //
- .header("Content-Type", "application/x-www-form-urlencoded") //
- .POST(HttpRequest.BodyPublishers.ofString(paramStr)) //
- .build();
- HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream());
- if (response.statusCode() == 200) {
- var json = HttpHelper.parseBody(response);
- return json.getAsJsonObject().get("access_token").getAsString();
- } else {
- LOG.error("Unexpected HTTP response {}: {}", response.statusCode(), HttpHelper.readBody(response));
- throw new IOException("Unexpected HTTP response code " + response.statusCode());
- }
- }
-
- private URI appendQueryParams(URI uri, Map params) {
- var oldParams = Splitter.on("&").omitEmptyStrings().splitToStream(Strings.nullToEmpty(uri.getQuery()));
- var newParams = paramString(params);
- var query = Streams.concat(oldParams, newParams).collect(Collectors.joining("&"));
- try {
- return new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), query, uri.getFragment());
- } catch (URISyntaxException e) {
- throw new IllegalArgumentException("Unable to create URI from given", e);
- }
- }
-
- private Stream paramString(Map params) {
- return params.entrySet().stream().map(param -> {
- var key = QUERY_STRING_ESCAPER.escape(param.getKey());
- var value = QUERY_STRING_ESCAPER.escape(param.getValue());
- return key + "=" + value;
- });
- }
-
- @Override
- public void close() throws Exception {
- receiver.close();
- }
-
- /**
- * @see RFC 7636
- */
- private static record PKCE(String challenge, String verifier) {
-
- public static final String METHOD = "S256";
-
- public PKCE(String verifier) {
- this(BASE64URL.encode(sha256(verifier.getBytes(StandardCharsets.US_ASCII))), verifier);
- }
-
- public PKCE() {
- this(BASE64URL.encode(randomBytes(32)));
- }
-
- }
-
- private static byte[] randomBytes(int len) {
- byte[] bytes = new byte[len];
- CSPRNG.nextBytes(bytes);
- return bytes;
- }
-
- private static byte[] sha256(byte[] input) {
- try {
- var digest = MessageDigest.getInstance("SHA-256");
- return digest.digest(input);
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException("Every implementation of the Java platform is required to support SHA-256.");
- }
- }
-
-}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java
index 339030cf2..23ecfff91 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java
@@ -11,6 +11,7 @@ import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Application;
+import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ObjectProperty;
@@ -75,8 +76,10 @@ public class AuthFlowController implements FxController {
}
private void setAuthUri(URI uri) {
- authUri.set(uri);
- browse();
+ Platform.runLater(() -> {
+ authUri.set(uri);
+ browse();
+ });
}
private void windowClosed(WindowEvent windowEvent) {
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java
deleted file mode 100644
index dc84c450c..000000000
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package org.cryptomator.ui.keyloading.hub;
-
-import org.eclipse.jetty.server.Connector;
-import org.eclipse.jetty.server.Server;
-import org.eclipse.jetty.server.ServerConnector;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-
-/**
- * A basic implementation for RFC 8252, Section 7.3:
- *
- * We're spawning a local http server on a system-assigned high port and
- * use http://127.0.0.1:{PORT}/callback as a redirect URI.
- *
- * Furthermore, we can deliver a html response to inform the user that the
- * auth workflow finished and she can close the browser tab.
- */
-class AuthFlowReceiver implements AutoCloseable {
-
- private static final String LOOPBACK_ADDR = "127.0.0.1";
- private static final String CALLBACK_PATH = "/callback";
-
- private final Server server;
- private final ServerConnector connector;
- private final CallbackServlet servlet;
-
- private AuthFlowReceiver(Server server, ServerConnector connector, CallbackServlet servlet) {
- this.server = server;
- this.connector = connector;
- this.servlet = servlet;
- }
-
- public static AuthFlowReceiver start(HubConfig hubConfig, AuthFlowContext authFlowContext) throws Exception {
- var server = new Server();
- var context = new ServletContextHandler();
-
- var servlet = new CallbackServlet(hubConfig, authFlowContext);
- context.addServlet(new ServletHolder(servlet), CALLBACK_PATH);
-
- var connector = new ServerConnector(server);
- connector.setPort(0);
- connector.setHost(LOOPBACK_ADDR);
- server.setConnectors(new Connector[]{connector});
- server.setHandler(context);
- server.start();
- return new AuthFlowReceiver(server, connector, servlet);
- }
-
- public String getRedirectUri() {
- return "http://" + LOOPBACK_ADDR + ":" + connector.getLocalPort() + CALLBACK_PATH;
- }
-
- public Callback receive() throws InterruptedException {
- return servlet.callback.take();
- }
-
- @Override
- public void close() throws Exception {
- server.stop();
- }
-
- public static record Callback(String error, String code, String state) {
-
- }
-
- private static class CallbackServlet extends HttpServlet {
-
- private final BlockingQueue callback = new LinkedBlockingQueue<>();
- private final HubConfig hubConfig;
- private final AuthFlowContext authFlowContext;
-
- public CallbackServlet(HubConfig hubConfig, AuthFlowContext authFlowContext) {
- this.hubConfig = hubConfig;
- this.authFlowContext = authFlowContext;
- }
-
- @Override
- protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
- var error = req.getParameter("error");
- var code = req.getParameter("code");
- var state = req.getParameter("state");
-
- res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
- if (error == null && code != null) {
- res.setHeader("Location", hubConfig.authSuccessUrl + "&device=" + authFlowContext.deviceId());
- } else {
- res.setHeader("Location", hubConfig.authErrorUrl + "&device=" + authFlowContext.deviceId());
- }
-
- callback.add(new Callback(error, code, state));
- }
- }
-
-}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java
index be41df2dd..b9b4fdf0b 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java
@@ -1,12 +1,16 @@
package org.cryptomator.ui.keyloading.hub;
-import javafx.application.Platform;
+import com.google.gson.JsonParser;
+import io.github.coffeelibs.tinyoauth2client.AuthFlow;
+
import javafx.concurrent.Task;
+import java.io.IOException;
import java.net.URI;
import java.util.function.Consumer;
class AuthFlowTask extends Task {
+ private final HubConfig hubConfig;
private final AuthFlowContext authFlowContext;
private final Consumer redirectUriConsumer;
@@ -23,11 +27,13 @@ class AuthFlowTask extends Task {
}
@Override
- protected String call() throws Exception {
- try (var authFlow = AuthFlow.init(hubConfig, authFlowContext)) {
- return authFlow.run(uri -> Platform.runLater(() -> redirectUriConsumer.accept(uri)));
- }
+ protected String call() throws IOException, InterruptedException {
+ // TODO configure redirectURIs with deviceId from authFlowContext
+ var response = AuthFlow.asClient(hubConfig.clientId) //
+ .authorize(URI.create(hubConfig.authEndpoint), redirectUriConsumer) //
+ .getAccessToken(URI.create(hubConfig.tokenEndpoint));
+ var json = JsonParser.parseString(response);
+ return json.getAsJsonObject().get("access_token").getAsString();
}
- private final HubConfig hubConfig;
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java
index 7de2902be..51f3662a7 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java
@@ -20,12 +20,4 @@ class HttpHelper {
}
}
- public static JsonElement parseBody(HttpResponse response) throws IOException {
- try (InputStream in = response.body(); Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
- return JsonParser.parseReader(reader);
- } catch (JsonParseException e) {
- throw new IOException("Failed to parse JSON", e);
- }
- }
-
}
diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
index be4c3b791..2bdb772a6 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java
@@ -8,7 +8,6 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
-import org.eclipse.jetty.io.RuntimeIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -82,7 +81,7 @@ public class ReceiveKeyController implements FxController {
default -> throw new IOException("Unexpected response " + response.statusCode());
}
} catch (IOException e) {
- throw new RuntimeIOException(e);
+ throw new UncheckedIOException(e);
}
}
diff --git a/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java b/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java
deleted file mode 100644
index f2b9e9cd5..000000000
--- a/src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.cryptomator.ui.keyloading.hub;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class AuthFlowIntegrationTest {
-
- static {
- System.setProperty("LOGLEVEL", "INFO");
- }
-
- private static final Logger LOG = LoggerFactory.getLogger(AuthFlowIntegrationTest.class);
-
- @Test
- @Disabled // only to be run manually
- public void testRetrieveToken() throws Exception {
- var hubConfig = new HubConfig();
- hubConfig.authEndpoint = "http://localhost:8080/auth/realms/cryptomator/protocol/openid-connect/auth";
- hubConfig.tokenEndpoint = "http://localhost:8080/auth/realms/cryptomator/protocol/openid-connect/token";
- hubConfig.clientId = "cryptomator-hub";
- hubConfig.authSuccessUrl = "http://localhost:3000/#/unlock-success?vault=vaultId";
- hubConfig.authErrorUrl = "http://localhost:3000/#/unlock-error?vault=vaultId";
-
- try (var authFlow = AuthFlow.init(hubConfig, new AuthFlowContext("deviceId"))) {
- var token = authFlow.run(uri -> {
- LOG.info("Visit {} to authenticate", uri);
- });
- LOG.info("Received token {}", token);
- Assertions.assertNotNull(token);
- }
- }
-
-}
\ No newline at end of file