diff --git a/pom.xml b/pom.xml index 42133be660..bd9bb94e04 100644 --- a/pom.xml +++ b/pom.xml @@ -44,7 +44,7 @@ 0.50 false - 0.11.5 + 0.12.3 @@ -83,7 +83,7 @@ maven-surefire-plugin - 2.22.2 + 3.2.2 false @@ -458,6 +458,18 @@ + + + + com.fasterxml.jackson + jackson-bom + 2.15.3 + import + pom + + + + org.apache.commons @@ -510,7 +522,6 @@ com.fasterxml.jackson.core jackson-databind - 2.15.2 commons-io @@ -652,7 +663,7 @@ - test-slow-multireleasejar-flaky + test-jwt-slow-multireleasejar-flaky !test @@ -715,6 +726,40 @@ src/test/resources/slow-or-flaky-tests.txt + + + jwt0.11.x-test + integration-test + + test + + + ${project.basedir}/target/github-api-${project.version}.jar + false + src/test/resources/slow-or-flaky-tests.txt + @{jacoco.surefire.argLine} ${surefire.argLine} -Dtest.github.connector=okhttp + + io.jsonwebtoken:* + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + + + + diff --git a/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java b/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java index 85ed1f69ed..cc3f3c4df5 100644 --- a/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java +++ b/src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java @@ -1,9 +1,5 @@ package org.kohsuke.github.extras.authorization; -import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.jackson.io.JacksonSerializer; import org.kohsuke.github.authorization.AuthorizationProvider; import java.io.File; @@ -19,7 +15,6 @@ import java.time.Duration; import java.time.Instant; import java.util.Base64; -import java.util.Date; import javax.annotation.Nonnull; @@ -171,18 +166,10 @@ private String refreshJWT() { // Setting the issued at to a time in the past to allow for clock skew Instant issuedAt = getIssuedAt(now); - // Let's set the JWT Claims - JwtBuilder builder = Jwts.builder() - .setIssuedAt(Date.from(issuedAt)) - .setExpiration(Date.from(expiration)) - .setIssuer(this.applicationId) - .signWith(privateKey, SignatureAlgorithm.RS256); - // Token will refresh 2 minutes before it expires validUntil = expiration.minus(Duration.ofMinutes(2)); - // Builds the JWT and serializes it to a compact, URL-safe string - return builder.serializeToJsonWith(new JacksonSerializer<>()).compact(); + return JwtBuilderUtil.buildJwt(issuedAt, expiration, applicationId, privateKey); } Instant getIssuedAt(Instant now) { diff --git a/src/main/java/org/kohsuke/github/extras/authorization/JwtBuilderUtil.java b/src/main/java/org/kohsuke/github/extras/authorization/JwtBuilderUtil.java new file mode 100644 index 0000000000..754b2f315c --- /dev/null +++ b/src/main/java/org/kohsuke/github/extras/authorization/JwtBuilderUtil.java @@ -0,0 +1,202 @@ +package org.kohsuke.github.extras.authorization; + +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.jackson.io.JacksonSerializer; +import io.jsonwebtoken.security.SignatureAlgorithm; +import org.kohsuke.github.GHException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.Key; +import java.security.PrivateKey; +import java.time.Instant; +import java.util.Date; +import java.util.logging.Logger; + +/** + * This is a util to build a JWT. + * + *

+ * This class is used to build a JWT using the jjwt library. It uses reflection to support older versions of jjwt. The + * class may be removed once we are sure we no longer need to support pre-0.12.x versions of jjwt. + *

+ */ +final class JwtBuilderUtil { + + private static final Logger LOGGER = Logger.getLogger(JwtBuilderUtil.class.getName()); + + private static IJwtBuilder builder; + + /** + * Build a JWT. + * + * @param issuedAt + * issued at + * @param expiration + * expiration + * @param applicationId + * application id + * @param privateKey + * private key + * @return JWT + */ + static String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey) { + if (builder == null) { + createBuilderImpl(issuedAt, expiration, applicationId, privateKey); + } + return builder.buildJwt(issuedAt, expiration, applicationId, privateKey); + } + + private static void createBuilderImpl(Instant issuedAt, + Instant expiration, + String applicationId, + PrivateKey privateKey) { + // Figure out which builder to use and cache it. We don't worry about thread safety here because we're fine if + // the builder is assigned multiple times. The end result will be the same. + try { + builder = new DefaultBuilderImpl(); + } catch (NoSuchMethodError | NoClassDefFoundError e) { + LOGGER.warning( + "You are using an outdated version of the io.jsonwebtoken:jjwt-* suite. v0.12.x or later is recommended."); + + try { + ReflectionBuilderImpl reflectionBuider = new ReflectionBuilderImpl(); + // Build a JWT to eagerly check for any reflection errors. + reflectionBuider.buildJwtWithReflection(issuedAt, expiration, applicationId, privateKey); + + builder = reflectionBuider; + } catch (ReflectiveOperationException re) { + throw new GHException( + "Could not build JWT using reflection on io.jsonwebtoken:jjwt-* suite." + + "The minimum supported version is v0.11.x, v0.12.x or later is recommended.", + re); + } + } + } + + /** + * IJwtBuilder interface to isolate loading of JWT classes allowing us to catch and handle linkage errors. + */ + interface IJwtBuilder { + /** + * Build a JWT. + * + * @param issuedAt + * issued at + * @param expiration + * expiration + * @param applicationId + * application id + * @param privateKey + * private key + * @return JWT + */ + String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey); + } + + /** + * A class to isolate loading of JWT classes allowing us to catch and handle linkage errors. + * + * Without this class, JwtBuilderUtil.buildJwt() immediately throws NoClassDefFoundError when called. With this + * class the error is thrown when DefaultBuilder.build() is called allowing us to catch and handle it. + */ + private static class DefaultBuilderImpl implements IJwtBuilder { + /** + * This method builds a JWT using 0.12.x or later versions of jjwt library + * + * @param issuedAt + * issued at + * @param expiration + * expiration + * @param applicationId + * application id + * @param privateKey + * private key + * @return JWT + */ + public String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey) { + + // io.jsonwebtoken.security.SignatureAlgorithm is not present in v0.11.x and below. + // Trying to call a method that uses it causes "NoClassDefFoundError" if v0.11.x is being used. + SignatureAlgorithm rs256 = Jwts.SIG.RS256; + + JwtBuilder jwtBuilder = Jwts.builder(); + jwtBuilder = jwtBuilder.issuedAt(Date.from(issuedAt)) + .expiration(Date.from(expiration)) + .issuer(applicationId) + .signWith(privateKey, rs256) + .json(new JacksonSerializer<>()); + return jwtBuilder.compact(); + } + } + + /** + * A class to encapsulate building a JWT using reflection. + */ + private static class ReflectionBuilderImpl implements IJwtBuilder { + + private Method setIssuedAtMethod; + private Method setExpirationMethod; + private Method setIssuerMethod; + private Enum rs256SignatureAlgorithm; + private Method signWithMethod; + private Method serializeToJsonMethod; + + ReflectionBuilderImpl() throws ReflectiveOperationException { + JwtBuilder jwtBuilder = Jwts.builder(); + Class jwtReflectionClass = jwtBuilder.getClass(); + + setIssuedAtMethod = jwtReflectionClass.getMethod("setIssuedAt", Date.class); + setIssuerMethod = jwtReflectionClass.getMethod("setIssuer", String.class); + setExpirationMethod = jwtReflectionClass.getMethod("setExpiration", Date.class); + Class signatureAlgorithmClass = Class.forName("io.jsonwebtoken.SignatureAlgorithm"); + rs256SignatureAlgorithm = createEnumInstance(signatureAlgorithmClass, "RS256"); + signWithMethod = jwtReflectionClass.getMethod("signWith", Key.class, signatureAlgorithmClass); + serializeToJsonMethod = jwtReflectionClass.getMethod("serializeToJsonWith", Serializer.class); + } + + /** + * This method builds a JWT using older (pre 0.12.x) versions of jjwt library by leveraging reflection. + * + * @param issuedAt + * issued at + * @param expiration + * expiration + * @param applicationId + * application id + * @param privateKey + * private key + * @return JWTBuilder + */ + public String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey) { + + try { + return buildJwtWithReflection(issuedAt, expiration, applicationId, privateKey); + } catch (ReflectiveOperationException e) { + // This should never happen. Reflection errors should have been caught during initialization. + throw new GHException("Reflection errors during JWT creation should have been checked already.", e); + } + } + + private String buildJwtWithReflection(Instant issuedAt, + Instant expiration, + String applicationId, + PrivateKey privateKey) throws IllegalAccessException, InvocationTargetException { + JwtBuilder jwtBuilder = Jwts.builder(); + Object builderObj = jwtBuilder; + builderObj = setIssuedAtMethod.invoke(builderObj, Date.from(issuedAt)); + builderObj = setExpirationMethod.invoke(builderObj, Date.from(expiration)); + builderObj = setIssuerMethod.invoke(builderObj, applicationId); + builderObj = signWithMethod.invoke(builderObj, privateKey, rs256SignatureAlgorithm); + builderObj = serializeToJsonMethod.invoke(builderObj, new JacksonSerializer<>()); + return ((JwtBuilder) builderObj).compact(); + } + + @SuppressWarnings("unchecked") + private static > T createEnumInstance(Class type, String name) { + return Enum.valueOf((Class) type, name); + } + } +} diff --git a/src/test/resources/slow-or-flaky-tests.txt b/src/test/resources/slow-or-flaky-tests.txt index e0aa93a72e..6b1b48fe7b 100644 --- a/src/test/resources/slow-or-flaky-tests.txt +++ b/src/test/resources/slow-or-flaky-tests.txt @@ -1,5 +1,6 @@ **/extras/** **/GHRateLimitTest +**/GHPullRequestTest **/RequesterRetryTest **/RateLimitCheckerTest **/RateLimitHandlerTest