Prechádzať zdrojové kódy

outsourced authorization flow to https://github.com/coffeelibs/tiny-oauth2-client

Sebastian Stenzel 2 rokov pred
rodič
commit
9d4f9c12b9

+ 4 - 16
pom.xml

@@ -48,7 +48,6 @@
 		<zxcvbn.version>1.6.0</zxcvbn.version>
 		<slf4j.version>1.7.36</slf4j.version>
 		<logback.version>1.2.11</logback.version>
-		<jetty.version>10.0.6</jetty.version>
 
 		<!-- test dependencies -->
 		<junit.jupiter.version>5.8.1</junit.jupiter.version>
@@ -140,23 +139,12 @@
 			<version>${commons-lang3.version}</version>
 		</dependency>
 
+		<!-- OAuth/JWT -->
 		<dependency>
-			<groupId>org.eclipse.jetty</groupId>
-			<artifactId>jetty-server</artifactId>
-			<version>${jetty.version}</version>
+			<groupId>io.github.coffeelibs</groupId>
+			<artifactId>tiny-oauth2-client</artifactId>
+			<version>0.1.1</version>
 		</dependency>
-		<dependency>
-			<groupId>org.eclipse.jetty</groupId>
-			<artifactId>jetty-webapp</artifactId>
-			<version>${jetty.version}</version>
-		</dependency>
-		<dependency>
-			<groupId>org.eclipse.jetty</groupId>
-			<artifactId>jetty-servlets</artifactId>
-			<version>${jetty.version}</version>
-		</dependency>
-
-		<!-- JWT -->
 		<dependency>
 			<groupId>com.auth0</groupId>
 			<artifactId>java-jwt</artifactId>

+ 1 - 3
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 */

+ 0 - 186
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlow.java

@@ -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}.
- * <p>
- * @see <a href="https://datatracker.ietf.org/doc/html/rfc8252">RFC 8252</a>
- * @see <a href="https://datatracker.ietf.org/doc/html/rfc6749">RFC 6749</a>
- * @see <a href="https://datatracker.ietf.org/doc/html/rfc7636">RFC 7636</a>
- */
-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.
-	 * <p>
-	 * 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<URI> browser) throws IOException, InterruptedException {
-		var pkce = new PKCE();
-		var authCode = auth(pkce, browser);
-		return token(pkce, authCode);
-	}
-
-	private String auth(PKCE pkce, Consumer<URI> 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<InputStream> 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<String, String> 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<String> paramString(Map<String, String> 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 <a href="https://datatracker.ietf.org/doc/html/rfc7636">RFC 7636</a>
-	 */
-	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.");
-		}
-	}
-
-}

+ 5 - 2
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) {

+ 0 - 101
src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowReceiver.java

@@ -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:
- * <p>
- * We're spawning a local http server on a system-assigned high port and
- * use <code>http://127.0.0.1:{PORT}/callback</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 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> 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));
-		}
-	}
-
-}

+ 12 - 6
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<String> {
 
+	private final HubConfig hubConfig;
 	private final AuthFlowContext authFlowContext;
 	private final Consumer<URI> redirectUriConsumer;
 
@@ -23,11 +27,13 @@ class AuthFlowTask extends Task<String> {
 	}
 
 	@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;
 }

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

@@ -20,12 +20,4 @@ class HttpHelper {
 		}
 	}
 
-	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);
-		}
-	}
-
 }

+ 1 - 2
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);
 		}
 	}
 

+ 0 - 36
src/test/java/org/cryptomator/ui/keyloading/hub/AuthFlowIntegrationTest.java

@@ -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);
-		}
-	}
-
-}