Use ServiceLoader instead of reflection to resolve implementation classes.

By using ServiceLoader the hardcoded dependency of implementation classes becomes obsolete, so that the API will be truly independent from the implementation. Also this approach paves the way for migration to JPMS modules, as these also leverage the ServiceLoader API.

Use ServiceLoader instead of reflection to resolve CompressionCodec implementation classes.

Isolate key- and key-pair generators and use ServiceLoader instead of reflection to invert dependencies.

Move FactoryLoader logic to Services class and improve package layout.

Resolve Deserializer using the ServiceLoader instead of reflection and hardcoded reference.

Resolve Serializer using the ServiceLoader instead of reflection and hardcoded reference.
This commit is contained in:
Jaap Coomans 2019-06-17 14:58:55 +02:00 committed by Brian Demers
parent bf7e300d6b
commit ef32a1386d
38 changed files with 713 additions and 196 deletions

View File

@ -0,0 +1,28 @@
package io.jsonwebtoken;
/**
* Factory for {@link CompressionCodec} implementations. Backs the {@link io.jsonwebtoken.CompressionCodecs} constants
* class. Implementations of jjwt-api should provide their own implementation and make it available via a provider
* configuration file in META-INF/services.
*/
public interface CompressionCodecFactory {
/**
* Creates a new instance of a CompressionCodec implementing the <a href="https://tools.ietf.org/html/rfc7518">JWA</a>
* standard <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
*
* @return A new instance of a deflate CompressionCodec
*/
CompressionCodec deflateCodec();
/**
* Creates a new instance of a CompressionCodec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a>
* compression algorithm. * <h3>Compatibility Warning</h3> * <p><b>This is not a standard JWA compression
* algorithm</b>. Be sure to use this only when you are confident * that all parties accessing the token support
* the gzip algorithm.</p> * <p>If you're concerned about compatibility, the {@link #deflateCodec()} code is JWA
* standards-compliant.</p>
*
* @return A new instance of a gzip CompressionCodec
*/
CompressionCodec gzipCodec();
}

View File

@ -15,7 +15,7 @@
*/
package io.jsonwebtoken;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Services;
/**
* Provides default implementations of the {@link CompressionCodec} interface.
@ -26,6 +26,8 @@ import io.jsonwebtoken.lang.Classes;
*/
public final class CompressionCodecs {
private static final CompressionCodecFactory FACTORY = Services.loadFirst(CompressionCodecFactory.class);
private CompressionCodecs() {
} //prevent external instantiation
@ -33,8 +35,7 @@ public final class CompressionCodecs {
* Codec implementing the <a href="https://tools.ietf.org/html/rfc7518">JWA</a> standard
* <a href="https://en.wikipedia.org/wiki/DEFLATE">deflate</a> compression algorithm
*/
public static final CompressionCodec DEFLATE =
Classes.newInstance("io.jsonwebtoken.impl.compression.DeflateCompressionCodec");
public static final CompressionCodec DEFLATE = FACTORY.deflateCodec();
/**
* Codec implementing the <a href="https://en.wikipedia.org/wiki/Gzip">gzip</a> compression algorithm.
@ -43,7 +44,6 @@ public final class CompressionCodecs {
* that all parties accessing the token support the gzip algorithm.</p>
* <p>If you're concerned about compatibility, the {@link #DEFLATE DEFLATE} code is JWA standards-compliant.</p>
*/
public static final CompressionCodec GZIP =
Classes.newInstance("io.jsonwebtoken.impl.compression.GzipCompressionCodec");
public static final CompressionCodec GZIP = FACTORY.gzipCodec();
}

View File

@ -0,0 +1,80 @@
package io.jsonwebtoken;
import java.util.Map;
/**
* Factory for creating instances of JWT interfaces. Backs the {@link io.jsonwebtoken.Jwts} factory class.
* Implementations of jjwt-api should provide their own implementation and make it available via a provider
* configuration file in META-INF/services.
*
* @since 0.1
*/
public interface JwtFactory {
/**
* Creates a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs. As this
* is a less common use of JWTs, consider using the {@link #jwsHeader()} factory method instead if you will later
* digitally sign the JWT.
*
* @return a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs.
*/
Header header();
/**
* Creates a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs, populated
* with the specified name/value pairs. As this is a less common use of JWTs, consider using the
* {@link #jwsHeader(java.util.Map)} factory method instead if you will later digitally sign the JWT.
*
* @return a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs.
*/
Header header(Map<String, Object> header);
/**
* Returns a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's).
*
* @return a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's).
* @see JwtBuilder#setHeader(Header)
*/
JwsHeader jwsHeader();
/**
* Returns a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's), populated with the
* specified name/value pairs.
*
* @return a new {@link JwsHeader} instance suitable for digitally signed JWTs (aka 'JWS's), populated with the
* specified name/value pairs.
* @see JwtBuilder#setHeader(Header)
*/
JwsHeader jwsHeader(Map<String, Object> header);
/**
* Returns a new {@link Claims} instance to be used as a JWT body.
*
* @return a new {@link Claims} instance to be used as a JWT body.
*/
Claims claims();
/**
* Returns a new {@link Claims} instance populated with the specified name/value pairs.
*
* @param claims the name/value pairs to populate the new Claims instance.
* @return a new {@link Claims} instance populated with the specified name/value pairs.
*/
Claims claims(Map<String, Object> claims);
/**
* Returns a new {@link JwtParser} instance that can be configured and then used to parse JWT strings.
*
* @return a new {@link JwtParser} instance that can be configured and then used to parse JWT strings.
*/
JwtParser parser();
/**
* Returns a new {@link JwtBuilder} instance that can be configured and then used to create JWT compact serialized
* strings.
*
* @return a new {@link JwtBuilder} instance that can be configured and then used to create JWT compact serialized
* strings.
*/
JwtBuilder builder();
}

View File

@ -15,7 +15,7 @@
*/
package io.jsonwebtoken;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Services;
import java.util.Map;
@ -23,11 +23,11 @@ import java.util.Map;
* Factory class useful for creating instances of JWT interfaces. Using this factory class can be a good
* alternative to tightly coupling your code to implementation classes.
*
* @since 0.1
* @since 0.11
*/
public final class Jwts {
private static final Class[] MAP_ARG = new Class[]{Map.class};
private static final JwtFactory FACTORY = Services.loadFirst(JwtFactory.class);
private Jwts() {
}
@ -40,7 +40,7 @@ public final class Jwts {
* @return a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs.
*/
public static Header header() {
return Classes.newInstance("io.jsonwebtoken.impl.DefaultHeader");
return FACTORY.header();
}
/**
@ -51,7 +51,7 @@ public final class Jwts {
* @return a new {@link Header} instance suitable for <em>plaintext</em> (not digitally signed) JWTs.
*/
public static Header header(Map<String, Object> header) {
return Classes.newInstance("io.jsonwebtoken.impl.DefaultHeader", MAP_ARG, header);
return FACTORY.header(header);
}
/**
@ -61,7 +61,7 @@ public final class Jwts {
* @see JwtBuilder#setHeader(Header)
*/
public static JwsHeader jwsHeader() {
return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwsHeader");
return FACTORY.jwsHeader();
}
/**
@ -73,7 +73,7 @@ public final class Jwts {
* @see JwtBuilder#setHeader(Header)
*/
public static JwsHeader jwsHeader(Map<String, Object> header) {
return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwsHeader", MAP_ARG, header);
return FACTORY.jwsHeader(header);
}
/**
@ -82,7 +82,7 @@ public final class Jwts {
* @return a new {@link Claims} instance to be used as a JWT body.
*/
public static Claims claims() {
return Classes.newInstance("io.jsonwebtoken.impl.DefaultClaims");
return FACTORY.claims();
}
/**
@ -92,7 +92,7 @@ public final class Jwts {
* @return a new {@link Claims} instance populated with the specified name/value pairs.
*/
public static Claims claims(Map<String, Object> claims) {
return Classes.newInstance("io.jsonwebtoken.impl.DefaultClaims", MAP_ARG, claims);
return FACTORY.claims(claims);
}
/**
@ -118,7 +118,7 @@ public final class Jwts {
*/
@Deprecated
public static JwtParser parser() {
return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwtParser");
return FACTORY.parser();
}
/**
@ -138,6 +138,6 @@ public final class Jwts {
* strings.
*/
public static JwtBuilder builder() {
return Classes.newInstance("io.jsonwebtoken.impl.DefaultJwtBuilder");
return FACTORY.builder();
}
}

View File

@ -0,0 +1,13 @@
package io.jsonwebtoken.lang;
import io.jsonwebtoken.JwtException;
/**
* Exception indicating that no implementation of an jjwt-api SPI was found on the classpath.
*/
public class ImplementationNotFoundException extends JwtException {
ImplementationNotFoundException(final String message) {
super(message);
}
}

View File

@ -0,0 +1,51 @@
package io.jsonwebtoken.lang;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
/**
* Helper class for loading services from the classpath, using a {@link ServiceLoader}. Decouples loading logic for
* better separation of concerns and testability.
*/
public final class Services {
/**
* Loads and instantiates all service implementation of the given SPI class and returns them as a List.
*
* @param spi The class of the Service Provider Interface
* @param <T> The type of the SPI
* @return An unmodifiable list with an instance of all available implementations of the SPI. No guarantee is given
* on the order of implementations, if more than one.
*/
public static <T> List<T> loadAllAvailableImplementations(Class<T> spi) {
ServiceLoader<T> serviceLoader = ServiceLoader.load(spi);
List<T> implementations = new ArrayList<>();
for (T implementation : serviceLoader) {
implementations.add(implementation);
}
return Collections.unmodifiableList(implementations);
}
/**
* Loads the first available implementation the given SPI class from the classpath. Uses the {@link ServiceLoader}
* to find implementations. When multiple implementations are available it will return the first one that it
* encounters. There is no guarantee with regard to ordering.
*
* @param spi The class of the Service Provider Interface
* @param <T> The type of the SPI
* @return A new instance of the service.
* @throws ImplementationNotFoundException When no implementation the SPI is available on the classpath.
*/
public static <T> T loadFirst(Class<T> spi) {
ServiceLoader<T> serviceLoader = ServiceLoader.load(spi);
if (serviceLoader.iterator().hasNext()) {
return serviceLoader.iterator().next();
} else {
throw new ImplementationNotFoundException("No implementation of " + spi.getName() + " found on the classpath. Make sure to include an implementation of jjwt-api.");
}
}
}

View File

@ -0,0 +1,19 @@
package io.jsonwebtoken.security;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
public interface KeyGenerator {
boolean supports(SignatureAlgorithm alg);
/**
* Generates a new secure-random secret key of a length suitable for creating and verifying HMAC signatures
* according to the specified {@code SignatureAlgorithm}.
*
* @param alg the desired signature algorithm
* @return a new secure-random secret key of a length suitable for creating and verifying HMAC signatures according
* to the specified {@code SignatureAlgorithm}.
*/
SecretKey generateKey(SignatureAlgorithm alg);
}

View File

@ -0,0 +1,19 @@
package io.jsonwebtoken.security;
import io.jsonwebtoken.SignatureAlgorithm;
import java.security.KeyPair;
public interface KeyPairGenerator {
boolean supports(SignatureAlgorithm alg);
/**
* Generates a new secure-random key pair of sufficient strength for the specified {@link SignatureAlgorithm} using
* JJWT's default SecureRandom instance.
*
* @param alg the algorithm indicating strength
* @return a new secure-randomly generated key pair of sufficient strength for the specified {@link
* SignatureAlgorithm}.
*/
KeyPair generateKeyPair(SignatureAlgorithm alg);
}

View File

@ -17,7 +17,7 @@ package io.jsonwebtoken.security;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Services;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@ -32,12 +32,8 @@ import java.util.List;
* @since 0.10.0
*/
public final class Keys {
private static final String MAC = "io.jsonwebtoken.impl.crypto.MacProvider";
private static final String RSA = "io.jsonwebtoken.impl.crypto.RsaProvider";
private static final String EC = "io.jsonwebtoken.impl.crypto.EllipticCurveProvider";
private static final Class[] SIG_ARG_TYPES = new Class[]{SignatureAlgorithm.class};
private static final List<KeyGenerator> KEY_GENERATORS = Services.loadAllAvailableImplementations(KeyGenerator.class);
private static final List<KeyPairGenerator> KEY_PAIR_GENERATORS = Services.loadAllAvailableImplementations(KeyPairGenerator.class);
//purposefully ordered higher to lower:
private static final List<SignatureAlgorithm> PREFERRED_HMAC_ALGS = Collections.unmodifiableList(Arrays.asList(
@ -129,15 +125,14 @@ public final class Keys {
*/
public static SecretKey secretKeyFor(SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
switch (alg) {
case HS256:
case HS384:
case HS512:
return Classes.invokeStatic(MAC, "generateKey", SIG_ARG_TYPES, alg);
default:
String msg = "The " + alg.name() + " algorithm does not support shared secret keys.";
throw new IllegalArgumentException(msg);
for (KeyGenerator keyGenerator : KEY_GENERATORS) {
if (keyGenerator.supports(alg)) {
return keyGenerator.generateKey(alg);
}
}
String msg = "The " + alg.name() + " algorithm does not support shared secret keys.";
throw new IllegalArgumentException(msg);
}
/**
@ -211,21 +206,14 @@ public final class Keys {
*/
public static KeyPair keyPairFor(SignatureAlgorithm alg) throws IllegalArgumentException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
switch (alg) {
case RS256:
case PS256:
case RS384:
case PS384:
case RS512:
case PS512:
return Classes.invokeStatic(RSA, "generateKeyPair", SIG_ARG_TYPES, alg);
case ES256:
case ES384:
case ES512:
return Classes.invokeStatic(EC, "generateKeyPair", SIG_ARG_TYPES, alg);
default:
String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
throw new IllegalArgumentException(msg);
for (KeyPairGenerator keyPairGenerator : KEY_PAIR_GENERATORS) {
if (keyPairGenerator.supports(alg)) {
return keyPairGenerator.generateKeyPair(alg);
}
}
String msg = "The " + alg.name() + " algorithm does not support Key Pairs.";
throw new IllegalArgumentException(msg);
}
}

View File

@ -15,41 +15,41 @@
*/
package io.jsonwebtoken
import io.jsonwebtoken.lang.Classes
import io.jsonwebtoken.lang.Services
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import static org.easymock.EasyMock.createMock
import static org.easymock.EasyMock.eq
import static org.easymock.EasyMock.expect
import static org.junit.Assert.assertSame
import static org.powermock.api.easymock.PowerMock.mockStatic
import static org.powermock.api.easymock.PowerMock.replay
import static org.powermock.api.easymock.PowerMock.verify
import static org.powermock.api.easymock.PowerMock.*
@RunWith(PowerMockRunner.class)
@PrepareForTest([Classes, CompressionCodecs])
@PrepareForTest([Services, CompressionCodecs])
class CompressionCodecsTest {
@Test
void testStatics() {
mockStatic(Classes)
mockStatic(Services)
def factory = createMock(CompressionCodecFactory)
expect(Services.loadFirst(CompressionCodecFactory)).andReturn(factory)
def deflate = createMock(CompressionCodec)
def gzip = createMock(CompressionCodec)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.compression.DeflateCompressionCodec"))).andReturn(deflate)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.compression.GzipCompressionCodec"))).andReturn(gzip)
expect(factory.deflateCodec()).andReturn(deflate)
expect(factory.gzipCodec()).andReturn(gzip)
replay Classes, deflate, gzip
replay Services, factory, deflate, gzip
assertSame deflate, CompressionCodecs.DEFLATE
assertSame gzip, CompressionCodecs.GZIP
verify Classes, deflate, gzip
verify Services, factory, deflate, gzip
//test coverage for private constructor:
new CompressionCodecs()

View File

@ -15,26 +15,44 @@
*/
package io.jsonwebtoken
import io.jsonwebtoken.lang.Classes
import io.jsonwebtoken.lang.Services
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import static org.easymock.EasyMock.createMock
import static org.easymock.EasyMock.eq
import static org.easymock.EasyMock.expect
import static org.easymock.EasyMock.mock
import static org.easymock.EasyMock.reset
import static org.easymock.EasyMock.same
import static org.junit.Assert.assertSame
import static org.powermock.api.easymock.PowerMock.createMock
import static org.powermock.api.easymock.PowerMock.mockStatic
import static org.powermock.api.easymock.PowerMock.replay
import static org.powermock.api.easymock.PowerMock.reset
import static org.powermock.api.easymock.PowerMock.verify
@RunWith(PowerMockRunner.class)
@PrepareForTest([Classes, Jwts])
@PrepareForTest([Services])
class JwtsTest {
static JwtFactory factory = mock(JwtFactory)
@BeforeClass
static void prepareFactory() {
mockStatic(Services)
expect(Services.loadFirst(JwtFactory)).andReturn(factory).anyTimes()
replay Services
}
@Before
void resetFactoryMock() {
reset(factory)
}
@Test
void testPrivateCtor() { //for code coverage only
new Jwts()
@ -43,146 +61,118 @@ class JwtsTest {
@Test
void testHeader() {
mockStatic(Classes)
def instance = createMock(Header)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultHeader"))).andReturn(instance)
expect(factory.header()).andReturn(instance)
replay Classes, instance
replay factory, instance
assertSame instance, Jwts.header()
verify Classes, instance
verify factory, instance
}
@Test
void testHeaderFromMap() {
mockStatic(Classes)
def map = [:]
def instance = createMock(Header)
expect(Classes.newInstance(
eq("io.jsonwebtoken.impl.DefaultHeader"),
same(Jwts.MAP_ARG),
same(map))
).andReturn(instance)
expect(factory.header(same(map) as Map<String, Object>)).andReturn(instance)
replay Classes, instance
replay factory, instance
assertSame instance, Jwts.header(map)
verify Classes, instance
verify factory, instance
}
@Test
void testJwsHeader() {
mockStatic(Classes)
def instance = createMock(JwsHeader)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwsHeader"))).andReturn(instance)
expect(factory.jwsHeader()).andReturn(instance)
replay Classes, instance
replay factory, instance
assertSame instance, Jwts.jwsHeader()
verify Classes, instance
verify factory, instance
}
@Test
void testJwsHeaderFromMap() {
mockStatic(Classes)
def map = [:]
def instance = createMock(JwsHeader)
expect(Classes.newInstance(
eq("io.jsonwebtoken.impl.DefaultJwsHeader"),
same(Jwts.MAP_ARG),
same(map))
).andReturn(instance)
expect(factory.jwsHeader(same(map) as Map<String, Object>)).andReturn(instance)
replay Classes, instance
replay factory, instance
assertSame instance, Jwts.jwsHeader(map)
verify Classes, instance
verify factory, instance
}
@Test
void testClaims() {
mockStatic(Classes)
def instance = createMock(Claims)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultClaims"))).andReturn(instance)
expect(factory.claims()).andReturn(instance)
replay Classes, instance
replay factory, instance
assertSame instance, Jwts.claims()
verify Classes, instance
verify factory, instance
}
@Test
void testClaimsFromMap() {
mockStatic(Classes)
def map = [:]
def instance = createMock(Claims)
expect(Classes.newInstance(
eq("io.jsonwebtoken.impl.DefaultClaims"),
same(Jwts.MAP_ARG),
same(map))
).andReturn(instance)
expect(factory.claims(same(map) as Map<String, Object>)).andReturn(instance)
replay Classes, instance
replay factory, instance
assertSame instance, Jwts.claims(map)
verify Classes, instance
verify factory, instance
}
@Test
void testParser() {
mockStatic(Classes)
def instance = createMock(JwtParser)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwtParser"))).andReturn(instance)
expect(factory.parser()).andReturn(instance)
replay Classes, instance
replay factory, instance
assertSame instance, Jwts.parser()
verify Classes, instance
verify factory, instance
}
@Test
void testBuilder() {
mockStatic(Classes)
def instance = createMock(JwtBuilder)
expect(Classes.newInstance(eq("io.jsonwebtoken.impl.DefaultJwtBuilder"))).andReturn(instance)
expect(factory.builder()).andReturn(instance)
replay Classes, instance
replay factory, instance
assertSame instance, Jwts.builder()
verify Classes, instance
verify factory, instance
}
}

View File

@ -0,0 +1,43 @@
package io.jsonwebtoken
class TestJwtFactory implements JwtFactory {
@Override
Header header() {
return null
}
@Override
Header header(final Map<String, Object> header) {
return null
}
@Override
JwsHeader jwsHeader() {
return null
}
@Override
JwsHeader jwsHeader(final Map<String, Object> header) {
return null
}
@Override
Claims claims() {
return null
}
@Override
Claims claims(final Map<String, Object> claim) {
return null
}
@Override
JwtParser parser() {
return null
}
@Override
JwtBuilder builder() {
return null
}
}

View File

@ -0,0 +1,48 @@
package io.jsonwebtoken.lang
import io.jsonwebtoken.JwtFactory
import io.jsonwebtoken.TestJwtFactory
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import static org.junit.Assert.assertNotNull
@RunWith(PowerMockRunner.class)
@PrepareForTest([Services])
class ServicesTest {
@Test
void testSuccessfulLoading() {
def factory = Services.loadFirst(JwtFactory.class)
assertNotNull factory
org.junit.Assert.assertEquals(TestJwtFactory, factory.class)
}
@Test(expected = ImplementationNotFoundException)
void testFailedLoading() {
ClassLoader cl = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new NoServicesClassLoader(cl))
Services.loadFirst(JwtFactory.class)
}
static class NoServicesClassLoader extends ClassLoader {
private NoServicesClassLoader(ClassLoader parent) {
super(parent)
}
@Override
Enumeration<URL> getResources(String name) throws IOException {
if (name.startsWith("META-INF/services/")) {
return java.util.Collections.emptyEnumeration()
} else {
return super.getResources(name)
}
}
}
}

View File

@ -16,7 +16,9 @@
package io.jsonwebtoken.security
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.lang.Classes
import io.jsonwebtoken.lang.Services
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
@ -25,8 +27,8 @@ import org.powermock.modules.junit4.PowerMockRunner
import javax.crypto.SecretKey
import java.security.KeyPair
import static org.easymock.EasyMock.eq
import static org.easymock.EasyMock.expect
import static org.easymock.EasyMock.mock
import static org.easymock.EasyMock.same
import static org.junit.Assert.*
import static org.powermock.api.easymock.PowerMock.*
@ -37,9 +39,28 @@ import static org.powermock.api.easymock.PowerMock.*
* The actual implementation assertions are done in KeysImplTest in the impl module.
*/
@RunWith(PowerMockRunner)
@PrepareForTest([Classes, Keys])
@PrepareForTest([Keys, Services])
class KeysTest {
static KeyGenerator keyGenerator = mock(KeyGenerator)
static KeyPairGenerator keyPairGenerator = mock(KeyPairGenerator)
@BeforeClass
static void prepareServices() {
mockStatic(Services)
expect(Services.loadAllAvailableImplementations(KeyGenerator)).andReturn([keyGenerator]).anyTimes()
expect(Services.loadAllAvailableImplementations(KeyPairGenerator)).andReturn([keyPairGenerator]).anyTimes()
replay Services
}
@Before
void reset() {
reset keyGenerator
reset keyPairGenerator
}
@Test
void testPrivateCtor() { //for code coverage only
new Keys()
@ -80,27 +101,30 @@ class KeysTest {
if (name.startsWith('H')) {
mockStatic(Classes)
def key = createMock(SecretKey)
expect(Classes.invokeStatic(eq(Keys.MAC), eq("generateKey"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(key)
expect(keyGenerator.supports(same(alg))).andReturn(true)
expect(keyGenerator.generateKey(same(alg))).andReturn(key)
replay Classes, key
replay keyGenerator, key
assertSame key, Keys.secretKeyFor(alg)
verify Classes, key
verify keyGenerator, key
reset Classes, key
reset keyGenerator, key
} else {
expect(keyGenerator.supports(same(alg))).andReturn(false)
replay(keyGenerator)
try {
Keys.secretKeyFor(alg)
fail()
} catch (IllegalArgumentException expected) {
assertEquals "The $name algorithm does not support shared secret keys." as String, expected.message
reset keyGenerator
}
}
}
@ -114,27 +138,29 @@ class KeysTest {
String name = alg.name()
if (name.equals('NONE') || name.startsWith('H')) {
expect(keyPairGenerator.supports(alg)).andReturn(false)
replay keyPairGenerator
try {
Keys.keyPairFor(alg)
fail()
} catch (IllegalArgumentException expected) {
assertEquals "The $name algorithm does not support Key Pairs." as String, expected.message
reset keyPairGenerator
}
} else {
String fqcn = name.startsWith('E') ? Keys.EC : Keys.RSA
mockStatic Classes
def pair = createMock(KeyPair)
expect(Classes.invokeStatic(eq(fqcn), eq("generateKeyPair"), same(Keys.SIG_ARG_TYPES), same(alg))).andReturn(pair)
expect(keyPairGenerator.supports(same(alg))).andReturn(true)
expect(keyPairGenerator.generateKeyPair(same(alg))).andReturn(pair)
replay Classes, pair
replay keyPairGenerator, pair
assertSame pair, Keys.keyPairFor(alg)
verify Classes, pair
verify keyPairGenerator, pair
reset Classes, pair
reset keyPairGenerator, pair
}
}
}

View File

@ -0,0 +1 @@
io.jsonwebtoken.TestJwtFactory

View File

@ -0,0 +1 @@
io.jsonwebtoken.io.JacksonDeserializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.io.JacksonSerializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.io.OrgJsonDeserializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.io.OrgJsonSerializer

View File

@ -0,0 +1,18 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.CompressionCodec;
import io.jsonwebtoken.CompressionCodecFactory;
import io.jsonwebtoken.impl.compression.DeflateCompressionCodec;
import io.jsonwebtoken.impl.compression.GzipCompressionCodec;
public class DefaultCompressionCodecFactory implements CompressionCodecFactory {
@Override
public CompressionCodec deflateCodec() {
return new DeflateCompressionCodec();
}
@Override
public CompressionCodec gzipCodec() {
return new GzipCompressionCodec();
}
}

View File

@ -0,0 +1,58 @@
package io.jsonwebtoken.impl;
import java.util.Map;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtFactory;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.impl.DefaultClaims;
import io.jsonwebtoken.impl.DefaultHeader;
import io.jsonwebtoken.impl.DefaultJwsHeader;
import io.jsonwebtoken.impl.DefaultJwtBuilder;
import io.jsonwebtoken.impl.DefaultJwtParser;
public class DefaultJwtFactory implements JwtFactory {
@Override
public Header header() {
return new DefaultHeader();
}
@Override
public Header header(final Map<String, Object> map) {
return new DefaultHeader(map);
}
@Override
public JwsHeader jwsHeader() {
return new DefaultJwsHeader();
}
@Override
public JwsHeader jwsHeader(final Map<String, Object> header) {
return new DefaultJwsHeader(header);
}
@Override
public Claims claims() {
return new DefaultClaims();
}
@Override
public Claims claims(final Map<String, Object> claims) {
return new DefaultClaims(claims);
}
@Override
public JwtBuilder builder() {
return new DefaultJwtBuilder();
}
@Override
public JwtParser parser() {
return new DefaultJwtParser();
}
}

View File

@ -0,0 +1,18 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.KeyPairGenerator;
import java.security.KeyPair;
public class EllipticCurveKeyPairGenerator implements KeyPairGenerator {
@Override
public boolean supports(SignatureAlgorithm alg) {
return alg.isEllipticCurve();
}
@Override
public KeyPair generateKeyPair(SignatureAlgorithm alg) {
return EllipticCurveProvider.generateKeyPair(alg);
}
}

View File

@ -0,0 +1,19 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.KeyGenerator;
import javax.crypto.SecretKey;
public final class MacKeyGenerator implements KeyGenerator {
@Override
public boolean supports(SignatureAlgorithm alg) {
return alg.isHmac();
}
@Override
public SecretKey generateKey(SignatureAlgorithm alg) {
return MacProvider.generateKey(alg);
}
}

View File

@ -0,0 +1,18 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.KeyPairGenerator;
import java.security.KeyPair;
public class RsaKeyPairGenerator implements KeyPairGenerator {
@Override
public boolean supports(SignatureAlgorithm alg) {
return alg.isRsa();
}
@Override
public KeyPair generateKeyPair(SignatureAlgorithm alg) {
return RsaProvider.generateKeyPair(alg);
}
}

View File

@ -132,7 +132,6 @@ public abstract class RsaProvider extends SignatureProvider {
* @see #generateKeyPair(String, int, SecureRandom)
* @since 0.10.0
*/
@SuppressWarnings("unused") //used by io.jsonwebtoken.security.Keys
public static KeyPair generateKeyPair(SignatureAlgorithm alg) {
Assert.isTrue(alg.isRsa(), "Only RSA algorithms are supported by this method.");
int keySizeInBits = 4096;

View File

@ -17,8 +17,9 @@ package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicReference;
/**
@ -45,12 +46,10 @@ public class RuntimeClasspathDeserializerLocator<T> implements InstanceLocator<D
@SuppressWarnings("WeakerAccess") //to allow testing override
protected Deserializer<T> locate() {
if (isAvailable("io.jsonwebtoken.jackson.io.JacksonDeserializer")) {
return Classes.newInstance("io.jsonwebtoken.jackson.io.JacksonDeserializer");
} else if (isAvailable("io.jsonwebtoken.orgjson.io.OrgJsonDeserializer")) {
return Classes.newInstance("io.jsonwebtoken.orgjson.io.OrgJsonDeserializer");
} else if (isAvailable("io.jsonwebtoken.gson.io.GsonDeserializer")) {
return Classes.newInstance("io.jsonwebtoken.gson.io.GsonDeserializer");
ServiceLoader<Deserializer> serviceLoader = ServiceLoader.load(Deserializer.class);
Iterator<Deserializer> iterator = serviceLoader.iterator();
if(iterator.hasNext()) {
return (Deserializer<T>)iterator.next();
} else {
throw new IllegalStateException("Unable to discover any JSON Deserializer implementations on the classpath.");
}
@ -60,9 +59,4 @@ public class RuntimeClasspathDeserializerLocator<T> implements InstanceLocator<D
protected boolean compareAndSet(Deserializer<T> d) {
return DESERIALIZER.compareAndSet(null, d);
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean isAvailable(String fqcn) {
return Classes.isAvailable(fqcn);
}
}

View File

@ -17,8 +17,9 @@ package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicReference;
/**
@ -43,14 +44,12 @@ public class RuntimeClasspathSerializerLocator implements InstanceLocator<Serial
return serializer;
}
@SuppressWarnings("WeakerAccess") //to allow testing override
@SuppressWarnings({"unchecked", "WeakerAccess"}) //to allow testing override
protected Serializer<Object> locate() {
if (isAvailable("io.jsonwebtoken.jackson.io.JacksonSerializer")) {
return Classes.newInstance("io.jsonwebtoken.jackson.io.JacksonSerializer");
} else if (isAvailable("io.jsonwebtoken.orgjson.io.OrgJsonSerializer")) {
return Classes.newInstance("io.jsonwebtoken.orgjson.io.OrgJsonSerializer");
} else if (isAvailable("io.jsonwebtoken.gson.io.GsonSerializer")) {
return Classes.newInstance("io.jsonwebtoken.gson.io.GsonSerializer");
ServiceLoader<Serializer> serviceLoader = ServiceLoader.load(Serializer.class);
Iterator<Serializer> iterator = serviceLoader.iterator();
if(iterator.hasNext()) {
return (Serializer<Object>)iterator.next();
} else {
throw new IllegalStateException("Unable to discover any JSON Serializer implementations on the classpath.");
}
@ -60,9 +59,4 @@ public class RuntimeClasspathSerializerLocator implements InstanceLocator<Serial
protected boolean compareAndSet(Serializer<Object> s) {
return SERIALIZER.compareAndSet(null, s);
}
@SuppressWarnings("WeakerAccess") //to allow testing override
protected boolean isAvailable(String fqcn) {
return Classes.isAvailable(fqcn);
}
}

View File

@ -0,0 +1 @@
io.jsonwebtoken.impl.DefaultCompressionCodecFactory

View File

@ -0,0 +1 @@
io.jsonwebtoken.impl.DefaultJwtFactory

View File

@ -0,0 +1 @@
io.jsonwebtoken.impl.crypto.MacKeyGenerator

View File

@ -0,0 +1,2 @@
io.jsonwebtoken.impl.crypto.RsaKeyPairGenerator
io.jsonwebtoken.impl.crypto.EllipticCurveKeyPairGenerator

View File

@ -0,0 +1,26 @@
package io.jsonwebtoken.impl
import io.jsonwebtoken.impl.DefaultCompressionCodecFactory
import io.jsonwebtoken.impl.compression.DeflateCompressionCodec
import io.jsonwebtoken.impl.compression.GzipCompressionCodec
import org.junit.Test
import static org.junit.Assert.assertEquals
class DefaultCompressionCodecFactoryTest {
@Test
void testCreateDeflateCodec() {
def deflate = new DefaultCompressionCodecFactory().deflateCodec()
assertEquals(DeflateCompressionCodec, deflate.class)
}
@Test
void testCreateGzipCodec() {
def gzipCodec = new DefaultCompressionCodecFactory().gzipCodec()
assertEquals(GzipCompressionCodec, gzipCodec.class)
}
}

View File

@ -0,0 +1,19 @@
package io.jsonwebtoken.impl.io
class FakeServiceDescriptorClassLoader extends ClassLoader {
private String serviceDescriptor
FakeServiceDescriptorClassLoader(ClassLoader parent, String serviceDescriptor) {
super(parent)
this.serviceDescriptor = serviceDescriptor
}
@Override
Enumeration<URL> getResources(String name) throws IOException {
if (name.startsWith("META-INF/services/")) {
return super.getResources(serviceDescriptor)
} else {
return super.getResources(name)
}
}
}

View File

@ -0,0 +1,16 @@
package io.jsonwebtoken.impl.io
class NoServiceDescriptorClassLoader extends ClassLoader {
NoServiceDescriptorClassLoader(ClassLoader parent) {
super(parent)
}
@Override
Enumeration<URL> getResources(String name) throws IOException {
if (name.startsWith("META-INF/services/")) {
return Collections.emptyEnumeration()
} else {
return super.getResources(name)
}
}
}

View File

@ -15,7 +15,6 @@
*/
package io.jsonwebtoken.impl.io
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.jackson.io.JacksonDeserializer
import io.jsonwebtoken.orgjson.io.OrgJsonDeserializer
@ -29,6 +28,10 @@ import static org.junit.Assert.*
class RuntimeClasspathDeserializerLocatorTest {
private static final String TEST_SERVICE_DESCRIPTOR = "io.jsonwebtoken.io.Deserializer.test.orgjson"
private ClassLoader originalClassLoader
@Before
void setUp() {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(null)
@ -37,18 +40,23 @@ class RuntimeClasspathDeserializerLocatorTest {
@After
void teardown() {
RuntimeClasspathDeserializerLocator.DESERIALIZER.set(null)
restoreOriginalClassLoader()
}
private void restoreOriginalClassLoader() {
if(originalClassLoader != null) {
Thread.currentThread().setContextClassLoader(originalClassLoader)
originalClassLoader = null
}
}
@Test
void testClassIsNotAvailable() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
return false
}
}
prepareNoServiceDescriptorClassLoader()
try {
locator.getInstance()
new RuntimeClasspathDeserializerLocator().getInstance()
fail 'Located Deserializer class, whereas none was expected.'
} catch (Exception ex) {
assertEquals 'Unable to discover any JSON Deserializer implementations on the classpath.', ex.message
}
@ -99,20 +107,12 @@ class RuntimeClasspathDeserializerLocatorTest {
@Test
void testOrgJson() {
def locator = new RuntimeClasspathDeserializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (JacksonDeserializer.class.getName().equals(fqcn)) {
return false; //skip it to allow the OrgJson impl to be created
}
return super.isAvailable(fqcn)
}
}
prepareFakeServiceClassLoader()
def deserializer = locator.getInstance()
def deserializer = new RuntimeClasspathDeserializerLocator().getInstance()
assertTrue deserializer instanceof OrgJsonDeserializer
}
@Test
void testGson() {
def locator = new RuntimeClasspathDeserializerLocator() {
@ -131,4 +131,14 @@ class RuntimeClasspathDeserializerLocatorTest {
def deserializer = locator.getInstance()
assertTrue deserializer instanceof GsonDeserializer
}
private void prepareNoServiceDescriptorClassLoader() {
originalClassLoader = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new NoServiceDescriptorClassLoader(originalClassLoader))
}
private void prepareFakeServiceClassLoader() {
originalClassLoader = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new FakeServiceDescriptorClassLoader(originalClassLoader, TEST_SERVICE_DESCRIPTOR))
}
}

View File

@ -28,6 +28,10 @@ import static org.junit.Assert.*
class RuntimeClasspathSerializerLocatorTest {
private static final String TEST_SERVICE_DESCRIPTOR = "io.jsonwebtoken.io.Serializer.test.orgjson"
private ClassLoader originalClassLoader
@Before
void setUp() {
RuntimeClasspathSerializerLocator.SERIALIZER.set(null)
@ -36,18 +40,23 @@ class RuntimeClasspathSerializerLocatorTest {
@After
void teardown() {
RuntimeClasspathSerializerLocator.SERIALIZER.set(null)
restoreOriginalClassLoader()
}
private void restoreOriginalClassLoader() {
if(originalClassLoader != null) {
Thread.currentThread().setContextClassLoader(originalClassLoader)
originalClassLoader = null
}
}
@Test
void testClassIsNotAvailable() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
return false
}
}
prepareNoServiceDescriptorClassLoader()
try {
locator.getInstance()
new RuntimeClasspathSerializerLocator().getInstance()
fail 'Located Deserializer class, whereas none was expected.'
} catch (Exception ex) {
assertEquals 'Unable to discover any JSON Serializer implementations on the classpath.', ex.message
}
@ -98,20 +107,12 @@ class RuntimeClasspathSerializerLocatorTest {
@Test
void testOrgJson() {
def locator = new RuntimeClasspathSerializerLocator() {
@Override
protected boolean isAvailable(String fqcn) {
if (JacksonSerializer.class.getName().equals(fqcn)) {
return false //skip it to allow the OrgJson impl to be created
}
return super.isAvailable(fqcn)
}
}
prepareFakeServiceClassLoader()
def serializer = locator.getInstance()
def serializer = new RuntimeClasspathSerializerLocator().getInstance()
assertTrue serializer instanceof OrgJsonSerializer
}
@Test
void testGson() {
def locator = new RuntimeClasspathSerializerLocator() {
@ -130,4 +131,14 @@ class RuntimeClasspathSerializerLocatorTest {
def serializer = locator.getInstance()
assertTrue serializer instanceof GsonSerializer
}
private void prepareNoServiceDescriptorClassLoader() {
originalClassLoader = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new NoServiceDescriptorClassLoader(originalClassLoader))
}
private void prepareFakeServiceClassLoader() {
originalClassLoader = Thread.currentThread().getContextClassLoader()
Thread.currentThread().setContextClassLoader(new FakeServiceDescriptorClassLoader(originalClassLoader, TEST_SERVICE_DESCRIPTOR))
}
}

View File

@ -0,0 +1 @@
io.jsonwebtoken.io.OrgJsonDeserializer

View File

@ -0,0 +1 @@
io.jsonwebtoken.io.OrgJsonSerializer