Ver Fonte

adjusted to new vault config format and unlock status codes

Sebastian Stenzel há 4 anos atrás
pai
commit
1322b872b6

+ 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-beta10</cryptomator.cryptofs.version>
+		<cryptomator.cryptofs.version>2.1.0-beta11</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>

+ 2 - 0
src/main/java/module-info.java

@@ -42,6 +42,8 @@ module org.cryptomator.desktop {
 	uses TrayIntegrationProvider;
 	uses UiAppearanceProvider;
 
+	exports org.cryptomator.ui.keyloading.hub to com.fasterxml.jackson.databind;
+
 	opens org.cryptomator.common.settings to com.google.gson;
 
 	opens org.cryptomator.common to javafx.fxml;

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

@@ -17,6 +17,7 @@ public enum FxmlFile {
 	HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), //
 	HUB_P12("/fxml/hub_p12.fxml"), //
 	HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), //
+	HUB_REGISTER_DEVICE("/fxml/hub_register_device.fxml"), //
 	LOCK_FORCED("/fxml/lock_forced.fxml"), //
 	LOCK_FAILED("/fxml/lock_failed.fxml"), //
 	MAIN_WINDOW("/fxml/main_window.fxml"), //

+ 11 - 13
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java

@@ -46,15 +46,15 @@ class AuthFlow implements AutoCloseable {
 	public static final Escaper QUERY_STRING_ESCAPER = new PercentEscaper("-_.!~*'()@:$,;/?", false);
 
 	private final AuthFlowReceiver receiver;
-	private final URI authEndpoint;
-	private final URI tokenEndpoint;
-	private final String clientId;
+	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, URI authEndpoint, URI tokenEndpoint, String clientId) {
+	private AuthFlow(AuthFlowReceiver receiver, HubConfig hubConfig) {
 		this.receiver = receiver;
-		this.authEndpoint = authEndpoint;
-		this.tokenEndpoint = tokenEndpoint;
-		this.clientId = clientId;
+		this.authEndpoint = URI.create(hubConfig.authEndpoint);
+		this.tokenEndpoint = URI.create(hubConfig.tokenEndpoint);
+		this.clientId = hubConfig.clientId;
 	}
 
 	/**
@@ -62,15 +62,13 @@ class AuthFlow implements AutoCloseable {
 	 * <p>
 	 * This will start a loopback server, so make sure to {@link #close()} this resource.
 	 *
-	 * @param authEndpoint Address of the <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.1">Authorization Endpoint</a>
-	 * @param tokenEndpoint Address of the <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-3.2">Token Endpoint</a>
-	 * @param clientId The <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1"><code>client_id</code></a>
+	 * @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(URI authEndpoint, URI tokenEndpoint, String clientId) throws Exception {
-		var receiver = AuthFlowReceiver.start();
-		return new AuthFlow(receiver, authEndpoint, tokenEndpoint, clientId);
+	public static AuthFlow init(HubConfig hubConfig) throws Exception {
+		var receiver = AuthFlowReceiver.start(hubConfig);
+		return new AuthFlow(receiver, hubConfig);
 	}
 
 	/**

+ 8 - 24
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java

@@ -1,7 +1,6 @@
 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;
@@ -24,7 +23,6 @@ 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;
@@ -33,14 +31,11 @@ import java.util.concurrent.atomic.AtomicReference;
 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 HubConfig hubConfig;
 	private final AtomicReference<String> tokenRef;
 	private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
 	private final Lazy<Scene> receiveKeyScene;
@@ -50,11 +45,11 @@ public class AuthFlowController implements FxController {
 	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) {
+	public AuthFlowController(Application application, @KeyLoading Stage window, ExecutorService executor, HubConfig hubConfig, @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.hubConfig = hubConfig;
 		this.tokenRef = tokenRef;
 		this.result = result;
 		this.receiveKeyScene = receiveKeyScene;
@@ -67,15 +62,10 @@ public class AuthFlowController implements FxController {
 	@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();
-		}
+		task = new AuthFlowTask(hubConfig, this::setAuthUri);;
+		task.setOnFailed(this::authFailed);
+		task.setOnSucceeded(this::authSucceeded);
+		executor.submit(task);
 	}
 
 	@FXML
@@ -88,13 +78,6 @@ public class AuthFlowController implements FxController {
 		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();
@@ -138,4 +121,5 @@ public class AuthFlowController implements FxController {
 			return uri.getAuthority().toString();
 		}
 	}
+
 }

+ 17 - 9
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java

@@ -40,18 +40,20 @@ class AuthFlowReceiver implements AutoCloseable {
 	private final Server server;
 	private final ServerConnector connector;
 	private final CallbackServlet servlet;
+	private final HubConfig hubConfig;
 
-	private AuthFlowReceiver(Server server, ServerConnector connector, CallbackServlet servlet) {
+	private AuthFlowReceiver(Server server, ServerConnector connector, CallbackServlet servlet, HubConfig hubConfig) {
 		this.server = server;
 		this.connector = connector;
 		this.servlet = servlet;
+		this.hubConfig = hubConfig;
 	}
 
-	public static AuthFlowReceiver start() throws Exception {
+	public static AuthFlowReceiver start(HubConfig hubConfig) throws Exception {
 		var server = new Server();
 		var context = new ServletContextHandler();
 
-		var servlet = new CallbackServlet();
+		var servlet = new CallbackServlet(hubConfig);
 		context.addServlet(new ServletHolder(servlet), CALLBACK_PATH);
 
 		var connector = new ServerConnector(server);
@@ -60,7 +62,7 @@ class AuthFlowReceiver implements AutoCloseable {
 		server.setConnectors(new Connector[]{connector});
 		server.setHandler(context);
 		server.start();
-		return new AuthFlowReceiver(server, connector, servlet);
+		return new AuthFlowReceiver(server, connector, servlet, hubConfig);
 	}
 
 	public String getRedirectUri() {
@@ -81,6 +83,11 @@ class AuthFlowReceiver implements AutoCloseable {
 	private static class CallbackServlet extends HttpServlet {
 
 		private final BlockingQueue<Callback> callback = new LinkedBlockingQueue<>();
+		private final HubConfig hubConfig;
+
+		public CallbackServlet(HubConfig hubConfig) {
+			this.hubConfig = hubConfig;
+		}
 
 		@Override
 		protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
@@ -88,14 +95,15 @@ class AuthFlowReceiver implements AutoCloseable {
 			var code = req.getParameter("code");
 			var state = req.getParameter("state");
 
-			// TODO 302 use redirect to configurable site
-			res.setContentType("text/html;charset=utf-8");
-			res.getWriter().write(HTML_SUCCESS);
-			res.getWriter().flush();
+			res.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
+			if (error == null && code != null) {
+				res.setHeader("Location", hubConfig.unlockSuccessUrl);
+			} else {
+				res.setHeader("Location", hubConfig.unlockErrorUrl);
+			}
 
 			callback.add(new Callback(error, code, state));
 		}
-
 	}
 
 }

+ 6 - 8
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowTask.java

@@ -7,27 +7,25 @@ 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 hubConfig Configuration object holding parameters required by {@link AuthFlow}
 	 * @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;
+	public AuthFlowTask(HubConfig hubConfig, Consumer<URI> redirectUriConsumer) {
+		this.hubConfig = hubConfig;
 		this.redirectUriConsumer = redirectUriConsumer;
 	}
 
 	@Override
 	protected String call() throws Exception {
-		try (var authFlow = AuthFlow.init(authUri, tokenUri, clientId)) {
+		try (var authFlow = AuthFlow.init(hubConfig)) {
 			return authFlow.run(uri -> Platform.runLater(() -> redirectUriConsumer.accept(uri)));
 		}
 	}
+
+	private final HubConfig hubConfig;
 }

+ 13 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/HubConfig.java

@@ -0,0 +1,13 @@
+package org.cryptomator.ui.keyloading.hub;
+
+// needs to be accessible by JSON decoder
+public class HubConfig {
+
+	public String clientId;
+	public String authEndpoint;
+	public String tokenEndpoint;
+	public String deviceRegistrationUrl;
+	public String unlockSuccessUrl;
+	public String unlockErrorUrl;
+
+}

+ 26 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java

@@ -5,6 +5,7 @@ import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.IntoMap;
 import dagger.multibindings.StringKey;
+import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxControllerKey;
 import org.cryptomator.ui.common.FxmlFile;
@@ -19,6 +20,8 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
 
 import javax.inject.Named;
 import javafx.scene.Scene;
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.net.URI;
 import java.security.KeyPair;
 import java.util.ResourceBundle;
@@ -33,6 +36,16 @@ public abstract class HubKeyLoadingModule {
 		CANCELLED
 	}
 
+	@Provides
+	@KeyLoadingScoped
+	static HubConfig provideHubConfig(@KeyLoading Vault vault) {
+		try {
+			return vault.getUnverifiedVaultConfig().getHeader("hub", HubConfig.class);
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+
 	@Provides
 	@KeyLoadingScoped
 	static AtomicReference<KeyPair> provideKeyPair() {
@@ -92,6 +105,14 @@ public abstract class HubKeyLoadingModule {
 		return fxmlLoaders.createScene(FxmlFile.HUB_RECEIVE_KEY);
 	}
 
+	@Provides
+	@FxmlScene(FxmlFile.HUB_REGISTER_DEVICE)
+	@KeyLoadingScoped
+	static Scene provideHubRegisterDeviceScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene(FxmlFile.HUB_REGISTER_DEVICE);
+	}
+
+
 	@Binds
 	@IntoMap
 	@FxControllerKey(P12Controller.class)
@@ -124,4 +145,9 @@ public abstract class HubKeyLoadingModule {
 	@FxControllerKey(ReceiveKeyController.class)
 	abstract FxController bindReceiveKeyController(ReceiveKeyController controller);
 
+	@Binds
+	@IntoMap
+	@FxControllerKey(RegisterDeviceController.class)
+	abstract FxController bindRegisterDeviceController(RegisterDeviceController controller);
+
 }

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

@@ -29,7 +29,6 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
 	static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http";
 	static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
 
-	private final Vault vault;
 	private final Stage window;
 	private final Lazy<Scene> p12LoadingScene;
 	private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction;
@@ -37,8 +36,7 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
 	private final AtomicReference<EciesParams> eciesParams;
 
 	@Inject
-	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;
+	public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> userInteraction, AtomicReference<KeyPair> keyPairRef, AtomicReference<EciesParams> eciesParams) {
 		this.window = window;
 		this.p12LoadingScene = p12LoadingScene;
 		this.userInteraction = userInteraction;

+ 17 - 49
src/main/java/org/cryptomator/ui/keyloading/hub/ReceiveKeyController.java

@@ -5,9 +5,12 @@ import com.google.common.base.Strings;
 import com.google.common.io.BaseEncoding;
 import com.google.gson.Gson;
 import com.google.gson.JsonElement;
+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;
@@ -23,6 +26,7 @@ import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
 import javafx.fxml.FXML;
+import javafx.scene.Scene;
 import javafx.scene.control.TextField;
 import javafx.stage.Stage;
 import javafx.stage.WindowEvent;
@@ -47,23 +51,20 @@ public class ReceiveKeyController implements FxController {
 
 	private final Stage window;
 	private final String bearerToken;
-	private final KeyPair keyPair;
 	private final AtomicReference<EciesParams> eciesParamsRef;
 	private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
+	private final Lazy<Scene> registerDeviceScene;
 	private final ErrorComponent.Builder errorComponent;
 	private final URI vaultBaseUri;
-	private final ObjectProperty<ReceiveKeyState> state = new SimpleObjectProperty<>(ReceiveKeyState.LOADING);
 	private final HttpClient httpClient;
 
-	public TextField deviceName;
-
 	@Inject
-	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) {
+	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, @FxmlScene(FxmlFile.HUB_REGISTER_DEVICE) Lazy<Scene> registerDeviceScene, ErrorComponent.Builder errorComponent) {
 		this.window = window;
 		this.bearerToken = Objects.requireNonNull(tokenRef.get());
-		this.keyPair = Objects.requireNonNull(keyPairRef.get());
 		this.eciesParamsRef = eciesParamsRef;
 		this.result = result;
+		this.registerDeviceScene = registerDeviceScene;
 		this.errorComponent = errorComponent;
 		this.vaultBaseUri = getVaultBaseUri(vault);
 		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
@@ -87,41 +88,8 @@ public class ReceiveKeyController implements FxController {
 		} else {
 			switch (response.statusCode()) {
 				case 200 -> retrievalSucceeded(response);
-				case 404 -> state.set(ReceiveKeyState.NEEDS_REGISTRATION);
-				default -> retrievalFailed(new IOException("Unexpected response " + response.statusCode()));
-			}
-		}
-	}
-
-	@FXML
-	public void register() {
-		Preconditions.checkArgument(!Strings.isNullOrEmpty(deviceName.getText()), "device name must not be empty");
-		var deviceKey = BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded());
-		var json = """
-    			{
-    				"id": "desktop-app",
-    				"name": "%s",
-    				"publicKey": "%s"
-    			}
-				""".formatted(deviceName.getText(), deviceKey); // TODO use gson
-
-		var regUri = URI.create("http://localhost:9090/devices/desktop-app"); // TODO read api base from vault config!
-		var request = HttpRequest.newBuilder(regUri) //
-				.header("Authorization", "Bearer " + bearerToken) //
-				.header("Content-Type", "application/json; charset=UTF-8") //
-				.PUT(HttpRequest.BodyPublishers.ofString(json)) //
-				.build();
-		httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) //
-				.whenCompleteAsync(this::createdNewDevice, Platform::runLater);
-	}
-
-	private void createdNewDevice(HttpResponse<InputStream> response, Throwable error) {
-		if (error != null) {
-			retrievalFailed(error);
-		} else {
-			switch (response.statusCode()) {
-				case 201 -> LOG.info("TODO device created, waiting for authorization");
-				case 409 -> LOG.info("TODO device already exists. still waiting for authorization");
+				case 403 -> accessNotGranted();
+				case 404 -> needsDeviceRegistration();
 				default -> retrievalFailed(new IOException("Unexpected response " + response.statusCode()));
 			}
 		}
@@ -143,6 +111,14 @@ public class ReceiveKeyController implements FxController {
 		}
 	}
 
+	private void needsDeviceRegistration() {
+		window.setScene(registerDeviceScene.get());
+	}
+
+	private void accessNotGranted() {
+		LOG.warn("unauthorized device"); // TODO
+	}
+
 	private void retrievalFailed(Throwable cause) {
 		result.interacted(HubKeyLoadingModule.HubLoadingResult.FAILED);
 		LOG.error("Key retrieval failed", cause);
@@ -183,13 +159,5 @@ public class ReceiveKeyController implements FxController {
 			throw new IllegalStateException("URI constructed from params known to be valid", e);
 		}
 	}
-	/* Getter/Setter */
 
-	public ObjectProperty<ReceiveKeyState> stateProperty() {
-		return state;
-	}
-
-	public ReceiveKeyState getState() {
-		return state.get();
-	}
 }

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

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

+ 59 - 0
src/main/java/org/cryptomator/ui/keyloading/hub/RegisterDeviceController.java

@@ -0,0 +1,59 @@
+package org.cryptomator.ui.keyloading.hub;
+
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.ui.keyloading.KeyLoading;
+import org.cryptomator.ui.keyloading.KeyLoadingScoped;
+
+import javax.inject.Inject;
+import javafx.application.Application;
+import javafx.event.Event;
+import javafx.fxml.FXML;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import java.security.KeyPair;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+@KeyLoadingScoped
+public class RegisterDeviceController implements FxController {
+
+	private final Application application;
+	private final Stage window;
+	private final HubConfig hubConfig;
+	private final KeyPair keyPair;
+	private final UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result;
+
+	@Inject
+	public RegisterDeviceController(Application application, @KeyLoading Stage window, HubConfig hubConfig, AtomicReference<KeyPair> keyPairRef, UserInteractionLock<HubKeyLoadingModule.HubLoadingResult> result) {
+		this.application = application;
+		this.window = window;
+		this.hubConfig = hubConfig;
+		this.keyPair = Objects.requireNonNull(keyPairRef.get());
+		this.result = result;
+		this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
+	}
+
+	@FXML
+	public void browse() {
+		var deviceKey = BaseEncoding.base64Url().omitPadding().encode(keyPair.getPublic().getEncoded());
+		var url = hubConfig.deviceRegistrationUrl + "?device_key=" + deviceKey;
+		// TODO append further params (including hmac of shown verification code)
+		application.getHostServices().showDocument(url);
+	}
+
+	@FXML
+	public void close() {
+		window.close();
+	}
+
+	private void windowClosed(WindowEvent windowEvent) {
+		// if not already interacted, mark this workflow as cancelled:
+		if (result.awaitingInteraction().get()) {
+			result.interacted(HubKeyLoadingModule.HubLoadingResult.CANCELLED);
+		}
+	}
+
+}

+ 2 - 15
src/main/resources/fxml/hub_receive_key.fxml

@@ -1,12 +1,9 @@
 <?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.Label?>
-<?import javafx.scene.control.TextField?>
 <?import javafx.scene.image.Image?>
 <?import javafx.scene.image.ImageView?>
 <?import javafx.scene.layout.HBox?>
@@ -18,10 +15,6 @@
 	  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>
@@ -31,19 +24,13 @@
 				<Image url="@../img/bot/bot.png"/>
 			</ImageView>
 
-			<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>
+			<FontAwesome5Spinner glyphSize="12"/>
 		</HBox>
 
 		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
-			<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+C">
 				<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>

+ 40 - 0
src/main/resources/fxml/hub_register_device.fxml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.image.Image?>
+<?import javafx.scene.image.ImageView?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.control.Hyperlink?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.keyloading.hub.RegisterDeviceController"
+	  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>
+
+			<VBox spacing="12">
+				<Hyperlink text="TODO: Register Device" onAction="#browse"/>
+			</VBox>
+		</HBox>
+
+		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
+			<ButtonBar buttonMinWidth="120" buttonOrder="+I">
+				<buttons>
+					<Button text="%generic.button.close" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#close"/>
+				</buttons>
+			</ButtonBar>
+		</VBox>
+	</children>
+</VBox>

+ 8 - 4
src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java

@@ -15,14 +15,18 @@ public class AuthFlowIntegrationTest {
 	}
 
 	private static final Logger LOG = LoggerFactory.getLogger(AuthFlowIntegrationTest.class);
-	private static final URI AUTH_URI = URI.create("http://localhost:8080/auth/realms/cryptomator/protocol/openid-connect/auth");
-	private static final URI TOKEN_URI = URI.create("http://localhost:8080/auth/realms/cryptomator/protocol/openid-connect/token");
-	private static final String CLIENT_ID = "cryptomator-hub";
 
 	@Test
 	@Disabled // only to be run manually
 	public void testRetrieveToken() throws Exception {
-		try (var authFlow = AuthFlow.init(AUTH_URI, TOKEN_URI, CLIENT_ID)) {
+		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.unlockSuccessUrl = "http://localhost:3000/#/unlock-success";
+		hubConfig.unlockErrorUrl = "http://localhost:3000/#/unlock-error";
+
+		try (var authFlow = AuthFlow.init(hubConfig)) {
 			var token = authFlow.run(uri -> {
 				LOG.info("Visit {} to authenticate", uri);
 			});