Explorar el Código

use new auth flow
talking directly to Authorization Server and Resource Server instead of web frontend

Sebastian Stenzel hace 4 años
padre
commit
a3a96496b6

+ 1 - 1
pom.xml

@@ -27,7 +27,7 @@
 		<nonModularGroupIds>com.github.serceman,com.github.jnr,org.ow2.asm,net.java.dev.jna,org.apache.jackrabbit,org.apache.httpcomponents,de.swiesend,org.purejava,com.github.hypfvieh</nonModularGroupIds>
 
 		<!-- cryptomator dependencies -->
-		<cryptomator.cryptofs.version>2.1.0-beta9</cryptomator.cryptofs.version>
+		<cryptomator.cryptofs.version>2.1.0</cryptomator.cryptofs.version>
 		<cryptomator.integrations.version>1.0.0-rc1</cryptomator.integrations.version>
 		<cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
 		<cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>

+ 1 - 0
src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -14,6 +14,7 @@ public enum FxmlFile {
 	HEALTH_START("/fxml/health_start.fxml"), //
 	HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), //
 	HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
+	HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
 	HUB_P12("/fxml/hub_p12.fxml"), //
 	HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
 	LOCK_FORCED("/fxml/lock_forced.fxml"), //

+ 2 - 16
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java

@@ -127,28 +127,14 @@ class AuthFlow implements AutoCloseable {
 				.build();
 		HttpResponse<InputStream> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofInputStream());
 		if (response.statusCode() == 200) {
-			var json = parseBody(response);
+			var json = HttpHelper.parseBody(response);
 			return json.getAsJsonObject().get("access_token").getAsString();
 		} else {
-			LOG.error("Unexpected HTTP response {}: {}", response.statusCode(), readBody(response));
+			LOG.error("Unexpected HTTP response {}: {}", response.statusCode(), HttpHelper.readBody(response));
 			throw new IOException("Unexpected HTTP response code " + response.statusCode());
 		}
 	}
 
-	private String readBody(HttpResponse<InputStream> response) throws IOException {
-		try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
-			return CharStreams.toString(reader);
-		}
-	}
-
-	private JsonElement parseBody(HttpResponse<InputStream> 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);
-		}
-	}
-
 	private URI appendQueryParams(URI uri, Map<String, String> params) {
 		var oldParams = Splitter.on("&").omitEmptyStrings().splitToStream(Strings.nullToEmpty(uri.getQuery()));
 		var newParams = paramString(params);

+ 141 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java

@@ -0,0 +1,141 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import dagger.Lazy;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.ErrorComponent;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.ui.keyloading.KeyLoading;
+import org.cryptomator.ui.keyloading.KeyLoadingScoped;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.StringBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.concurrent.WorkerStateEvent;
+import javafx.fxml.FXML;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicReference;
+
+@KeyLoadingScoped
+public class AuthFlowController implements FxController {
+
+	private static final Logger LOG = LoggerFactory.getLogger(AuthFlowController.class);
+	private static final String JWT_KEY_AUTH_ENDPOINT = "authEndpoint";
+	private static final String JWT_KEY_TOKEN_ENDPOINT = "tokenEndpoint";
+	private static final String JWT_KEY_CLIENT_ID = "clientId";
+
+	private final Application application;
+	private final Stage window;
+	private final ExecutorService executor;
+	private final Vault vault;
+	private final AtomicReference<String> tokenRef;
+	private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
+	private final Lazy<Scene> receiveKeyScene;
+	private final ErrorComponent.Builder errorComponent;
+	private final ObjectProperty<URI> authUri;
+	private final StringBinding authHost;
+	private AuthFlowTask task;
+
+	@Inject
+	public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, @KeyLoading Vault vault, @Named("bearerToken") AtomicReference<String> tokenRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene, ErrorComponent.Builder errorComponent) {
+		this.application = application;
+		this.window = window;
+		this.executor = executor;
+		this.vault = vault;
+		this.tokenRef = tokenRef;
+		this.result = result;
+		this.receiveKeyScene = receiveKeyScene;
+		this.errorComponent = errorComponent;
+		this.authUri = new SimpleObjectProperty<>();
+		this.authHost = Bindings.createStringBinding(this::getAuthHost, authUri);
+		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
+	}
+
+	@FXML
+	public void initialize() {
+		assert task == null;
+		try {
+			task = setupTask();
+			task.setOnFailed(this::authFailed);
+			task.setOnSucceeded(this::authSucceeded);
+			executor.submit(task);
+		} catch (IOException e) {
+			LOG.error("Unreadable vault config", e);
+			errorComponent.cause(e).window(window).build().showErrorScene();
+		}
+	}
+
+	@FXML
+	public void browse() {
+		application.getHostServices().showDocument(authUri.get().toString());
+	}
+
+	@FXML
+	public void cancel() {
+		window.close();
+	}
+
+	private AuthFlowTask setupTask() throws IOException {
+		var authUri = URI.create(vault.getUnverifiedVaultConfig().get(JWT_KEY_AUTH_ENDPOINT).asString());
+		var tokenUri = URI.create(vault.getUnverifiedVaultConfig().get(JWT_KEY_TOKEN_ENDPOINT).asString());
+		var clientId = vault.getUnverifiedVaultConfig().get(JWT_KEY_CLIENT_ID).asString();
+		return new AuthFlowTask(authUri, tokenUri, clientId, this::setAuthUri);
+	}
+
+	private void setAuthUri(URI uri) {
+		authUri.set(uri);
+		browse();
+	}
+
+	private void windowClosed(WindowEvent windowEvent) {
+		// stop server, if it is still running
+		task.cancel();
+		// if not already interacted, mark this workflow as cancelled:
+		if (result.awaitingInteraction().get()) {
+			LOG.debug("Authorization cancelled by user.");
+			result.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED);
+		}
+	}
+
+	private void authSucceeded(WorkerStateEvent workerStateEvent) {
+		tokenRef.set(task.getValue());
+		window.requestFocus();
+		window.setScene(receiveKeyScene.get());
+	}
+
+	private void authFailed(WorkerStateEvent workerStateEvent) {
+		result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED);
+		window.requestFocus();
+		var exception = workerStateEvent.getSource().getException();
+		LOG.error("Authentication failed", exception);
+		errorComponent.cause(exception).window(window).build().showErrorScene();
+	}
+
+	/* Getter/Setter */
+
+	public StringBinding authHostProperty() {
+		return authHost;
+	}
+
+	public String getAuthHost() {
+		var uri = authUri.get();
+		if (uri == null) {
+			return "";
+		} else {
+			return uri.getAuthority().toString();
+		}
+	}
+}

+ 33 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java

@@ -0,0 +1,33 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import java.net.URI;
+import java.util.function.Consumer;
+
+class AuthFlowTask extends Task<String> {
+
+	private final URI authUri;
+	private final URI tokenUri;
+	private final String clientId;
+	private final Consumer<URI> redirectUriConsumer;
+
+	/**
+	 * Spawns a server and waits for the redirectUri to be called.
+	 *
+	 * @param redirectUriConsumer A callback invoked with the redirectUri, as soon as the server has started
+	 */
+	public AuthFlowTask(URI authUri, URI tokenUri, String clientId, Consumer<URI> redirectUriConsumer) {
+		this.authUri = authUri;
+		this.tokenUri = tokenUri;
+		this.clientId = clientId;
+		this.redirectUriConsumer = redirectUriConsumer;
+	}
+
+	@Override
+	protected String call() throws Exception {
+		try (var authFlow = AuthFlow.init(authUri, tokenUri, clientId)) {
+			return authFlow.run(uri -> Platform.runLater(() -> redirectUriConsumer.accept(uri)));
+		}
+	}
+}

+ 0 - 35
src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiveTask.java

@@ -1,35 +0,0 @@
-package org.cryptomator.ui.keyloading.hub;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javafx.application.Platform;
-import javafx.concurrent.Task;
-import java.net.URI;
-import java.util.function.Consumer;
-
-class AuthReceiveTask extends Task<EciesParams> {
-
-	private static final Logger LOG = LoggerFactory.getLogger(AuthReceiveTask.class);
-
-	private final Consumer<URI> redirectUriConsumer;
-
-	/**
-	 * Spawns a server and waits for the redirectUri to be called.
-	 *
-	 * @param redirectUriConsumer A callback invoked with the redirectUri, as soon as the server has started
-	 */
-	public AuthReceiveTask(Consumer<URI> redirectUriConsumer) {
-		this.redirectUriConsumer = redirectUriConsumer;
-	}
-
-	@Override
-	protected EciesParams call() throws Exception {
-		try (var receiver = AuthReceiver.start()) {
-			var redirectUri = receiver.getRedirectURL();
-			Platform.runLater(() -> redirectUriConsumer.accept(redirectUri));
-			LOG.debug("Waiting for key on {}", redirectUri);
-			return receiver.receive();
-		}
-	}
-}

+ 0 - 120
src/main/java/org/cryptomator/ui/keyloading/hub/AuthReceiver.java

@@ -1,120 +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.FilterHolder;
-import org.eclipse.jetty.servlet.ServletContextHandler;
-import org.eclipse.jetty.servlet.ServletHolder;
-import org.eclipse.jetty.servlets.CrossOriginFilter;
-
-import javax.servlet.DispatcherType;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.charset.StandardCharsets;
-import java.util.EnumSet;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-
-/**
- * A basic implementation for RFC 8252, Section 7.3:
- * <p>
- * We're spawning a local http server on a system-assigned high port and
- * use <code>http://127.0.0.1:{PORT}/success</code> as a redirect URI.
- * <p>
- * Furthermore, we can deliver a html response to inform the user that the
- * auth workflow finished and she can close the browser tab.
- */
-class AuthReceiver implements AutoCloseable {
-
-	private static final String REDIRECT_SCHEME = "http";
-	private static final String LOOPBACK_ADDR = "127.0.0.1";
-	private static final String JSON_200 = """
-			{"status": "success"}
-			""";
-	private static final String JSON_400 = """
-			{"status": "missing param"}
-			""";
-
-	private final Server server;
-	private final ServerConnector connector;
-	private final CallbackServlet servlet;
-
-	private AuthReceiver(Server server, ServerConnector connector, CallbackServlet servlet) {
-		assert server.isRunning();
-		this.server = server;
-		this.connector = connector;
-		this.servlet = servlet;
-	}
-
-	public URI getRedirectURL() {
-		try {
-			return new URI(REDIRECT_SCHEME, null, LOOPBACK_ADDR, connector.getLocalPort(), null, null, null);
-		} catch (URISyntaxException e) {
-			throw new IllegalStateException("URI constructed from well-formed components.", e);
-		}
-	}
-
-	public static AuthReceiver start() throws Exception {
-		var server = new Server();
-		var context = new ServletContextHandler();
-
-		var corsFilter = new FilterHolder(new CrossOriginFilter());
-		corsFilter.setInitParameter(CrossOriginFilter.ALLOWED_ORIGINS_PARAM, "*"); // TODO restrict to hub host
-		context.addFilter(corsFilter, "/*", EnumSet.of(DispatcherType.REQUEST));
-
-		var servlet = new CallbackServlet();
-		context.addServlet(new ServletHolder(servlet), "/*");
-
-		var connector = new ServerConnector(server);
-		connector.setPort(0);
-		connector.setHost(LOOPBACK_ADDR);
-		server.setConnectors(new Connector[]{connector});
-		server.setHandler(context);
-		server.start();
-		return new AuthReceiver(server, connector, servlet);
-	}
-
-	public EciesParams receive() throws InterruptedException {
-		return servlet.receivedKeys.take();
-	}
-
-	@Override
-	public void close() throws Exception {
-		server.stop();
-	}
-
-	private static class CallbackServlet extends HttpServlet {
-
-		private final BlockingQueue<EciesParams> receivedKeys = new LinkedBlockingQueue<>();
-
-		// TODO change to POST?
-		@Override
-		protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
-			var m = req.getParameter("m"); // encrypted masterkey
-			var epk = req.getParameter("epk"); // ephemeral public key
-			byte[] response;
-			if (m != null && epk != null) {
-				res.setStatus(HttpServletResponse.SC_OK);
-				response = JSON_200.getBytes(StandardCharsets.UTF_8);
-			} else {
-				res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
-				response = JSON_400.getBytes(StandardCharsets.UTF_8);
-			}
-			res.setContentType("application/json;charset=utf-8");
-			res.setContentLength(response.length);
-			res.getOutputStream().write(response);
-			res.getOutputStream().flush();
-
-			// the following line might trigger a server shutdown,
-			// so let's make sure the response is flushed first
-			if (m != null && epk != null) {
-				receivedKeys.add(new EciesParams(m, epk));
-			}
-		}
-	}
-}

+ 31 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/HttpHelper.java

@@ -0,0 +1,31 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.google.common.io.CharStreams;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+
+class HttpHelper {
+
+	public static String readBody(HttpResponse<InputStream> response) throws IOException {
+		try (var in = response.body(); var reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
+			return CharStreams.toString(reader);
+		}
+	}
+
+	public static JsonElement parseBody(HttpResponse<InputStream> 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);
+		}
+	}
+
+}

+ 22 - 7
src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java

@@ -17,6 +17,7 @@ import org.cryptomator.ui.keyloading.KeyLoading;
 import org.cryptomator.ui.keyloading.KeyLoadingScoped;
 import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
 
+import javax.inject.Named;
 import javafx.scene.Scene;
 import java.net.URI;
 import java.security.KeyPair;
@@ -26,7 +27,7 @@ import java.util.concurrent.atomic.AtomicReference;
 @Module
 public abstract class HubKeyLoadingModule {
 
-	public enum AuthFlow {
+	public enum HubLoadingResult {
 		SUCCESS,
 		FAILED,
 		CANCELLED
@@ -39,21 +40,22 @@ public abstract class HubKeyLoadingModule {
 	}
 
 	@Provides
+	@Named("bearerToken")
 	@KeyLoadingScoped
-	static AtomicReference<EciesParams> provideAuthParamsRef() {
+	static AtomicReference<String> provideBearerTokenRef() {
 		return new AtomicReference<>();
 	}
 
 	@Provides
 	@KeyLoadingScoped
-	static UserInteractionLock<AuthFlow> provideAuthFlowLock() {
-		return new UserInteractionLock<>(null);
+	static AtomicReference<EciesParams> provideEciesParamsRef() {
+		return new AtomicReference<>();
 	}
 
 	@Provides
 	@KeyLoadingScoped
-	static AtomicReference<URI> provideHubUri() {
-		return new AtomicReference<>();
+	static UserInteractionLock<HubLoadingResult> provideResultLock() {
+		return new UserInteractionLock<>(null);
 	}
 
 	@Binds
@@ -75,10 +77,18 @@ public abstract class HubKeyLoadingModule {
 		return fxmlLoaders.createScene(FxmlFile.HUB_P12);
 	}
 
+	@Provides
+	@FxmlScene(FxmlFile.HUB_AUTH_FLOW)
+	@KeyLoadingScoped
+	static Scene provideHubAuthFlowScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene(FxmlFile.HUB_AUTH_FLOW);
+	}
+
+
 	@Provides
 	@FxmlScene(FxmlFile.HUB_RECEIVE_KEY)
 	@KeyLoadingScoped
-	static Scene provideHubAuthScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+	static Scene provideHubReceiveKeyScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
 		return fxmlLoaders.createScene(FxmlFile.HUB_RECEIVE_KEY);
 	}
 
@@ -97,6 +107,11 @@ public abstract class HubKeyLoadingModule {
 	@FxControllerKey(P12CreateController.class)
 	abstract FxController bindP12CreateController(P12CreateController controller);
 
+	@Binds
+	@IntoMap
+	@FxControllerKey(AuthFlowController.class)
+	abstract FxController bindAuthFlowController(AuthFlowController controller);
+
 	@Provides
 	@IntoMap
 	@FxControllerKey(NewPasswordController.class)

+ 7 - 20
src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java

@@ -19,7 +19,6 @@ import javafx.scene.Scene;
 import javafx.stage.Stage;
 import javafx.stage.Window;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.security.KeyPair;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -33,28 +32,26 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
 	private final Vault vault;
 	private final Stage window;
 	private final Lazy<Scene> p12LoadingScene;
-	private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction;
-	private final AtomicReference<URI> hubUriRef;
+	private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction;
 	private final AtomicReference<KeyPair> keyPairRef;
-	private final AtomicReference<EciesParams> authParamsRef;
+	private final AtomicReference<EciesParams> eciesParams;
 
 	@Inject
-	public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction, AtomicReference<URI> hubUriRef, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> authParamsRef) {
+	public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> eciesParams) {
 		this.vault = vault;
 		this.window = window;
 		this.p12LoadingScene = p12LoadingScene;
 		this.userInteraction = userInteraction;
-		this.hubUriRef = hubUriRef;
 		this.keyPairRef = keyPairRef;
-		this.authParamsRef = authParamsRef;
+		this.eciesParams = eciesParams;
 	}
 
 	@Override
 	public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
-		hubUriRef.set(getHubUri(keyId));
+		Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
 		try {
 			return switch (auth()) {
-				case SUCCESS -> EciesHelper.decryptMasterkey(keyPairRef.get(), authParamsRef.get());
+				case SUCCESS -> EciesHelper.decryptMasterkey(keyPairRef.get(), eciesParams.get());
 				case FAILED -> throw new MasterkeyLoadingFailedException("failed to load keypair");
 				case CANCELLED -> throw new UnlockCancelledException("User cancelled auth workflow");
 			};
@@ -72,17 +69,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
 		}
 	}
 
-	private URI getHubUri(URI keyId) {
-		Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
-		var hubUriScheme = keyId.getScheme().substring(SCHEME_PREFIX.length());
-		try {
-			return new URI(hubUriScheme, keyId.getSchemeSpecificPart(), keyId.getFragment());
-		} catch (URISyntaxException e) {
-			throw new IllegalStateException("URI constructed from params known to be valid", e);
-		}
-	}
-
-	private HubKeyLoadingModule.AuthFlow auth() throws InterruptedException {
+	private HubKeyLoadingModule.HubLoadingResult auth() throws InterruptedException {
 		Platform.runLater(() -> {
 			window.setScene(p12LoadingScene.get());
 			window.show();

+ 3 - 3
src/main/java/org/cryptomator/ui/keyloading/hub/P12Controller.java

@@ -20,10 +20,10 @@ public class P12Controller implements FxController {
 
 	private final Stage window;
 	private final Environment env;
-	private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction;
+	private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction;
 
 	@Inject
-	public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock<HubKeyLoadingModule.AuthFlow> userInteraction) {
+	public P12Controller(@KeyLoading Stage window, Environment env, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction) {
 		this.window = window;
 		this.env = env;
 		this.userInteraction = userInteraction;
@@ -34,7 +34,7 @@ public class P12Controller implements FxController {
 		// if not already interacted, mark this workflow as cancelled:
 		if (userInteraction.awaitingInteraction().get()) {
 			LOG.debug("P12 loading cancelled by user.");
-			userInteraction.interacted(HubKeyLoadingModule.AuthFlow.CANCELLED);
+			userInteraction.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED);
 		}
 	}
 

+ 4 - 4
src/main/java/org/cryptomator/ui/keyloading/hub/P12CreateController.java

@@ -39,7 +39,7 @@ public class P12CreateController implements FxController  {
 	private final Stage window;
 	private final Environment env;
 	private final AtomicReference<KeyPair> keyPairRef;
-	private final Lazy<Scene> receiveKeyScene;
+	private final Lazy<Scene> authFlowScene;
 
 	private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
 	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled);
@@ -48,11 +48,11 @@ public class P12CreateController implements FxController  {
 	public NewPasswordController newPasswordController;
 
 	@Inject
-	public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
+	public P12CreateController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene) {
 		this.window = window;
 		this.env = env;
 		this.keyPairRef = keyPairRef;
-		this.receiveKeyScene = receiveKeyScene;
+		this.authFlowScene = authFlowScene;
 		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
 	}
 
@@ -81,7 +81,7 @@ public class P12CreateController implements FxController  {
 			var keyPair = P12AccessHelper.createNew(p12File, pw);
 			setKeyPair(keyPair);
 			LOG.debug("Created .p12 file {}", p12File);
-			window.setScene(receiveKeyScene.get());
+			window.setScene(authFlowScene.get());
 		} catch (IOException e) {
 			LOG.error("Failed to load .p12 file.", e);
 			// TODO

+ 4 - 4
src/main/java/org/cryptomator/ui/keyloading/hub/P12LoadController.java

@@ -41,18 +41,18 @@ public class P12LoadController implements FxController {
 	private final Stage window;
 	private final Environment env;
 	private final AtomicReference<KeyPair> keyPairRef;
-	private final Lazy<Scene> receiveKeyScene;
+	private final Lazy<Scene> authFlowScene;
 	private final BooleanProperty userInteractionDisabled = new SimpleBooleanProperty();
 	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, userInteractionDisabled);
 
 	public NiceSecurePasswordField passwordField;
 
 	@Inject
-	public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_RECEIVE_KEY) Lazy<Scene> receiveKeyScene) {
+	public P12LoadController(@KeyLoading Stage window, Environment env, AtomicReference<KeyPair> keyPairRef, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene) {
 		this.window = window;
 		this.env = env;
 		this.keyPairRef = keyPairRef;
-		this.receiveKeyScene = receiveKeyScene;
+		this.authFlowScene = authFlowScene;
 		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
 	}
 
@@ -78,7 +78,7 @@ public class P12LoadController implements FxController {
 			var keyPair = P12AccessHelper.loadExisting(p12File, pw);
 			setKeyPair(keyPair);
 			LOG.debug("Loaded .p12 file {}", p12File);
-			window.setScene(receiveKeyScene.get());
+			window.setScene(authFlowScene.get());
 		} catch (InvalidPassphraseException e) {
 			LOG.warn("Invalid passphrase entered for .p12 file");
 			Animations.createShakeWindowAnimation(window).playFromStart();

+ 104 - 62
src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java

@@ -2,6 +2,8 @@ package org.cryptomator.ui.keyloading.hub;
 
 import com.google.common.base.Preconditions;
 import com.google.common.io.BaseEncoding;
+import com.google.gson.JsonElement;
+import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.common.ErrorComponent;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.UserInteractionLock;
@@ -11,17 +13,25 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 import javafx.application.Application;
-import javafx.beans.binding.BooleanBinding;
+import javafx.application.Platform;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
-import javafx.concurrent.WorkerStateEvent;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
 import javafx.fxml.FXML;
+import javafx.scene.control.TextField;
 import javafx.stage.Stage;
 import javafx.stage.WindowEvent;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
 import java.net.URI;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
 import java.security.KeyPair;
 import java.util.Objects;
 import java.util.concurrent.ExecutorService;
@@ -31,53 +41,88 @@ import java.util.concurrent.atomic.AtomicReference;
 public class ReceiveKeyController implements FxController {
 
 	private static final Logger LOG = LoggerFactory.getLogger(ReceiveKeyController.class);
+	private static final String SCHEME_PREFIX = "hub+";
 
-	private final Application application;
-	private final ExecutorService executor;
 	private final Stage window;
-	private final KeyPair keyPair;
-	private final AtomicReference<EciesParams> authParamsRef;
-	private final UserInteractionLock<HubKeyLoadingModule.AuthFlow> authFlowLock;
-	private final AtomicReference<URI> hubUriRef;
+	private final String bearerToken;
+	private final AtomicReference<EciesParams> eciesParamsRef;
+	private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
 	private final ErrorComponent.Builder errorComponent;
-	private final ObjectProperty<URI> redirectUriRef;
-	private final BooleanBinding ready;
-	private final AuthReceiveTask receiveTask;
+	private final URI vaultBaseUri;
+	private final ObjectProperty<ReceiveKeyState> state = new SimpleObjectProperty<>(ReceiveKeyState.LOADING);
+	private final HttpClient httpClient;
+
+	public TextField deviceName;
 
 	@Inject
-	public ReceiveKeyController(Application application, ExecutorService executor, @KeyLoading Stage window, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> authParamsRef, UserInteractionLock<HubKeyLoadingModule.AuthFlow> authFlowLock, AtomicReference<URI> hubUriRef, ErrorComponent.Builder errorComponent) {
-		this.application = application;
-		this.executor = executor;
+	public ReceiveKeyController(@KeyLoading Vault vault, ExecutorService executor, @KeyLoading Stage window, AtomicReference<KeyPair> keyPairRef, @Named("bearerToken") AtomicReference<String> tokenRef, AtomicReference<EciesParams> eciesParamsRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result, ErrorComponent.Builder errorComponent) {
 		this.window = window;
-		this.keyPair = Objects.requireNonNull(keyPairRef.get());
-		this.authParamsRef = authParamsRef;
-		this.authFlowLock = authFlowLock;
-		this.hubUriRef = hubUriRef;
+		this.bearerToken = Objects.requireNonNull(tokenRef.get());
+		this.eciesParamsRef = eciesParamsRef;
+		this.result = result;
 		this.errorComponent = errorComponent;
-		this.redirectUriRef = new SimpleObjectProperty<>();
-		this.ready = redirectUriRef.isNotNull();
-		this.receiveTask = new AuthReceiveTask(redirectUriRef::set);
+		this.vaultBaseUri = getVaultBaseUri(vault);
 		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
+		this.httpClient = HttpClient.newBuilder().executor(executor).build();
+//		var deviceKey = BaseEncoding.base64Url().omitPadding().encode(keyPairRef.get().getPublic().getEncoded());
+//		LOG.info("deviceKey {}", deviceKey);
 	}
 
 	@FXML
 	public void initialize() {
-		Preconditions.checkState(hubUriRef.get() != null);
-		receiveTask.setOnSucceeded(this::receivedKey);
-		receiveTask.setOnFailed(this::keyRetrievalFailed);
-		executor.submit(receiveTask);
+		var keyUri = appendPath(vaultBaseUri, "/keys/desktop-app-3000"); // TODO use actual device id
+		var request = HttpRequest.newBuilder(keyUri) //
+				.header("Authorization", "Bearer " + bearerToken) //
+				.GET() //
+				.build();
+		httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
+				.whenCompleteAsync(this::loadedExistingKey, Platform::runLater);
 	}
 
-	private void keyRetrievalFailed(WorkerStateEvent workerStateEvent) {
-		LOG.error("Cryptomator Hub login failed with error", receiveTask.getException());
-		authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.FAILED);
-		errorComponent.cause(receiveTask.getException()).window(window).build().showErrorScene();
+	private void loadedExistingKey(HttpResponse<InputStream> response, Throwable error) {
+		if (error != null) {
+			retrievalFailed(error);
+		} else {
+			switch (response.statusCode()) {
+				case 200 -> retrievalSucceeded(response);
+				case 404 -> state.set(ReceiveKeyState.NEEDS_REGISTRATION);
+				default -> retrievalFailed(new IOException("Unexpected response " + response.statusCode()));
+			}
+		}
 	}
 
-	private void receivedKey(WorkerStateEvent workerStateEvent) {
-		authParamsRef.set(Objects.requireNonNull(receiveTask.getValue()));
-		authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.SUCCESS);
-		window.close();
+	@FXML
+	public void register() {
+		Preconditions.checkArgument(deviceName.textProperty().isNotEmpty().get(), "device name must not be empty");
+//		var keyUri = appendPath(vaultBaseUri, "../../devices/desktop-app-3000");
+//		var request = HttpRequest.newBuilder(keyUri) //
+//				.header("Authorization", "Bearer " + bearerToken) //
+//				.GET() //
+//				.build();
+//		httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
+//				.whenCompleteAsync(this::loadedExistingKey, Platform::runLater);
+	}
+
+	private void retrievalSucceeded(HttpResponse<InputStream> response) {
+		try {
+			var json = HttpHelper.parseBody(response);
+			Preconditions.checkArgument(json.isJsonObject());
+			Preconditions.checkArgument(json.getAsJsonObject().has("device_specific_masterkey"));
+			Preconditions.checkArgument(json.getAsJsonObject().has("ephemeral_public_key"));
+			var m = json.getAsJsonObject().get("device_specific_masterkey").getAsString();
+			var epk = json.getAsJsonObject().get("ephemeral_public_key").getAsString();
+			eciesParamsRef.set(new EciesParams(m, epk));
+			result.interacted(HubKeyLoadingModule.HubLoadingResult.SUCCESS);
+			window.close();
+		} catch (IOException | IllegalArgumentException e) {
+			retrievalFailed(e);
+		}
+	}
+
+	private void retrievalFailed(Throwable cause) {
+		result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED);
+		LOG.error("Key retrieval failed", cause);
+		errorComponent.cause(cause).window(window).build().showErrorScene();
 	}
 
 	@FXML
@@ -86,44 +131,41 @@ public class ReceiveKeyController implements FxController {
 	}
 
 	private void windowClosed(WindowEvent windowEvent) {
-		// stop server, if it is still running
-		receiveTask.cancel();
 		// if not already interacted, mark this workflow as cancelled:
-		if (authFlowLock.awaitingInteraction().get()) {
+		if (result.awaitingInteraction().get()) {
 			LOG.debug("Authorization cancelled by user.");
-			authFlowLock.interacted(HubKeyLoadingModule.AuthFlow.CANCELLED);
+			result.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED);
 		}
 	}
 
-	@FXML
-	public void openBrowser() {
-		assert ready.get();
-		var hubUri = Objects.requireNonNull(hubUriRef.get());
-		var redirectUri = Objects.requireNonNull(redirectUriRef.get());
-		var sb = new StringBuilder(hubUri.toString());
-		sb.append("?redirect_uri=").append(URLEncoder.encode(redirectUri.toString(), StandardCharsets.US_ASCII));
-		sb.append("&device_id=").append("desktop-app-3000");
-		sb.append("&device_key=").append(BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded()));
-		var url = sb.toString();
-		application.getHostServices().showDocument(url);
+	private static URI appendPath(URI base, String path) {
+		try {
+			var newPath = base.getPath() + path;
+			return new URI(base.getScheme(), base.getAuthority(), newPath, base.getQuery(), base.getFragment());
+		} catch (URISyntaxException e) {
+			throw new IllegalArgumentException("Can't append '" + path + "' to URI: " + base, e);
+		}
 	}
 
-	/* Getter/Setter */
-
-	public String getHubUriHost() {
-		var hubUri = hubUriRef.get();
-		if (hubUri == null) {
-			return null;
-		} else {
-			return hubUri.getHost();
+	private static URI getVaultBaseUri(Vault vault) {
+		try {
+			var kid = vault.getUnverifiedVaultConfig().getKeyId();
+			assert kid.getScheme().startsWith(SCHEME_PREFIX);
+			var hubUriScheme = kid.getScheme().substring(SCHEME_PREFIX.length());
+			return new URI(hubUriScheme, kid.getSchemeSpecificPart(), kid.getFragment());
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		} catch (URISyntaxException e) {
+			throw new IllegalStateException("URI constructed from params known to be valid", e);
 		}
 	}
+	/* Getter/Setter */
 
-	public BooleanBinding readyProperty() {
-		return ready;
+	public ObjectProperty<ReceiveKeyState> stateProperty() {
+		return state;
 	}
 
-	public boolean isReady() {
-		return ready.get();
+	public ReceiveKeyState getState() {
+		return state.get();
 	}
 }

+ 6 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyState.java

@@ -0,0 +1,6 @@
+package org.cryptomator.ui.keyloading.hub;
+
+public enum ReceiveKeyState {
+	LOADING,
+	NEEDS_REGISTRATION
+}

+ 42 - 0
src/main/resources/fxml/hub_auth_flow.fxml

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.image.Image?>
+<?import javafx.scene.image.ImageView?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.text.Text?>
+<?import javafx.scene.text.TextFlow?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.keyloading.hub.AuthFlowController"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<HBox spacing="12" VBox.vgrow="ALWAYS">
+			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" cache="true">
+				<Image url="@../img/bot/bot.png"/>
+			</ImageView>
+			<TextFlow visible="${!controller.authHost.empty}" managed="${!controller.authHost.empty}">
+				<Text text="TODO: please login via " />
+				<Hyperlink styleClass="hyperlink-underline" text="${controller.authHost}" onAction="#browse"/>
+			</TextFlow>
+		</HBox>
+
+		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+C">
+				<buttons>
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</VBox>

+ 16 - 9
src/main/resources/fxml/hub_receive_key.fxml

@@ -1,16 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
+<?import org.cryptomator.ui.keyloading.hub.ReceiveKeyState?>
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.control.ButtonBar?>
-<?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.TextField?>
 <?import javafx.scene.image.Image?>
 <?import javafx.scene.image.ImageView?>
 <?import javafx.scene.layout.HBox?>
 <?import javafx.scene.layout.VBox?>
-<?import javafx.scene.text.Text?>
-<?import javafx.scene.text.TextFlow?>
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.keyloading.hub.ReceiveKeyController"
@@ -18,6 +18,10 @@
 	  maxWidth="400"
 	  minHeight="145"
 	  spacing="12">
+	<fx:define>
+		<ReceiveKeyState fx:id="loading" fx:constant="LOADING" />
+		<ReceiveKeyState fx:id="needsRegistration" fx:constant="NEEDS_REGISTRATION"/>
+	</fx:define>
 	<padding>
 		<Insets topRightBottomLeft="12"/>
 	</padding>
@@ -26,17 +30,20 @@
 			<ImageView VBox.vgrow="ALWAYS" fitWidth="64" preserveRatio="true" cache="true">
 				<Image url="@../img/bot/bot.png"/>
 			</ImageView>
-			<TextFlow visible="${controller.ready}" managed="${controller.ready}">
-				<Text text="TODO: please login via " />
-				<Hyperlink styleClass="hyperlink-underline" text="${controller.hubUriHost}" onAction="#openBrowser"/>
-			</TextFlow>
-			<FontAwesome5Spinner glyphSize="12" visible="${!controller.ready}" managed="${!controller.ready}"/>
+
+			<FontAwesome5Spinner glyphSize="12" visible="${controller.state == loading}" managed="${controller.state == loading}"/>
+
+			<VBox spacing="6" visible="${controller.state == needsRegistration}" managed="${controller.state == needsRegistration}">
+				<Label text="TODO: register device" labelFor="$locationTextField"/>
+				<TextField fx:id="deviceName" promptText="TODO: device name" VBox.vgrow="ALWAYS"/>
+			</VBox>
 		</HBox>
 
 		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
-			<ButtonBar buttonMinWidth="120" buttonOrder="+C">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
 				<buttons>
 					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
+					<Button text="TODO: register device" ButtonBar.buttonData="NEXT_FORWARD" visible="${controller.state == needsRegistration}" managed="${controller.state == needsRegistration}" defaultButton="true" onAction="#register"/>
 				</buttons>
 			</ButtonBar>
 		</VBox>

+ 7 - 1
src/main/resources/license/THIRD-PARTY.txt

@@ -11,7 +11,7 @@ GNU General Public License for more details.
 You should have received a copy of the GNU General Public License
 along with this program.  If not, see http://www.gnu.org/licenses/.
 
-Cryptomator uses 43 third-party dependencies under the following licenses:
+Cryptomator uses 46 third-party dependencies under the following licenses:
         Apache License v2.0:
 			- jffi (com.github.jnr:jffi:1.2.23 - http://github.com/jnr/jffi)
 			- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
@@ -33,7 +33,10 @@ Cryptomator uses 43 third-party dependencies under the following licenses:
 			- Jetty :: Security (org.eclipse.jetty:jetty-security:10.0.6 - https://eclipse.org/jetty/jetty-security)
 			- Jetty :: Server Core (org.eclipse.jetty:jetty-server:10.0.6 - https://eclipse.org/jetty/jetty-server)
 			- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:10.0.6 - https://eclipse.org/jetty/jetty-servlet)
+			- Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:10.0.6 - https://eclipse.org/jetty/jetty-servlets)
 			- Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - https://eclipse.org/jetty/jetty-util)
+			- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:10.0.6 - https://eclipse.org/jetty/jetty-webapp)
+			- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:10.0.6 - https://eclipse.org/jetty/jetty-xml)
 			- Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - https://eclipse.org/jetty/jetty-servlet-api)
         BSD:
 			- asm (org.ow2.asm:asm:7.1 - http://asm.ow2.org/)
@@ -53,7 +56,10 @@ Cryptomator uses 43 third-party dependencies under the following licenses:
 			- Jetty :: Security (org.eclipse.jetty:jetty-security:10.0.6 - https://eclipse.org/jetty/jetty-security)
 			- Jetty :: Server Core (org.eclipse.jetty:jetty-server:10.0.6 - https://eclipse.org/jetty/jetty-server)
 			- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:10.0.6 - https://eclipse.org/jetty/jetty-servlet)
+			- Jetty :: Utility Servlets and Filters (org.eclipse.jetty:jetty-servlets:10.0.6 - https://eclipse.org/jetty/jetty-servlets)
 			- Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - https://eclipse.org/jetty/jetty-util)
+			- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:10.0.6 - https://eclipse.org/jetty/jetty-webapp)
+			- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:10.0.6 - https://eclipse.org/jetty/jetty-xml)
         Eclipse Public License - v 1.0:
 			- Logback Classic Module (ch.qos.logback:logback-classic:1.2.3 - http://logback.qos.ch/logback-classic)
 			- Logback Core Module (ch.qos.logback:logback-core:1.2.3 - http://logback.qos.ch/logback-core)

+ 0 - 23
src/test/java/org/cryptomator/ui/keyloading/hub/AuthReceiverTest.java

@@ -1,23 +0,0 @@
-package org.cryptomator.ui.keyloading.hub;
-
-public class AuthReceiverTest {
-
-	static {
-		System.setProperty("LOGLEVEL", "INFO");
-	}
-
-	public static void main(String[] args) {
-		try (var receiver = AuthReceiver.start()) {
-			System.out.println("Waiting on " + receiver.getRedirectURL());
-			var token = receiver.receive();
-			System.out.println("SUCCESS: " + token);
-		} catch (InterruptedException e) {
-			Thread.currentThread().interrupt();
-			System.out.println("CANCELLED");
-		} catch (Exception e) {
-			System.out.println("ERROR");
-			e.printStackTrace();
-		}
-	}
-
-}