diff --git a/pom.xml b/pom.xml
index a3cfe9aa01..87c4ac2c5a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -70,6 +70,8 @@
4.5.13
5.4.1
3.9.9
+ 9.15.2
+ 1.69
1.7.30
2.8.6
3.0.1
@@ -140,6 +142,16 @@
vertx-web-client
${io.vertx.web.version}
+
+ com.nimbusds
+ nimbus-jose-jwt
+ ${com.nimbusds.jose.jwt.version}
+
+
+ org.bouncycastle
+ bcprov-jdk15on
+ ${org.bouncycastle.version}
+
org.slf4j
slf4j-jdk14
diff --git a/src/main/java/io/cryostat/Cryostat.java b/src/main/java/io/cryostat/Cryostat.java
index 9839272acd..7495fe8380 100644
--- a/src/main/java/io/cryostat/Cryostat.java
+++ b/src/main/java/io/cryostat/Cryostat.java
@@ -37,6 +37,7 @@
*/
package io.cryostat;
+import java.security.Security;
import java.util.concurrent.CompletableFuture;
import javax.inject.Singleton;
@@ -52,6 +53,7 @@
import io.cryostat.rules.RuleProcessor;
import io.cryostat.rules.RuleRegistry;
+import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton;
import dagger.Component;
class Cryostat {
@@ -59,6 +61,8 @@ class Cryostat {
public static void main(String[] args) throws Exception {
CryostatCore.initialize();
+ Security.addProvider(BouncyCastleProviderSingleton.getInstance());
+
final Logger logger = Logger.INSTANCE;
final Environment environment = new Environment();
diff --git a/src/main/java/io/cryostat/net/NetworkModule.java b/src/main/java/io/cryostat/net/NetworkModule.java
index afb4c0db52..ddc36e335c 100644
--- a/src/main/java/io/cryostat/net/NetworkModule.java
+++ b/src/main/java/io/cryostat/net/NetworkModule.java
@@ -53,6 +53,7 @@
import io.cryostat.core.sys.FileSystem;
import io.cryostat.core.tui.ClientWriter;
import io.cryostat.net.reports.ReportsModule;
+import io.cryostat.net.security.SecurityModule;
import io.cryostat.net.web.WebModule;
import com.github.benmanes.caffeine.cache.Scheduler;
@@ -71,6 +72,7 @@
includes = {
WebModule.class,
ReportsModule.class,
+ SecurityModule.class,
})
public abstract class NetworkModule {
diff --git a/src/main/java/io/cryostat/net/SslConfiguration.java b/src/main/java/io/cryostat/net/SslConfiguration.java
index 795518ead8..26d081c001 100644
--- a/src/main/java/io/cryostat/net/SslConfiguration.java
+++ b/src/main/java/io/cryostat/net/SslConfiguration.java
@@ -49,7 +49,7 @@
import io.vertx.core.net.PfxOptions;
import org.apache.commons.lang3.tuple.Pair;
-class SslConfiguration {
+public class SslConfiguration {
private final Environment env;
private final FileSystem fs;
private final Logger logger;
@@ -67,6 +67,12 @@ class SslConfiguration {
this.fs = fs;
this.logger = logger;
+ if (env.hasEnv("CRYOSTAT_DISABLE_SSL")) {
+ strategy = new NoSslStrategy();
+ logger.info("Selected NoSSL strategy");
+ return;
+ }
+
{
Path path = obtainKeyStorePathIfSpecified();
if (path != null) {
@@ -199,11 +205,11 @@ Pair discoverKeyCertPathPairInDefaultLocations() {
return Pair.of(key, cert);
}
- HttpServerOptions applyToHttpServerOptions(HttpServerOptions options) {
+ public HttpServerOptions applyToHttpServerOptions(HttpServerOptions options) {
return strategy.applyToHttpServerOptions(options);
}
- boolean enabled() {
+ public boolean enabled() {
return strategy.enabled();
}
diff --git a/src/main/java/io/cryostat/net/security/SecurityModule.java b/src/main/java/io/cryostat/net/security/SecurityModule.java
new file mode 100644
index 0000000000..7a96eb2406
--- /dev/null
+++ b/src/main/java/io/cryostat/net/security/SecurityModule.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright The Cryostat Authors
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or data
+ * (collectively the "Software"), free of charge and under any and all copyright
+ * rights in the Software, and any and all patent rights owned or freely
+ * licensable by each licensor hereunder covering either (i) the unmodified
+ * Software as contributed to or provided by such licensor, or (ii) the Larger
+ * Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software (each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ * The above copyright notice and either this complete permission notice or at
+ * a minimum a reference to the UPL must be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package io.cryostat.net.security;
+
+import io.cryostat.net.security.jwt.JwtModule;
+
+import dagger.Module;
+
+@Module(
+ includes = {
+ JwtModule.class,
+ })
+public abstract class SecurityModule {}
diff --git a/src/main/java/io/cryostat/net/security/jwt/AssetJwtHelper.java b/src/main/java/io/cryostat/net/security/jwt/AssetJwtHelper.java
new file mode 100644
index 0000000000..acdaada053
--- /dev/null
+++ b/src/main/java/io/cryostat/net/security/jwt/AssetJwtHelper.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright The Cryostat Authors
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or data
+ * (collectively the "Software"), free of charge and under any and all copyright
+ * rights in the Software, and any and all patent rights owned or freely
+ * licensable by each licensor hereunder covering either (i) the unmodified
+ * Software as contributed to or provided by such licensor, or (ii) the Larger
+ * Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software (each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ * The above copyright notice and either this complete permission notice or at
+ * a minimum a reference to the UPL must be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package io.cryostat.net.security.jwt;
+
+import java.net.MalformedURLException;
+import java.net.SocketException;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import io.cryostat.core.log.Logger;
+import io.cryostat.net.web.WebServer;
+
+import com.nimbusds.jose.EncryptionMethod;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWEAlgorithm;
+import com.nimbusds.jose.JWEDecrypter;
+import com.nimbusds.jose.JWEEncrypter;
+import com.nimbusds.jose.JWEHeader;
+import com.nimbusds.jose.JWEObject;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
+import dagger.Lazy;
+
+public class AssetJwtHelper {
+
+ public static final String RESOURCE_CLAIM = "resource";
+ public static final String JMXAUTH_CLAIM = "jmxauth";
+
+ private final Lazy webServer;
+ private final JWSSigner signer;
+ private final JWSVerifier verifier;
+ private final JWEEncrypter encrypter;
+ private final JWEDecrypter decrypter;
+ private final boolean subjectRequired;
+
+ AssetJwtHelper(
+ Lazy webServer,
+ JWSSigner signer,
+ JWSVerifier verifier,
+ JWEEncrypter encrypter,
+ JWEDecrypter decrypter,
+ boolean subjectRequired,
+ Logger logger) {
+ this.webServer = webServer;
+ this.signer = signer;
+ this.verifier = verifier;
+ this.encrypter = encrypter;
+ this.decrypter = decrypter;
+ this.subjectRequired = subjectRequired;
+ }
+
+ public String createAssetDownloadJwt(String subject, String resource, String jmxauth)
+ throws JOSEException, SocketException, UnknownHostException, URISyntaxException,
+ MalformedURLException {
+ String issuer = webServer.get().getHostUrl().toString();
+ Date now = Date.from(Instant.now());
+ Date expiry = Date.from(now.toInstant().plusSeconds(120));
+ JWTClaimsSet claims =
+ new JWTClaimsSet.Builder()
+ .issuer(issuer)
+ .audience(issuer)
+ .issueTime(now)
+ .notBeforeTime(now)
+ .expirationTime(expiry)
+ .subject(subject)
+ .claim(RESOURCE_CLAIM, resource)
+ .claim(JMXAUTH_CLAIM, jmxauth)
+ .build();
+
+ SignedJWT jwt = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.HS256).build(), claims);
+ jwt.sign(signer);
+
+ JWEHeader header =
+ new JWEHeader.Builder(JWEAlgorithm.DIR, EncryptionMethod.A256GCM)
+ .contentType("JWT")
+ .build();
+ JWEObject jwe = new JWEObject(header, new Payload(jwt));
+ jwe.encrypt(encrypter);
+
+ return jwe.serialize();
+ }
+
+ public JWT parseAssetDownloadJwt(String rawToken)
+ throws ParseException, JOSEException, BadJWTException, SocketException,
+ UnknownHostException, URISyntaxException, MalformedURLException {
+ JWEObject jwe = JWEObject.parse(rawToken);
+ jwe.decrypt(decrypter);
+
+ SignedJWT jwt = jwe.getPayload().toSignedJWT();
+ jwt.verify(verifier);
+
+ // TODO extract this claims verifier
+ // TODO add a SecurityContext
+ String cryostatUri = webServer.get().getHostUrl().toString();
+ JWTClaimsSet exactMatchClaims =
+ new JWTClaimsSet.Builder().issuer(cryostatUri).audience(cryostatUri).build();
+ Set requiredClaimNames =
+ new HashSet<>(Set.of("exp", "nbf", "iat", "iss", "aud", RESOURCE_CLAIM));
+ if (subjectRequired) {
+ requiredClaimNames.add("sub");
+ }
+ new DefaultJWTClaimsVerifier<>(cryostatUri, exactMatchClaims, requiredClaimNames)
+ .verify(jwt.getJWTClaimsSet(), null);
+
+ return jwt;
+ }
+}
diff --git a/src/main/java/io/cryostat/net/security/jwt/JwtModule.java b/src/main/java/io/cryostat/net/security/jwt/JwtModule.java
new file mode 100644
index 0000000000..ca81da0f79
--- /dev/null
+++ b/src/main/java/io/cryostat/net/security/jwt/JwtModule.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright The Cryostat Authors
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or data
+ * (collectively the "Software"), free of charge and under any and all copyright
+ * rights in the Software, and any and all patent rights owned or freely
+ * licensable by each licensor hereunder covering either (i) the unmodified
+ * Software as contributed to or provided by such licensor, or (ii) the Larger
+ * Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software (each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ * The above copyright notice and either this complete permission notice or at
+ * a minimum a reference to the UPL must be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package io.cryostat.net.security.jwt;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.inject.Singleton;
+
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.sys.Environment;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.AuthenticationScheme;
+import io.cryostat.net.web.WebServer;
+
+import com.nimbusds.jose.JWEDecrypter;
+import com.nimbusds.jose.JWEEncrypter;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jose.crypto.DirectDecrypter;
+import com.nimbusds.jose.crypto.DirectEncrypter;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.MACVerifier;
+import dagger.Lazy;
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public abstract class JwtModule {
+
+ @Provides
+ @Singleton
+ static AssetJwtHelper provideJwtFactory(
+ Lazy webServer,
+ JWSSigner signer,
+ JWSVerifier verifier,
+ JWEEncrypter encrypter,
+ JWEDecrypter decrypter,
+ AuthManager auth,
+ Logger logger) {
+ try {
+ return new AssetJwtHelper(
+ webServer,
+ signer,
+ verifier,
+ encrypter,
+ decrypter,
+ !AuthenticationScheme.NONE.equals(auth.getScheme()),
+ logger);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Provides
+ @Singleton
+ static SecretKey provideSecretKey() {
+ try {
+ KeyGenerator generator = KeyGenerator.getInstance("AES");
+ generator.init(256);
+ return generator.generateKey();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Provides
+ @Singleton
+ static JWSSigner provideJwsSigner(SecretKey key) {
+ try {
+ return new MACSigner(key);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Provides
+ @Singleton
+ static JWSVerifier provideJwsVerifier(SecretKey key) {
+ try {
+ return new MACVerifier(key);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Provides
+ @Singleton
+ static JWEEncrypter provideJweEncrypter(Environment env, SecretKey key, Logger logger) {
+ try {
+ return new DirectEncrypter(key);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Provides
+ @Singleton
+ static JWEDecrypter provideJweDecrypter(Environment env, SecretKey key) {
+ try {
+ return new DirectDecrypter(key);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/main/java/io/cryostat/net/web/WebServer.java b/src/main/java/io/cryostat/net/web/WebServer.java
index cd19f61173..0d9b91b9c4 100644
--- a/src/main/java/io/cryostat/net/web/WebServer.java
+++ b/src/main/java/io/cryostat/net/web/WebServer.java
@@ -47,6 +47,7 @@
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -64,6 +65,7 @@
import io.cryostat.net.web.http.api.ApiData;
import io.cryostat.net.web.http.api.ApiMeta;
import io.cryostat.net.web.http.api.ApiResponse;
+import io.cryostat.net.web.http.api.ApiVersion;
import io.cryostat.net.web.http.api.v2.ApiException;
import io.cryostat.util.HttpStatusCodeIdentifier;
@@ -292,48 +294,41 @@ URI getHostUri() throws SocketException, UnknownHostException, URISyntaxExceptio
.normalize();
}
+ // FIXME this has an implicit dependency on the RecordingGetHandler path
public String getArchivedDownloadURL(String recordingName)
throws UnknownHostException, URISyntaxException, SocketException {
- return new URIBuilder(getHostUri())
- .setScheme(server.isSsl() ? "https" : "http")
- .setPathSegments("api", "v1", "recordings", recordingName)
- .build()
- .normalize()
- .toString();
+ return getAssetDownloadURL(ApiVersion.V1, "recordings", recordingName);
}
+ // FIXME this has a an implicit dependency on the TargetRecordingGetHandler path
public String getDownloadURL(JFRConnection connection, String recordingName)
throws URISyntaxException, IOException {
- return new URIBuilder(getHostUri())
- .setScheme(server.isSsl() ? "https" : "http")
- .setPathSegments(
- "api",
- "v1",
- "targets",
- getTargetId(connection),
- "recordings",
- recordingName)
- .build()
- .normalize()
- .toString();
+ return getAssetDownloadURL(
+ ApiVersion.V1, "targets", getTargetId(connection), "recordings", recordingName);
}
+ // FIXME this has a an implicit dependency on the ReportGetHandler path
public String getArchivedReportURL(String recordingName)
throws SocketException, UnknownHostException, URISyntaxException {
- return new URIBuilder(getHostUri())
- .setScheme(server.isSsl() ? "https" : "http")
- .setPathSegments("api", "v1", "reports", recordingName)
- .build()
- .normalize()
- .toString();
+ return getAssetDownloadURL(ApiVersion.V1, "reports", recordingName);
}
+ // FIXME this has a an implicit dependency on the TargetReportGetHandler path
public String getReportURL(JFRConnection connection, String recordingName)
throws URISyntaxException, IOException {
+ return getAssetDownloadURL(
+ ApiVersion.V1, "targets", getTargetId(connection), "reports", recordingName);
+ }
+
+ public String getAssetDownloadURL(ApiVersion apiVersion, String... pathSegments)
+ throws SocketException, UnknownHostException, URISyntaxException {
+ List segments = new ArrayList<>();
+ segments.add("api");
+ segments.add(apiVersion.getVersionString());
+ segments.addAll(Arrays.asList(pathSegments));
return new URIBuilder(getHostUri())
.setScheme(server.isSsl() ? "https" : "http")
- .setPathSegments(
- "api", "v1", "targets", getTargetId(connection), "reports", recordingName)
+ .setPathSegments(segments)
.build()
.normalize()
.toString();
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/AbstractJwtConsumingHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/AbstractJwtConsumingHandler.java
new file mode 100644
index 0000000000..6735cc630b
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/AbstractJwtConsumingHandler.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright The Cryostat Authors
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or data
+ * (collectively the "Software"), free of charge and under any and all copyright
+ * rights in the Software, and any and all patent rights owned or freely
+ * licensable by each licensor hereunder covering either (i) the unmodified
+ * Software as contributed to or provided by such licensor, or (ii) the Larger
+ * Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software (each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ * The above copyright notice and either this complete permission notice or at
+ * a minimum a reference to the UPL must be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package io.cryostat.net.web.http.api.beta;
+
+import java.net.MalformedURLException;
+import java.net.SocketException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.rmi.ConnectIOException;
+import java.text.ParseException;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.regex.Matcher;
+
+import javax.security.sasl.SaslException;
+
+import org.openjdk.jmc.rjmx.ConnectionException;
+
+import io.cryostat.core.log.Logger;
+import io.cryostat.core.net.Credentials;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.ConnectionDescriptor;
+import io.cryostat.net.security.jwt.AssetJwtHelper;
+import io.cryostat.net.web.WebServer;
+import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler;
+import io.cryostat.net.web.http.RequestHandler;
+import io.cryostat.net.web.http.api.v2.ApiException;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.proc.BadJWTException;
+import dagger.Lazy;
+import io.vertx.ext.web.RoutingContext;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+abstract class AbstractJwtConsumingHandler implements RequestHandler {
+
+ protected final AuthManager auth;
+ protected final AssetJwtHelper jwt;
+ protected final Lazy webServer;
+ protected final Logger logger;
+
+ protected AbstractJwtConsumingHandler(
+ AuthManager auth, AssetJwtHelper jwt, Lazy webServer, Logger logger) {
+ this.auth = auth;
+ this.jwt = jwt;
+ this.webServer = webServer;
+ this.logger = logger;
+ }
+
+ abstract void handleWithValidJwt(RoutingContext ctx, JWT jwt) throws Exception;
+
+ @Override
+ public final void handle(RoutingContext ctx) {
+ try {
+ JWT jwt = validateJwt(ctx);
+ handleWithValidJwt(ctx, jwt);
+ } catch (ConnectionException e) {
+ Throwable cause = e.getCause();
+ if (cause instanceof SecurityException || cause instanceof SaslException) {
+ ctx.response()
+ .putHeader(
+ AbstractAuthenticatedRequestHandler.JMX_AUTHENTICATE_HEADER,
+ "Basic");
+ throw new ApiException(427, "JMX Authentication Failure", e);
+ }
+ Throwable rootCause = ExceptionUtils.getRootCause(e);
+ if (rootCause instanceof ConnectIOException) {
+ throw new ApiException(502, "Target SSL Untrusted", e);
+ }
+ if (rootCause instanceof UnknownHostException) {
+ throw new ApiException(404, "Target Not Found", e);
+ }
+ throw new ApiException(500, e);
+ } catch (Exception e) {
+ if (e instanceof ApiException) {
+ throw (ApiException) e;
+ }
+ throw new ApiException(500, e);
+ }
+ }
+
+ private JWT validateJwt(RoutingContext ctx)
+ throws ParseException, JOSEException, SocketException, UnknownHostException,
+ URISyntaxException, MalformedURLException {
+ String token = ctx.queryParams().get("token");
+ JWT parsed;
+ try {
+ parsed = jwt.parseAssetDownloadJwt(token);
+ } catch (BadJWTException e) {
+ throw new ApiException(401, e);
+ }
+
+ URL hostUrl = webServer.get().getHostUrl();
+ URI requestUri = new URI(ctx.request().absoluteURI());
+ URI fullRequestUri =
+ new URI(hostUrl.getProtocol(), hostUrl.getAuthority(), null, null, null)
+ .resolve(requestUri.getRawPath());
+ URI resourceClaim;
+ try {
+ resourceClaim =
+ new URI(parsed.getJWTClaimsSet().getStringClaim(AssetJwtHelper.RESOURCE_CLAIM));
+ } catch (URISyntaxException use) {
+ throw new ApiException(401, use);
+ }
+ if (!Objects.equals(fullRequestUri, resourceClaim)) {
+ throw new ApiException(401, "Token resource claim does not match requested resource");
+ }
+
+ try {
+ String subject = parsed.getJWTClaimsSet().getSubject();
+ if (!auth.validateHttpHeader(() -> subject, resourceActions()).get()) {
+ throw new ApiException(401, "Token subject has insufficient permissions");
+ }
+ } catch (ExecutionException | InterruptedException e) {
+ throw new ApiException(401, "Token subject permissions could not be determined");
+ }
+
+ return parsed;
+ }
+
+ protected ConnectionDescriptor getConnectionDescriptorFromJwt(RoutingContext ctx, JWT jwt)
+ throws ParseException {
+ String targetId = ctx.pathParam("targetId");
+ // TODO inject the CredentialsManager here to check for stored credentials
+ Credentials credentials = null;
+ String jmxauth = jwt.getJWTClaimsSet().getStringClaim(AssetJwtHelper.JMXAUTH_CLAIM);
+ if (jmxauth != null) {
+ String c;
+ try {
+ Matcher m =
+ AbstractAuthenticatedRequestHandler.AUTH_HEADER_PATTERN.matcher(jmxauth);
+ if (!m.find()) {
+ throw new ApiException(
+ 427,
+ String.format("Invalid %s claim format", AssetJwtHelper.JMXAUTH_CLAIM));
+ }
+ String t = m.group("type");
+ if (!"basic".equals(t.toLowerCase())) {
+ throw new ApiException(
+ 427,
+ String.format(
+ "Unacceptable %s credentials type",
+ AssetJwtHelper.JMXAUTH_CLAIM));
+ }
+ c =
+ new String(
+ Base64.getUrlDecoder().decode(m.group("credentials")),
+ StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException iae) {
+ throw new ApiException(
+ 427,
+ String.format(
+ "%s claim credentials do not appear to be Base64-encoded",
+ AssetJwtHelper.JMXAUTH_CLAIM),
+ iae);
+ }
+ String[] parts = c.split(":");
+ if (parts.length != 2) {
+ throw new ApiException(
+ 427,
+ String.format(
+ "Unrecognized %s claim credential format",
+ AssetJwtHelper.JMXAUTH_CLAIM));
+ }
+ credentials = new Credentials(parts[0], parts[1]);
+ }
+ return new ConnectionDescriptor(targetId, credentials);
+ }
+}
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/AuthTokenPostBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/AuthTokenPostBodyHandler.java
new file mode 100644
index 0000000000..be01b13ab4
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/AuthTokenPostBodyHandler.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright The Cryostat Authors
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or data
+ * (collectively the "Software"), free of charge and under any and all copyright
+ * rights in the Software, and any and all patent rights owned or freely
+ * licensable by each licensor hereunder covering either (i) the unmodified
+ * Software as contributed to or provided by such licensor, or (ii) the Larger
+ * Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software (each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ * The above copyright notice and either this complete permission notice or at
+ * a minimum a reference to the UPL must be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package io.cryostat.net.web.http.api.beta;
+
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler;
+import io.cryostat.net.web.http.api.ApiVersion;
+
+import io.vertx.core.http.HttpMethod;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.handler.BodyHandler;
+
+class AuthTokenPostBodyHandler extends AbstractAuthenticatedRequestHandler {
+
+ static final BodyHandler BODY_HANDLER = BodyHandler.create(true);
+
+ @Inject
+ AuthTokenPostBodyHandler(AuthManager auth) {
+ super(auth);
+ }
+
+ @Override
+ public int getPriority() {
+ return DEFAULT_PRIORITY - 1;
+ }
+
+ @Override
+ public ApiVersion apiVersion() {
+ return ApiVersion.BETA;
+ }
+
+ @Override
+ public HttpMethod httpMethod() {
+ return HttpMethod.POST;
+ }
+
+ @Override
+ public Set resourceActions() {
+ return ResourceAction.NONE;
+ }
+
+ @Override
+ public String path() {
+ return basePath() + AuthTokenPostHandler.PATH;
+ }
+
+ @Override
+ public void handleAuthenticated(RoutingContext ctx) throws Exception {
+ BODY_HANDLER.handle(ctx);
+ }
+}
diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/AuthTokenPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/AuthTokenPostHandler.java
new file mode 100644
index 0000000000..e2f1be70bd
--- /dev/null
+++ b/src/main/java/io/cryostat/net/web/http/api/beta/AuthTokenPostHandler.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright The Cryostat Authors
+ *
+ * The Universal Permissive License (UPL), Version 1.0
+ *
+ * Subject to the condition set forth below, permission is hereby granted to any
+ * person obtaining a copy of this software, associated documentation and/or data
+ * (collectively the "Software"), free of charge and under any and all copyright
+ * rights in the Software, and any and all patent rights owned or freely
+ * licensable by each licensor hereunder covering either (i) the unmodified
+ * Software as contributed to or provided by such licensor, or (ii) the Larger
+ * Works (as defined below), to deal in both
+ *
+ * (a) the Software, and
+ * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
+ * one is included with the Software (each a "Larger Work" to which the Software
+ * is contributed by such licensors),
+ *
+ * without restriction, including without limitation the rights to copy, create
+ * derivative works of, display, perform, and distribute the Software and make,
+ * use, sell, offer for sale, import, export, have made, and have sold the
+ * Software and the Larger Work(s), and to sublicense the foregoing rights on
+ * either these or other terms.
+ *
+ * This license is subject to the following condition:
+ * The above copyright notice and either this complete permission notice or at
+ * a minimum a reference to the UPL must be included in all copies or
+ * substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package io.cryostat.net.web.http.api.beta;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import io.cryostat.core.log.Logger;
+import io.cryostat.net.AuthManager;
+import io.cryostat.net.security.ResourceAction;
+import io.cryostat.net.security.jwt.AssetJwtHelper;
+import io.cryostat.net.web.WebServer;
+import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler;
+import io.cryostat.net.web.http.HttpMimeType;
+import io.cryostat.net.web.http.api.ApiVersion;
+import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler;
+import io.cryostat.net.web.http.api.v2.ApiException;
+import io.cryostat.net.web.http.api.v2.IntermediateResponse;
+import io.cryostat.net.web.http.api.v2.RequestParameters;
+
+import com.google.gson.Gson;
+import dagger.Lazy;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.http.HttpMethod;
+import org.apache.http.client.utils.URIBuilder;
+
+class AuthTokenPostHandler extends AbstractV2RequestHandler