Skip to content

Commit

Permalink
Restructured JwtBuilderUtil based on testing
Browse files Browse the repository at this point in the history
When I went to run tests on the reflection code I ran into a number of
errors. A few were straighfoward fixes but not all.

The issue requiring the biggest change as that some JWT suite classes
were being loaded as the JwtBuilderUtil class was being loaded,
or at least before the buildJwt() method started executing.

I had to move the JWT calls into a nested class to delay the loading
of JWT suite classes until inside a try-catch where I could handle it
as expected.
  • Loading branch information
bitwiseman committed Nov 8, 2023
1 parent de68272 commit 9b02348
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.kohsuke.github.extras.authorization;

import org.kohsuke.github.authorization.AuthorizationProvider;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
Expand All @@ -16,8 +18,6 @@

import javax.annotation.Nonnull;

import org.kohsuke.github.authorization.AuthorizationProvider;

/**
* A authorization provider that gives valid JWT tokens. These tokens are then used to create a time-based token to
* authenticate as an application. This token provider does not provide any kind of caching, and will always request a
Expand Down
Original file line number Diff line number Diff line change
@@ -1,127 +1,151 @@
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.Method;
import java.security.Key;
import java.security.PrivateKey;
import java.time.Instant;
import java.util.Date;
import java.util.logging.Logger;

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.jackson.io.JacksonSerializer;

/**
* This is a util to build a JWT.
*
* <p>
* 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 again, once we
* are sure, we do no longer need to support pre-0.12.x versions of jjwt.
* 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.
* </p>
*/
final class JwtBuilderUtil {

private static final Logger LOGGER = Logger.getLogger(JwtBuilderUtil.class.getName());

/**
* Get a method from an object.
* Build a JWT.
*
* @param obj object
* @param method method name
* @param params parameters of the method
* @return method
* @throws NoSuchMethodException if the method does not exist
* @param issuedAt
* issued at
* @param expiration
* expiration
* @param applicationId
* application id
* @param privateKey
* private key
* @return JWT
*/
private static Method getMethod(Object obj, String method, Class<?>... params) throws NoSuchMethodException {
Class<?> type = obj.getClass();
return type.getMethod(method, params);
}
static String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey) {

/**
* Check if an object has a method.
*
* @param obj object
* @param method method name
* @param params parameters of the method
* @return true if the method exists
*/
private static boolean hasMethod(Object obj, String method, Class<?>... params) {
try {
return JwtBuilderUtil.getMethod(obj, method, params) != null;
} catch (NoSuchMethodException e) {
return false;
return DefaultBuilderImpl.buildJwt(issuedAt, expiration, applicationId, privateKey);
} catch (NoSuchMethodError | NoClassDefFoundError e) {
LOGGER.info(
"You are using an outdated version of the io.jsonwebtoken:jjwt-* suite. v0.12.x or later is recommended.");
}

// older jjwt library versions
try {
return ReflectionBuilderImpl.buildJwt(issuedAt, expiration, applicationId, privateKey);
} catch (SecurityException | 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);
}
}

/**
* Build a JWT.
* A class to isolate loading of JWT classes allowing us to catch and handle linkage errors.
*
* @param issuedAt issued at
* @param expiration expiration
* @param applicationId application id
* @param privateKey private key
* @return JWT
* 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.
*/
static String buildJwt(Instant issuedAt, Instant expiration, String applicationId, PrivateKey privateKey) {
JwtBuilder jwtBuilder = Jwts.builder();
if (JwtBuilderUtil.hasMethod(jwtBuilder, "issuedAt", Date.class)) {
private static class DefaultBuilderImpl {

/**
* 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
*/
private static 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, Jwts.SIG.RS256);
return jwtBuilder.json(new JacksonSerializer<>()).compact();
}

LOGGER.warning(
"You are using an outdated version of the io.jsonwebtoken:jjwt-* suite. Please consider an update.");

// older jjwt library versions
try {
return JwtBuilderUtil.buildByReflection(jwtBuilder, issuedAt, expiration, applicationId, privateKey);
} catch (ReflectiveOperationException e) {
throw new JwtReflectiveBuilderException(
"Exception building a JWT with reflective access to outdated versions of the io.jsonwebtoken:jjwt-* suite. Please consider an update.",
e);
.signWith(privateKey, rs256)
.json(new JacksonSerializer<>());
return jwtBuilder.compact();
}
}

/**
* This method builds a JWT using older (pre 0.12.x) versions of jjwt library by
* leveraging reflection.
*
* @param jwtBuilder builder object
* @param issuedAt issued at
* @param expiration expiration
* @param applicationId application id
* @param privateKey private key
* @return JWT
* @throws ReflectiveOperationException if reflection fails
* A class to encapsulate building a JWT using reflection.
*/
private static String buildByReflection(JwtBuilder jwtBuilder, Instant issuedAt, Instant expiration,
String applicationId,
PrivateKey privateKey) throws ReflectiveOperationException {

Object builderObj = jwtBuilder;

Method setIssuedAtMethod = JwtBuilderUtil.getMethod(builderObj, "setIssuedAt", Date.class);
builderObj = setIssuedAtMethod.invoke(builderObj, Date.from(issuedAt));

Method setExpirationMethod = JwtBuilderUtil.getMethod(builderObj, "setExpiration", Date.class);
builderObj = setExpirationMethod.invoke(builderObj, Date.from(expiration));

Method setIssuerMethod = JwtBuilderUtil.getMethod(builderObj, "setIssuer", String.class);
builderObj = setIssuerMethod.invoke(builderObj, applicationId);

Method signWithMethod = JwtBuilderUtil.getMethod(builderObj, "signWith", PrivateKey.class,
SignatureAlgorithm.class);
builderObj = signWithMethod.invoke(builderObj, privateKey, SignatureAlgorithm.RS256);

Method serializeToJsonMethod = JwtBuilderUtil.getMethod(builderObj, "serializeToJsonWith",
JacksonSerializer.class);
builderObj = serializeToJsonMethod.invoke(builderObj, new JacksonSerializer<>());
private static class ReflectionBuilderImpl {
/**
* 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
* @throws ReflectiveOperationException
* if reflection fails
*/
private static String buildJwt(Instant issuedAt,
Instant expiration,
String applicationId,
PrivateKey privateKey) throws ReflectiveOperationException {

JwtBuilder jwtBuilder = Jwts.builder();
Class<?> jwtReflectionClass = jwtBuilder.getClass();

Method setIssuedAtMethod = jwtReflectionClass.getMethod("setIssuedAt", Date.class);
Method setIssuerMethod = jwtReflectionClass.getMethod("setIssuer", String.class);
Method setExpirationMethod = jwtReflectionClass.getMethod("setExpiration", Date.class);
Class<?> signatureAlgorithmClass = Class.forName("io.jsonwebtoken.SignatureAlgorithm");
Enum<?> rs256SignatureAlgorithm = createEnumInstance(signatureAlgorithmClass, "RS256");
Method signWithMethod = jwtReflectionClass.getMethod("signWith", Key.class, signatureAlgorithmClass);
Method serializeToJsonMethod = jwtReflectionClass.getMethod("serializeToJsonWith", Serializer.class);

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

JwtBuilder resultBuilder = (JwtBuilder) builderObj;
return resultBuilder.compact();
@SuppressWarnings("unchecked")
private static <T extends Enum<T>> T createEnumInstance(Class<?> type, String name) {
return Enum.valueOf((Class<T>) type, name);
}
}
}

0 comments on commit 9b02348

Please sign in to comment.