+ * 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