Skip to content

Commit 17af7e6

Browse files
authored
Merge pull request hub4j#1727 from ihrigb/main
Update to newer version of jjwt library
2 parents ccf1ade + ef87bb7 commit 17af7e6

File tree

4 files changed

+253
-18
lines changed

4 files changed

+253
-18
lines changed

pom.xml

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
<jacoco.coverage.target.class.method>0.50</jacoco.coverage.target.class.method>
4545
<!-- For non-ci builds we'd like the build to still complete if jacoco metrics aren't met. -->
4646
<jacoco.haltOnFailure>false</jacoco.haltOnFailure>
47-
<jjwt.suite.version>0.11.5</jjwt.suite.version>
47+
<jjwt.suite.version>0.12.3</jjwt.suite.version>
4848

4949
<jacoco.surefire.argLine />
5050
<surefire.argLine />
@@ -83,7 +83,7 @@
8383
<plugins>
8484
<plugin>
8585
<artifactId>maven-surefire-plugin</artifactId>
86-
<version>2.22.2</version>
86+
<version>3.2.2</version>
8787
<configuration>
8888
<!-- SUREFIRE-1226 workaround -->
8989
<trimStackTrace>false</trimStackTrace>
@@ -458,6 +458,18 @@
458458
</plugins>
459459
</build>
460460

461+
<dependencyManagement>
462+
<dependencies>
463+
<dependency>
464+
<groupId>com.fasterxml.jackson</groupId>
465+
<artifactId>jackson-bom</artifactId>
466+
<version>2.15.3</version>
467+
<scope>import</scope>
468+
<type>pom</type>
469+
</dependency>
470+
</dependencies>
471+
</dependencyManagement>
472+
461473
<dependencies>
462474
<dependency>
463475
<groupId>org.apache.commons</groupId>
@@ -510,7 +522,6 @@
510522
<dependency>
511523
<groupId>com.fasterxml.jackson.core</groupId>
512524
<artifactId>jackson-databind</artifactId>
513-
<version>2.15.2</version>
514525
</dependency>
515526
<dependency>
516527
<groupId>commons-io</groupId>
@@ -652,7 +663,7 @@
652663
<profiles>
653664
<!-- only enable slow-or-flaky-test if -Dtest= is not present -->
654665
<profile>
655-
<id>test-slow-multireleasejar-flaky</id>
666+
<id>test-jwt-slow-multireleasejar-flaky</id>
656667
<activation>
657668
<property>
658669
<name>!test</name>
@@ -715,6 +726,40 @@
715726
<includesFile>src/test/resources/slow-or-flaky-tests.txt</includesFile>
716727
</configuration>
717728
</execution>
729+
<execution>
730+
<!-- Verify that JWT suite 0.11.x still works -->
731+
<id>jwt0.11.x-test</id>
732+
<phase>integration-test</phase>
733+
<goals>
734+
<goal>test</goal>
735+
</goals>
736+
<configuration>
737+
<classesDirectory>${project.basedir}/target/github-api-${project.version}.jar</classesDirectory>
738+
<useSystemClassLoader>false</useSystemClassLoader>
739+
<excludesFile>src/test/resources/slow-or-flaky-tests.txt</excludesFile>
740+
<argLine>@{jacoco.surefire.argLine} ${surefire.argLine} -Dtest.github.connector=okhttp</argLine>
741+
<classpathDependencyExcludes>
742+
<classpathDependencyExclude>io.jsonwebtoken:*</classpathDependencyExclude>
743+
</classpathDependencyExcludes>
744+
<additionalClasspathDependencies>
745+
<additionalClasspathDependency>
746+
<groupId>io.jsonwebtoken</groupId>
747+
<artifactId>jjwt-api</artifactId>
748+
<version>0.11.5</version>
749+
</additionalClasspathDependency>
750+
<additionalClasspathDependency>
751+
<groupId>io.jsonwebtoken</groupId>
752+
<artifactId>jjwt-impl</artifactId>
753+
<version>0.11.5</version>
754+
</additionalClasspathDependency>
755+
<additionalClasspathDependency>
756+
<groupId>io.jsonwebtoken</groupId>
757+
<artifactId>jjwt-jackson</artifactId>
758+
<version>0.11.5</version>
759+
</additionalClasspathDependency>
760+
</additionalClasspathDependencies>
761+
</configuration>
762+
</execution>
718763
</executions>
719764
</plugin>
720765
</plugins>

src/main/java/org/kohsuke/github/extras/authorization/JWTTokenProvider.java

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package org.kohsuke.github.extras.authorization;
22

3-
import io.jsonwebtoken.JwtBuilder;
4-
import io.jsonwebtoken.Jwts;
5-
import io.jsonwebtoken.SignatureAlgorithm;
6-
import io.jsonwebtoken.jackson.io.JacksonSerializer;
73
import org.kohsuke.github.authorization.AuthorizationProvider;
84

95
import java.io.File;
@@ -19,7 +15,6 @@
1915
import java.time.Duration;
2016
import java.time.Instant;
2117
import java.util.Base64;
22-
import java.util.Date;
2318

2419
import javax.annotation.Nonnull;
2520

@@ -171,18 +166,10 @@ private String refreshJWT() {
171166
// Setting the issued at to a time in the past to allow for clock skew
172167
Instant issuedAt = getIssuedAt(now);
173168

174-
// Let's set the JWT Claims
175-
JwtBuilder builder = Jwts.builder()
176-
.setIssuedAt(Date.from(issuedAt))
177-
.setExpiration(Date.from(expiration))
178-
.setIssuer(this.applicationId)
179-
.signWith(privateKey, SignatureAlgorithm.RS256);
180-
181169
// Token will refresh 2 minutes before it expires
182170
validUntil = expiration.minus(Duration.ofMinutes(2));
183171

184-
// Builds the JWT and serializes it to a compact, URL-safe string
185-
return builder.serializeToJsonWith(new JacksonSerializer<>()).compact();
172+
return JwtBuilderUtil.buildJwt(issuedAt, expiration, applicationId, privateKey);
186173
}
187174

188175
Instant getIssuedAt(Instant now) {
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package org.kohsuke.github.extras.authorization;
2+
3+
import io.jsonwebtoken.JwtBuilder;
4+
import io.jsonwebtoken.Jwts;
5+
import io.jsonwebtoken.io.Serializer;
6+
import io.jsonwebtoken.jackson.io.JacksonSerializer;
7+
import io.jsonwebtoken.security.SignatureAlgorithm;
8+
import org.kohsuke.github.GHException;
9+
10+
import java.lang.reflect.InvocationTargetException;
11+
import java.lang.reflect.Method;
12+
import java.security.Key;
13+
import java.security.PrivateKey;
14+
import java.time.Instant;
15+
import java.util.Date;
16+
import java.util.logging.Logger;
17+
18+
/**
19+
* This is a util to build a JWT.
20+
*
21+
* <p>
22+
* This class is used to build a JWT using the jjwt library. It uses reflection to support older versions of jjwt. The
23+
* class may be removed once we are sure we no longer need to support pre-0.12.x versions of jjwt.
24+
* </p>
25+
*/
26+
final class JwtBuilderUtil {
27+
28+
private static final Logger LOGGER = Logger.getLogger(JwtBuilderUtil.class.getName());
29+
30+
private static IJwtBuilder builder;
31+
32+
/**
33+
* Build a JWT.
34+
*
35+
* @param issuedAt
36+
* issued at
37+
* @param expiration
38+
* expiration
39+
* @param applicationId
40+
* application id
41+
* @param privateKey
42+
* private key
43+
* @return JWT
44+
*/
45+
static String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey) {
46+
if (builder == null) {
47+
createBuilderImpl(issuedAt, expiration, applicationId, privateKey);
48+
}
49+
return builder.buildJwt(issuedAt, expiration, applicationId, privateKey);
50+
}
51+
52+
private static void createBuilderImpl(Instant issuedAt,
53+
Instant expiration,
54+
String applicationId,
55+
PrivateKey privateKey) {
56+
// Figure out which builder to use and cache it. We don't worry about thread safety here because we're fine if
57+
// the builder is assigned multiple times. The end result will be the same.
58+
try {
59+
builder = new DefaultBuilderImpl();
60+
} catch (NoSuchMethodError | NoClassDefFoundError e) {
61+
LOGGER.warning(
62+
"You are using an outdated version of the io.jsonwebtoken:jjwt-* suite. v0.12.x or later is recommended.");
63+
64+
try {
65+
ReflectionBuilderImpl reflectionBuider = new ReflectionBuilderImpl();
66+
// Build a JWT to eagerly check for any reflection errors.
67+
reflectionBuider.buildJwtWithReflection(issuedAt, expiration, applicationId, privateKey);
68+
69+
builder = reflectionBuider;
70+
} catch (ReflectiveOperationException re) {
71+
throw new GHException(
72+
"Could not build JWT using reflection on io.jsonwebtoken:jjwt-* suite."
73+
+ "The minimum supported version is v0.11.x, v0.12.x or later is recommended.",
74+
re);
75+
}
76+
}
77+
}
78+
79+
/**
80+
* IJwtBuilder interface to isolate loading of JWT classes allowing us to catch and handle linkage errors.
81+
*/
82+
interface IJwtBuilder {
83+
/**
84+
* Build a JWT.
85+
*
86+
* @param issuedAt
87+
* issued at
88+
* @param expiration
89+
* expiration
90+
* @param applicationId
91+
* application id
92+
* @param privateKey
93+
* private key
94+
* @return JWT
95+
*/
96+
String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey);
97+
}
98+
99+
/**
100+
* A class to isolate loading of JWT classes allowing us to catch and handle linkage errors.
101+
*
102+
* Without this class, JwtBuilderUtil.buildJwt() immediately throws NoClassDefFoundError when called. With this
103+
* class the error is thrown when DefaultBuilder.build() is called allowing us to catch and handle it.
104+
*/
105+
private static class DefaultBuilderImpl implements IJwtBuilder {
106+
/**
107+
* This method builds a JWT using 0.12.x or later versions of jjwt library
108+
*
109+
* @param issuedAt
110+
* issued at
111+
* @param expiration
112+
* expiration
113+
* @param applicationId
114+
* application id
115+
* @param privateKey
116+
* private key
117+
* @return JWT
118+
*/
119+
public String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey) {
120+
121+
// io.jsonwebtoken.security.SignatureAlgorithm is not present in v0.11.x and below.
122+
// Trying to call a method that uses it causes "NoClassDefFoundError" if v0.11.x is being used.
123+
SignatureAlgorithm rs256 = Jwts.SIG.RS256;
124+
125+
JwtBuilder jwtBuilder = Jwts.builder();
126+
jwtBuilder = jwtBuilder.issuedAt(Date.from(issuedAt))
127+
.expiration(Date.from(expiration))
128+
.issuer(applicationId)
129+
.signWith(privateKey, rs256)
130+
.json(new JacksonSerializer<>());
131+
return jwtBuilder.compact();
132+
}
133+
}
134+
135+
/**
136+
* A class to encapsulate building a JWT using reflection.
137+
*/
138+
private static class ReflectionBuilderImpl implements IJwtBuilder {
139+
140+
private Method setIssuedAtMethod;
141+
private Method setExpirationMethod;
142+
private Method setIssuerMethod;
143+
private Enum<?> rs256SignatureAlgorithm;
144+
private Method signWithMethod;
145+
private Method serializeToJsonMethod;
146+
147+
ReflectionBuilderImpl() throws ReflectiveOperationException {
148+
JwtBuilder jwtBuilder = Jwts.builder();
149+
Class<?> jwtReflectionClass = jwtBuilder.getClass();
150+
151+
setIssuedAtMethod = jwtReflectionClass.getMethod("setIssuedAt", Date.class);
152+
setIssuerMethod = jwtReflectionClass.getMethod("setIssuer", String.class);
153+
setExpirationMethod = jwtReflectionClass.getMethod("setExpiration", Date.class);
154+
Class<?> signatureAlgorithmClass = Class.forName("io.jsonwebtoken.SignatureAlgorithm");
155+
rs256SignatureAlgorithm = createEnumInstance(signatureAlgorithmClass, "RS256");
156+
signWithMethod = jwtReflectionClass.getMethod("signWith", Key.class, signatureAlgorithmClass);
157+
serializeToJsonMethod = jwtReflectionClass.getMethod("serializeToJsonWith", Serializer.class);
158+
}
159+
160+
/**
161+
* This method builds a JWT using older (pre 0.12.x) versions of jjwt library by leveraging reflection.
162+
*
163+
* @param issuedAt
164+
* issued at
165+
* @param expiration
166+
* expiration
167+
* @param applicationId
168+
* application id
169+
* @param privateKey
170+
* private key
171+
* @return JWTBuilder
172+
*/
173+
public String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey) {
174+
175+
try {
176+
return buildJwtWithReflection(issuedAt, expiration, applicationId, privateKey);
177+
} catch (ReflectiveOperationException e) {
178+
// This should never happen. Reflection errors should have been caught during initialization.
179+
throw new GHException("Reflection errors during JWT creation should have been checked already.", e);
180+
}
181+
}
182+
183+
private String buildJwtWithReflection(Instant issuedAt,
184+
Instant expiration,
185+
String applicationId,
186+
PrivateKey privateKey) throws IllegalAccessException, InvocationTargetException {
187+
JwtBuilder jwtBuilder = Jwts.builder();
188+
Object builderObj = jwtBuilder;
189+
builderObj = setIssuedAtMethod.invoke(builderObj, Date.from(issuedAt));
190+
builderObj = setExpirationMethod.invoke(builderObj, Date.from(expiration));
191+
builderObj = setIssuerMethod.invoke(builderObj, applicationId);
192+
builderObj = signWithMethod.invoke(builderObj, privateKey, rs256SignatureAlgorithm);
193+
builderObj = serializeToJsonMethod.invoke(builderObj, new JacksonSerializer<>());
194+
return ((JwtBuilder) builderObj).compact();
195+
}
196+
197+
@SuppressWarnings("unchecked")
198+
private static <T extends Enum<T>> T createEnumInstance(Class<?> type, String name) {
199+
return Enum.valueOf((Class<T>) type, name);
200+
}
201+
}
202+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
**/extras/**
22
**/GHRateLimitTest
3+
**/GHPullRequestTest
34
**/RequesterRetryTest
45
**/RateLimitCheckerTest
56
**/RateLimitHandlerTest

0 commit comments

Comments
 (0)