From eba522b6c0a4eb2e2e66a7aa361b17d61d214b5a Mon Sep 17 00:00:00 2001 From: Jamie Li Date: Fri, 24 May 2024 14:51:40 -0700 Subject: [PATCH] [ANCHOR-687] Upgrade to JDK-17 (#1367) ### Description - Upgrade JDK from 11 to 17. - Fix Jwt Json serialization ### Context - JDK 11 is expected to sunset in 10/2024. ### Testing - `./gradlew test` ### Documentation Update `How to Contribute` doc. ### Known limitations N/A --- .github/workflows/sub_essential_tests.yml | 6 ++-- .github/workflows/sub_extended_tests.yml | 6 ++-- .github/workflows/sub_gradle_build.yml | 4 +-- .github/workflows/sub_jacoco_report.yml | 6 ++-- Dockerfile | 4 +-- build.gradle.kts | 19 ++---------- core/build.gradle.kts | 2 ++ .../org/stellar/anchor/auth/AuthHelper.java | 11 +++++++ .../org/stellar/anchor/auth/JwtService.java | 18 +++++++---- .../anchor/auth/JwtsGsonDeserializer.java | 26 ++++++++++++++++ .../anchor/auth/JwtsGsonSerializer.java | 30 +++++++++++++++++++ .../java/org/stellar/anchor/util/Log.java | 16 +++++++++- .../A - Development Environment.md | 6 ++-- .../platform/integrationtest/Sep24Tests.kt | 2 +- gradle/libs.versions.toml | 2 +- kotlin-reference-server/build.gradle.kts | 2 +- .../fireblocks/FireblocksApiClient.java | 4 +-- .../callback/PlatformIntegrationHelperTest.kt | 13 ++++---- .../fireblocks/FireblocksApiClientTest.kt | 4 +-- wallet-reference-server/build.gradle.kts | 2 +- 20 files changed, 129 insertions(+), 54 deletions(-) create mode 100644 core/src/main/java/org/stellar/anchor/auth/JwtsGsonDeserializer.java create mode 100644 core/src/main/java/org/stellar/anchor/auth/JwtsGsonSerializer.java diff --git a/.github/workflows/sub_essential_tests.yml b/.github/workflows/sub_essential_tests.yml index 20f54ce7ba..9f3c8e5822 100644 --- a/.github/workflows/sub_essential_tests.yml +++ b/.github/workflows/sub_essential_tests.yml @@ -10,14 +10,14 @@ jobs: runs-on: ubuntu-latest-8-cores steps: ############################################# - # Setup JDK 11 + # Setup JDK 17 # Download, and Extract java-stellar-anchor-sdk.tar # Setup hostnames (/etc/hosts) ############################################# - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - name: Download java-stellar-anchor-sdk.tar diff --git a/.github/workflows/sub_extended_tests.yml b/.github/workflows/sub_extended_tests.yml index 10e69eddea..567d935974 100644 --- a/.github/workflows/sub_extended_tests.yml +++ b/.github/workflows/sub_extended_tests.yml @@ -10,14 +10,14 @@ jobs: runs-on: ubuntu-latest-16-cores steps: ############################################# - # Setup JDK 11 + # Setup JDK 17 # Download, and Extract java-stellar-anchor-sdk.tar # Setup hostnames (/etc/hosts) ############################################# - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - name: Download java-stellar-anchor-sdk.tar diff --git a/.github/workflows/sub_gradle_build.yml b/.github/workflows/sub_gradle_build.yml index 8ad00f9777..40a616c925 100644 --- a/.github/workflows/sub_gradle_build.yml +++ b/.github/workflows/sub_gradle_build.yml @@ -18,10 +18,10 @@ jobs: with: show-progress: false - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - name: Gradle Build with unit tests only diff --git a/.github/workflows/sub_jacoco_report.yml b/.github/workflows/sub_jacoco_report.yml index 7c31bcdd7b..fd7c0f3177 100644 --- a/.github/workflows/sub_jacoco_report.yml +++ b/.github/workflows/sub_jacoco_report.yml @@ -16,14 +16,14 @@ jobs: runs-on: ubuntu-latest steps: ############################################# - # Setup JDK 11 + # Setup JDK 17 # Download, and Extract java-stellar-anchor-sdk.tar # Setup hostnames (/etc/hosts) ############################################# - - name: Set up JDK 11 + - name: Set up JDK 17 uses: actions/setup-java@v3 with: - java-version: '11' + java-version: '17' distribution: 'adopt' - name: Download java-stellar-anchor-sdk.tar diff --git a/Dockerfile b/Dockerfile index cb54e4fb64..1cb59c1cf9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG BASE_IMAGE=gradle:7.6.4-jdk11-alpine +ARG BASE_IMAGE=gradle:7.6.4-jdk17-alpine FROM ${BASE_IMAGE} AS build WORKDIR /code @@ -9,7 +9,7 @@ RUN gradle clean bootJar --stacktrace -x test FROM ubuntu:22.04 RUN apt-get update && \ - apt-get install -y --no-install-recommends openjdk-11-jre + apt-get install -y --no-install-recommends openjdk-17-jre COPY --from=build /code/service-runner/build/libs/anchor-platform-runner*.jar /app/anchor-platform-runner.jar COPY --from=build /code/scripts/docker-start.sh /app/start.sh diff --git a/build.gradle.kts b/build.gradle.kts index e2d73c0fc3..171fdcb64e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,25 +51,10 @@ subprojects { maven { url = uri("https://jitpack.io") } } - /** Specifies JDK-11 */ - java { toolchain { languageVersion.set(JavaLanguageVersion.of(11)) } } + /** Specifies JDK-17 */ + java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } } spotless { - val javaVersion = System.getProperty("java.version") - if (javaVersion >= "17") { - logger.warn("!!! WARNING !!!") - logger.warn("=================") - logger.warn( - " You are running Java version:[{}]. Spotless may not work well with JDK 17.", - javaVersion) - logger.warn( - " In IntelliJ, go to [File -> Build -> Execution, Build, Deployment -> Gradle] and check Gradle JVM") - } - - if (javaVersion < "11") { - throw GradleException("Java 11 or greater is required for spotless Gradle plugin.") - } - java { importOrder("java", "javax", "org.stellar") removeUnusedImports() diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 25a9c6bb42..9b72c12ccd 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -16,6 +16,8 @@ dependencies { // Lombok should be used by all subprojects to reduce Java verbosity annotationProcessor(libs.lombok) + implementation("io.jsonwebtoken:jjwt-gson:0.12.5") + implementation(libs.spring.kafka) implementation(libs.spring.data.commons) diff --git a/core/src/main/java/org/stellar/anchor/auth/AuthHelper.java b/core/src/main/java/org/stellar/anchor/auth/AuthHelper.java index 4a16300c57..6689ba6995 100644 --- a/core/src/main/java/org/stellar/anchor/auth/AuthHelper.java +++ b/core/src/main/java/org/stellar/anchor/auth/AuthHelper.java @@ -1,5 +1,8 @@ package org.stellar.anchor.auth; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.Jwts; import java.util.Calendar; import javax.annotation.Nullable; import org.stellar.anchor.api.exception.InvalidConfigException; @@ -66,6 +69,14 @@ public AuthHeader createCustodyAuthHeader() throws InvalidConfig return createAuthHeader(CustodyAuthJwt.class); } + public static JwtBuilder jwtsBuilder() { + return Jwts.builder().json(JwtsGsonSerializer.newInstance()); + } + + public static JwtParserBuilder jwtsParser() { + return Jwts.parser().json(JwtsGsonDeserializer.newInstance()); + } + @Nullable private AuthHeader createAuthHeader(Class jwtClass) throws InvalidConfigException { diff --git a/core/src/main/java/org/stellar/anchor/auth/JwtService.java b/core/src/main/java/org/stellar/anchor/auth/JwtService.java index bffed6c8c7..8a159e214d 100644 --- a/core/src/main/java/org/stellar/anchor/auth/JwtService.java +++ b/core/src/main/java/org/stellar/anchor/auth/JwtService.java @@ -1,6 +1,7 @@ package org.stellar.anchor.auth; import static java.util.Date.from; +import static org.stellar.anchor.auth.AuthHelper.jwtsBuilder; import io.jsonwebtoken.*; import java.lang.reflect.InvocationTargetException; @@ -81,8 +82,9 @@ public JwtService( public String encode(Sep10Jwt token) { Instant timeExp = Instant.ofEpochSecond(token.getExp()); Instant timeIat = Instant.ofEpochSecond(token.getIat()); + JwtBuilder builder = - Jwts.builder() + jwtsBuilder() .id(token.getJti()) .issuer(token.getIss()) .subject(token.getSub()) @@ -106,7 +108,7 @@ public String encode(MoreInfoUrlJwt token) throws InvalidConfigException { Instant timeExp = Instant.ofEpochSecond(token.getExp()); JwtBuilder builder = - Jwts.builder().id(token.getJti()).expiration(from(timeExp)).subject(token.getSub()); + jwtsBuilder().id(token.getJti()).expiration(from(timeExp)).subject(token.getSub()); for (Map.Entry claim : token.claims.entrySet()) { builder.claim(claim.getKey(), claim.getValue()); } @@ -132,7 +134,7 @@ public String encode(Sep24InteractiveUrlJwt token) throws InvalidConfigException } Instant timeExp = Instant.ofEpochSecond(token.getExp()); JwtBuilder builder = - Jwts.builder().id(token.getJti()).expiration(from(timeExp)).subject(token.getSub()); + jwtsBuilder().id(token.getJti()).expiration(from(timeExp)).subject(token.getSub()); for (Map.Entry claim : token.claims.entrySet()) { builder.claim(claim.getKey(), claim.getValue()); } @@ -164,7 +166,7 @@ private String encode(ApiAuthJwt token, String secret) throws InvalidConfigExcep Instant timeExp = Instant.ofEpochSecond(token.getExp()); Instant timeIat = Instant.ofEpochSecond(token.getIat()); - JwtBuilder builder = Jwts.builder().issuedAt(from(timeIat)).expiration(from(timeExp)); + JwtBuilder builder = jwtsBuilder().issuedAt(from(timeIat)).expiration(from(timeExp)); return builder.signWith(KeyUtil.toSecretKeySpecOrNull(secret), Jwts.SIG.HS256).compact(); } @@ -193,7 +195,11 @@ public T decode(String cipher, Class cls) String.format("The Jwt class:[%s] is not supported", cls.getName())); } - Jwt jwt = Jwts.parser().verifyWith(KeyUtil.toSecretKeySpecOrNull(secret)).build().parse(cipher); + Jwt jwt = + AuthHelper.jwtsParser() + .verifyWith(KeyUtil.toSecretKeySpecOrNull(secret)) + .build() + .parse(cipher); if (cls.equals(Sep6MoreInfoUrlJwt.class)) { return (T) Sep6MoreInfoUrlJwt.class.getConstructor(Jwt.class).newInstance(jwt); @@ -223,7 +229,7 @@ public Jws getHeaderJwt(String signingKey, String cipher) { var jcaPublicKey = factory.generatePublic(x509KeySpec); try { - return Jwts.parser().verifyWith(jcaPublicKey).build().parseSignedClaims(cipher); + return AuthHelper.jwtsParser().verifyWith(jcaPublicKey).build().parseSignedClaims(cipher); } catch (Exception e) { Log.debugF("Invalid header signature {}", e.getMessage()); throw new SepValidationException("Invalid header signature"); diff --git a/core/src/main/java/org/stellar/anchor/auth/JwtsGsonDeserializer.java b/core/src/main/java/org/stellar/anchor/auth/JwtsGsonDeserializer.java new file mode 100644 index 0000000000..240926258e --- /dev/null +++ b/core/src/main/java/org/stellar/anchor/auth/JwtsGsonDeserializer.java @@ -0,0 +1,26 @@ +package org.stellar.anchor.auth; + +import com.google.gson.Gson; +import io.jsonwebtoken.io.DeserializationException; +import io.jsonwebtoken.io.Deserializer; +import java.io.Reader; +import java.util.Map; +import org.stellar.anchor.util.GsonUtils; + +public class JwtsGsonDeserializer implements Deserializer> { + private static final Gson gson = GsonUtils.getInstance(); + + public static JwtsGsonDeserializer newInstance() { + return new JwtsGsonDeserializer(); + } + + @Override + public Map deserialize(byte[] bytes) throws DeserializationException { + return gson.fromJson(new String(bytes), Map.class); + } + + @Override + public Map deserialize(Reader reader) throws DeserializationException { + return gson.fromJson(reader, Map.class); + } +} diff --git a/core/src/main/java/org/stellar/anchor/auth/JwtsGsonSerializer.java b/core/src/main/java/org/stellar/anchor/auth/JwtsGsonSerializer.java new file mode 100644 index 0000000000..39b7df5f36 --- /dev/null +++ b/core/src/main/java/org/stellar/anchor/auth/JwtsGsonSerializer.java @@ -0,0 +1,30 @@ +package org.stellar.anchor.auth; + +import com.google.gson.Gson; +import io.jsonwebtoken.io.SerializationException; +import io.jsonwebtoken.io.Serializer; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import lombok.SneakyThrows; +import org.stellar.anchor.util.GsonUtils; + +public class JwtsGsonSerializer implements Serializer> { + private static final Gson gson = GsonUtils.getInstance(); + + public static JwtsGsonSerializer newInstance() { + return new JwtsGsonSerializer(); + } + + @Override + public byte[] serialize(Map stringMap) throws SerializationException { + return gson.toJson(stringMap).getBytes(StandardCharsets.UTF_8); + } + + @SneakyThrows + @Override + public void serialize(Map stringMap, OutputStream outputStream) + throws SerializationException { + outputStream.write(serialize(stringMap)); + } +} diff --git a/core/src/main/java/org/stellar/anchor/util/Log.java b/core/src/main/java/org/stellar/anchor/util/Log.java index 095cc46c5b..c9628288c7 100644 --- a/core/src/main/java/org/stellar/anchor/util/Log.java +++ b/core/src/main/java/org/stellar/anchor/util/Log.java @@ -7,7 +7,9 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.function.Consumer; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,7 +83,19 @@ public static void error(final String msg) { * @param detail The additional object to be logged. */ public static void error(final String message, final Object detail) { - logMessageWithJson(message, detail, getLogger()::error); + if (detail instanceof Exception) { + Exception ex = (Exception) detail; + + logMessageWithJson( + message, + Arrays.stream(ex.getStackTrace()) + .map(StackTraceElement::toString) + .collect(Collectors.joining("\n")), + getLogger()::error); + return; + } else { + logMessageWithJson(message, detail, getLogger()::error); + } Metrics.counter("logger", "type", "error").increment(); } diff --git a/docs/01 - Contributing/A - Development Environment.md b/docs/01 - Contributing/A - Development Environment.md index 1b47157895..9f063db1fc 100644 --- a/docs/01 - Contributing/A - Development Environment.md +++ b/docs/01 - Contributing/A - Development Environment.md @@ -3,7 +3,7 @@ * [How to set up the development environment](#how-to-set-up-the-development-environment) - * [Install JDK 11](#install-jdk-11) + * [Install JDK 17](#install-jdk-17) * [Checkout the Project](#checkout-the-project) * [Set up `docker`](#set-up-docker) * [Set up your hosts file](#set-up-your-hosts-file) @@ -27,10 +27,10 @@ -## Install JDK 11 +## Install JDK 17 Before you start, please make sure you -have [JDK-11](https://www.oracle.com/java/technologies/javase-jdk11-downloads.html) installed on your machine. +have [JDK-17](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) installed on your machine. To check if you have it installed, run: diff --git a/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep24Tests.kt b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep24Tests.kt index 69cdd724db..6e613e898b 100644 --- a/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep24Tests.kt +++ b/essential-tests/src/testFixtures/kotlin/org/stellar/anchor/platform/integrationtest/Sep24Tests.kt @@ -140,7 +140,7 @@ class Sep24Tests : AbstractIntegrationTests(TestConfig()) { val jwt = jwtService.decode(cipher, Sep24InteractiveUrlJwt::class.java) assertEquals(response.id, jwt.jti) assertNotNull(jwt.claims["data"]) - assertNotNull((jwt.claims["data"] as HashMap<*, *>)["asset"]) + assertNotNull((jwt.claims["data"] as Map<*, *>)["asset"]) } /* diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41e9633d31..0592e6b7e0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ dotenv = "2.3.2" exposed = "0.49.0" findbugs-jsr305 = "3.0.2" flyway-core = "8.5.13" -google-gson = "2.8.9" +google-gson = "2.10.1" httpclient = "4.5.13" h2database = "2.2.224" hibernate-types = "2.18.0" diff --git a/kotlin-reference-server/build.gradle.kts b/kotlin-reference-server/build.gradle.kts index 46f882e40a..24b9340261 100644 --- a/kotlin-reference-server/build.gradle.kts +++ b/kotlin-reference-server/build.gradle.kts @@ -27,7 +27,7 @@ dependencies { tasks { compileKotlin { dependsOn("spotlessKotlinApply") - kotlinOptions.jvmTarget = "11" + kotlinOptions.jvmTarget = "17" } test { useJUnitPlatform() } diff --git a/platform/src/main/java/org/stellar/anchor/platform/custody/fireblocks/FireblocksApiClient.java b/platform/src/main/java/org/stellar/anchor/platform/custody/fireblocks/FireblocksApiClient.java index f856686b84..3550c8335b 100644 --- a/platform/src/main/java/org/stellar/anchor/platform/custody/fireblocks/FireblocksApiClient.java +++ b/platform/src/main/java/org/stellar/anchor/platform/custody/fireblocks/FireblocksApiClient.java @@ -1,8 +1,8 @@ package org.stellar.anchor.platform.custody.fireblocks; +import static org.stellar.anchor.auth.AuthHelper.jwtsBuilder; import static org.stellar.anchor.util.OkHttpUtil.TYPE_JSON; -import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.io.IOException; import java.math.BigInteger; @@ -117,7 +117,7 @@ private String signJwt(String path, String dataJSONString) { Instant now = Instant.now(); Instant expirationTime = now.plusSeconds(TOKEN_EXPIRATION_SECONDS); - return Jwts.builder() + return jwtsBuilder() .setSubject(apiKey) .setIssuedAt(Date.from(now)) .setExpiration(Date.from(expirationTime)) diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/callback/PlatformIntegrationHelperTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/callback/PlatformIntegrationHelperTest.kt index 5f679fc76c..daf86d1cf0 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/callback/PlatformIntegrationHelperTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/callback/PlatformIntegrationHelperTest.kt @@ -7,8 +7,8 @@ import okhttp3.Request import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource -import org.stellar.anchor.LockAndMockStatic import org.stellar.anchor.LockAndMockTest +import org.stellar.anchor.LockStatic import org.stellar.anchor.auth.* import org.stellar.anchor.auth.ApiAuthJwt.PlatformAuthJwt import org.stellar.anchor.auth.AuthType.* @@ -23,23 +23,24 @@ class PlatformIntegrationHelperTest { @ParameterizedTest @EnumSource(AuthType::class) - @LockAndMockStatic([Calendar::class]) + @LockStatic([Calendar::class]) fun test_getRequestBuilder(authType: AuthType) { when (authType) { JWT -> { - // Mock calendar to guarantee the jwt token format - val calendarSingleton = Calendar.getInstance() + val calendarSingleton = mockk() + every { calendarSingleton.timeInMillis } returns System.currentTimeMillis() val currentTimeMilliseconds = calendarSingleton.timeInMillis - mockkObject(calendarSingleton) every { calendarSingleton.timeInMillis } returns currentTimeMilliseconds every { calendarSingleton.timeInMillis = any() } answers { callOriginal() } + // Mock calendar to guarantee the jwt token format + mockkStatic(Calendar::class) every { Calendar.getInstance() } returns calendarSingleton // mock jwt token based on the mocked calendar val wantJwtToken = PlatformAuthJwt( currentTimeMilliseconds / 1000L, - (currentTimeMilliseconds + JWT_EXPIRATION_MILLISECONDS) / 1000L + (currentTimeMilliseconds + JWT_EXPIRATION_MILLISECONDS) / 1000L, ) val jwtService = diff --git a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/fireblocks/FireblocksApiClientTest.kt b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/fireblocks/FireblocksApiClientTest.kt index 908b6d6d1d..2592bc4a54 100644 --- a/platform/src/test/kotlin/org/stellar/anchor/platform/custody/fireblocks/FireblocksApiClientTest.kt +++ b/platform/src/test/kotlin/org/stellar/anchor/platform/custody/fireblocks/FireblocksApiClientTest.kt @@ -1,6 +1,5 @@ package org.stellar.anchor.platform.custody.fireblocks -import io.jsonwebtoken.Jwts import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -26,6 +25,7 @@ import org.skyscreamer.jsonassert.JSONAssert import org.skyscreamer.jsonassert.JSONCompareMode import org.stellar.anchor.api.exception.FireblocksException import org.stellar.anchor.api.exception.InvalidConfigException +import org.stellar.anchor.auth.AuthHelper import org.stellar.anchor.config.CustodySecretConfig import org.stellar.anchor.platform.config.FireblocksConfig import org.stellar.anchor.util.FileUtil.getResourceFileAsString @@ -231,7 +231,7 @@ class FireblocksApiClientTest { Assertions.assertTrue(token.startsWith("Bearer ")) val claims = - Jwts.parser() + AuthHelper.jwtsParser() .verifyWith(getPublicKey()) .build() .parseSignedClaims(StringUtils.substringAfter(token, "Bearer ")) diff --git a/wallet-reference-server/build.gradle.kts b/wallet-reference-server/build.gradle.kts index 34e2855490..a3dc538f53 100644 --- a/wallet-reference-server/build.gradle.kts +++ b/wallet-reference-server/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { tasks { compileKotlin { dependsOn("spotlessKotlinApply") - kotlinOptions.jvmTarget = "11" + kotlinOptions.jvmTarget = "17" } test { useJUnitPlatform() }