diff --git a/pom.xml b/pom.xml index 05af17b..7d0f480 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ io.dropwizard-bundles dropwizard-api-key-bundle - 0.8.5-1-SNAPSHOT + 0.9.2-SNAPSHOT jar dropwizard-api-key-bundle @@ -48,10 +48,10 @@ UTF-8 UTF-8 - 0.8.5 - 2.21 + 0.9.2 + 2.22.1 4.12 - 1.10.17 + 1.10.19 diff --git a/readme.md b/readme.md index e658da0..7abaaf5 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,10 @@ # dropwizard-api-key-bundle A [Dropwizard][dropwizard] bundle that provides a simple way to manage API keys for callers of -your service. +your service. The bundle provides support for authentication only; authorization is supported +by optionally providing an `Authorizer` as documented below. -[![Build Status](https://secure.travis-ci.org/dropwizard-bundles/dropwizard-api-key-bundle.png?branch=master)] +[![Build Status](https://secure.travis-ci.org/dropwizard-bundles/dropwizard-api-key-bundle.png?branch=dropwizard-0.9)] (http://travis-ci.org/dropwizard-bundles/dropwizard-api-key-bundle) @@ -15,17 +16,18 @@ Just add this maven dependency to get started: io.dropwizard-bundles dropwizard-api-key-bundle - 0.8.4-1 + 0.9.2-SNAPSHOT ``` -Add the bundle to your environment: +If you only need authentication and a default `Principal` implementation add the default +version of the bundle to your environment: ```java public class MyApplication extends Application { @Override public void initialize(Bootstrap bootstrap) { - bootstrap.addBundle(new ApiKeyBundle<>()); + bootstrap.addBundle(new DefaultApiKeyBundle<>()); } @Override @@ -35,6 +37,37 @@ public class MyApplication extends Application { } ``` +If you need to provide an `Authorizer` or a different `Principal` (extending type), or both, +add the bundle to your environment and provide the type extending the `Principal` interface, an +implementation of the `Authorizer` and `PrincipalFactory` as appropriate: + +```java +public class MyApplication extends Application { + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(new ApiKeyBundle<>(User.class, new PrincipalFactory() { + @Override + public User create(String name) { + // Do something interesting... + return new User(name); + }}, new Authorizer() { + @Override + public boolean authorize(User user, String role) { + return user.getName().equals("application-1") && role.equals("ADMIN"); + } + }); + } + + @Override + public void run(MyConfiguration cfg, Environment env) throws Exception { + // ... + } +} +``` + +Additionally you can also pass an `UnauthorizedHandler` when creating the bundle, which is useful +if you need to customize the unauthorized response (e.g. type or entity). + You will also need to make your `MyConfiguration` class implement `ApiKeyBundleConfiguration` in order to provide the bundle with the necessary information it needs to know your API keys. @@ -55,9 +88,9 @@ public class MyConfiguration implements ApiKeyBundleConfiguration { ``` Now you can use API key based authentication in your application by declaring a method on a resource -that has an `@Auth` annotated `String` parameter. See the -[Dropwizard Authentication][authentication] documentation for more details. The passed in parameter -value will be the name of the application that made the request if the authentication process was +that has an `@Auth` annotated `Principal` parameter (or an extending type). See the +[Dropwizard Authentication][authentication] documentation for more details. The name of the `Principal` +will be the name of the application that made the request if the authentication process was successful. As far as configuration goes you can define your API keys in your application's config file. @@ -75,4 +108,4 @@ authentication: ``` [dropwizard]: http://dropwizard.io -[authentication]: http://www.dropwizard.io/0.8.5/docs/manual/auth.html \ No newline at end of file +[authentication]: http://www.dropwizard.io/0.9.2/docs/manual/auth.html \ No newline at end of file diff --git a/src/main/java/io/dropwizard/bundles/apikey/ApiKeyBundle.java b/src/main/java/io/dropwizard/bundles/apikey/ApiKeyBundle.java index df8c261..9c0ec7f 100644 --- a/src/main/java/io/dropwizard/bundles/apikey/ApiKeyBundle.java +++ b/src/main/java/io/dropwizard/bundles/apikey/ApiKeyBundle.java @@ -4,23 +4,66 @@ import com.google.common.base.Optional; import com.google.common.cache.CacheBuilderSpec; import io.dropwizard.ConfiguredBundle; -import io.dropwizard.auth.AuthFactory; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; import io.dropwizard.auth.Authenticator; +import io.dropwizard.auth.Authorizer; import io.dropwizard.auth.CachingAuthenticator; -import io.dropwizard.auth.basic.BasicAuthFactory; +import io.dropwizard.auth.DefaultUnauthorizedHandler; +import io.dropwizard.auth.UnauthorizedHandler; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; import io.dropwizard.auth.basic.BasicCredentials; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; +import java.security.Principal; import java.util.Map; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; /** * An API key bundle that allows you to configure a set of users/applications that are allowed to - * access APIs of the application in the Dropwizard configuration file. + * access APIs of the application in the Dropwizard configuration file. The API key bundle is bound + * to an ApiKeyBundleConfiguration type and a Principal type. You can use the DefaultApiKeyBundle + * class if you use use a default Principal implementation and do not require authorization. */ -@SuppressWarnings("UnusedDeclaration") -public class ApiKeyBundle implements ConfiguredBundle { +public class ApiKeyBundle + implements ConfiguredBundle { + private final Class

principalClass; + private final PrincipalFactory

factory; + private final Authorizer

authorizer; + private final UnauthorizedHandler unauthorizedHandler; + + /** + * Construct the ApiKeyBundle using the provided Principal class, PrincipalFactory and + * Authorizer. + * + * @param principalClass The class of the class extending the Principal type. + * @param factory The PrincipalFactory instance, which can create new P objects. + * @param authorizer The Authorizer instance, which can create new P objects. + */ + public ApiKeyBundle(Class

principalClass, PrincipalFactory

factory, + Authorizer

authorizer) { + this(principalClass, factory, authorizer, new DefaultUnauthorizedHandler()); + } + + /** + * Construct the ApiKeyBundle using the provided Principal class, PrincipalFactory, + * Authorizer and UnauthorizedHandler. + * + * @param principalClass The class of the class extending the Principal type. + * @param factory The PrincipalFactory instance, which can create new P objects. + * @param authorizer The Authorizer instance, which can create new P objects. + * @param unauthorizedHandler The UnauthorizedHandler instance. + */ + public ApiKeyBundle(Class

principalClass, PrincipalFactory

factory, + Authorizer

authorizer, UnauthorizedHandler unauthorizedHandler) { + this.principalClass = checkNotNull(principalClass); + this.factory = checkNotNull(factory); + this.authorizer = checkNotNull(authorizer); + this.unauthorizedHandler = checkNotNull(unauthorizedHandler); + } + @Override public void initialize(Bootstrap bootstrap) { } @@ -32,25 +75,34 @@ public void run(T bundleConfiguration, Environment environment) throws Exception Optional basic = configuration.getBasicConfiguration(); checkState(basic.isPresent(), "A basic-http configuration option must be specified"); - AuthFactory factory = createBasicAuthFactory(basic.get(), environment.metrics()); - environment.jersey().register(AuthFactory.binder(factory)); + environment.jersey().register(new AuthDynamicFeature( + createBasicCredentialAuthFilter(basic.get(), environment.metrics()))); + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(principalClass)); + } + + private BasicCredentialAuthFilter

createBasicCredentialAuthFilter(AuthConfiguration config, + MetricRegistry metrics) { + final BasicCredentialAuthFilter

authFilter = + new BasicCredentialAuthFilter.Builder

() + .setAuthenticator(createAuthenticator(config, metrics)) + .setRealm(config.getRealm()) + .setAuthorizer(authorizer) + .setUnauthorizedHandler(unauthorizedHandler) + .buildAuthFilter(); + return authFilter; } - private BasicAuthFactory createBasicAuthFactory(AuthConfiguration config, - MetricRegistry metrics) { - Authenticator authenticator = createAuthenticator(config); + private Authenticator createAuthenticator(AuthConfiguration config, + MetricRegistry metrics) { + Map keys = config.getApiKeys(); + Authenticator authenticator = + new BasicCredentialsAuthenticator<>(keys::get, factory); Optional cacheSpec = config.getCacheSpec(); if (cacheSpec.isPresent()) { CacheBuilderSpec spec = CacheBuilderSpec.parse(cacheSpec.get()); authenticator = new CachingAuthenticator<>(metrics, authenticator, spec); } - - return new BasicAuthFactory<>(authenticator, config.getRealm(), String.class); - } - - private Authenticator createAuthenticator(AuthConfiguration config) { - Map keys = config.getApiKeys(); - return new BasicCredentialsAuthenticator(keys::get); + return authenticator; } } diff --git a/src/main/java/io/dropwizard/bundles/apikey/BasicCredentialsAuthenticator.java b/src/main/java/io/dropwizard/bundles/apikey/BasicCredentialsAuthenticator.java index 5ab5c10..90dc681 100644 --- a/src/main/java/io/dropwizard/bundles/apikey/BasicCredentialsAuthenticator.java +++ b/src/main/java/io/dropwizard/bundles/apikey/BasicCredentialsAuthenticator.java @@ -4,21 +4,25 @@ import io.dropwizard.auth.AuthenticationException; import io.dropwizard.auth.Authenticator; import io.dropwizard.auth.basic.BasicCredentials; +import java.security.Principal; import static com.google.common.base.Preconditions.checkNotNull; /** * An Authenticator that converts HTTP basic authentication credentials into an API key. */ -public class BasicCredentialsAuthenticator implements Authenticator { +public class BasicCredentialsAuthenticator

+ implements Authenticator { private final ApiKeyProvider provider; + private final PrincipalFactory

factory; - BasicCredentialsAuthenticator(ApiKeyProvider provider) { + BasicCredentialsAuthenticator(ApiKeyProvider provider, PrincipalFactory

factory) { this.provider = checkNotNull(provider); + this.factory = checkNotNull(factory); } @Override - public Optional authenticate(BasicCredentials credentials) + public Optional

authenticate(BasicCredentials credentials) throws AuthenticationException { checkNotNull(credentials); @@ -34,6 +38,6 @@ public Optional authenticate(BasicCredentials credentials) return Optional.absent(); } - return Optional.of(key.getUsername()); + return Optional.of(factory.create(key.getUsername())); } } diff --git a/src/main/java/io/dropwizard/bundles/apikey/DefaultApiKeyBundle.java b/src/main/java/io/dropwizard/bundles/apikey/DefaultApiKeyBundle.java new file mode 100644 index 0000000..b9d3e83 --- /dev/null +++ b/src/main/java/io/dropwizard/bundles/apikey/DefaultApiKeyBundle.java @@ -0,0 +1,16 @@ +package io.dropwizard.bundles.apikey; + +import io.dropwizard.auth.PermitAllAuthorizer; +import java.security.Principal; + +/** + * The DefaultApiKeyBundle class provides the base implementation of the API key-based + * authentication, providing a simple Principal implementation and no authorization logic (permit + * all). + */ +public class DefaultApiKeyBundle + extends ApiKeyBundle { + public DefaultApiKeyBundle() { + super(Principal.class, new DefaultPrincipalFactory(), new PermitAllAuthorizer<>()); + } +} diff --git a/src/main/java/io/dropwizard/bundles/apikey/DefaultPrincipalFactory.java b/src/main/java/io/dropwizard/bundles/apikey/DefaultPrincipalFactory.java new file mode 100644 index 0000000..fc7361e --- /dev/null +++ b/src/main/java/io/dropwizard/bundles/apikey/DefaultPrincipalFactory.java @@ -0,0 +1,15 @@ +package io.dropwizard.bundles.apikey; + +import io.dropwizard.auth.PrincipalImpl; +import java.security.Principal; + +/** + * A PrincipalFactory that provides a simple implementation of a Principal provided + * by the Dropwizard Auth module. + */ +public class DefaultPrincipalFactory implements PrincipalFactory { + @Override + public Principal create(String name) { + return new PrincipalImpl(name); + } +} diff --git a/src/main/java/io/dropwizard/bundles/apikey/PrincipalFactory.java b/src/main/java/io/dropwizard/bundles/apikey/PrincipalFactory.java new file mode 100644 index 0000000..7560b5a --- /dev/null +++ b/src/main/java/io/dropwizard/bundles/apikey/PrincipalFactory.java @@ -0,0 +1,15 @@ +package io.dropwizard.bundles.apikey; + +import java.security.Principal; + +/** + * An interface for classes which create principal objects. + * + * @param

the type of principal + */ +public interface PrincipalFactory

{ + /** + * Create an instance of P from the specified name. + */ + P create(String name); +} diff --git a/src/test/java/io/dropwizard/bundles/apikey/ApiKeyBundleClientTest.java b/src/test/java/io/dropwizard/bundles/apikey/ApiKeyBundleClientTest.java index b6b011a..816ec43 100644 --- a/src/test/java/io/dropwizard/bundles/apikey/ApiKeyBundleClientTest.java +++ b/src/test/java/io/dropwizard/bundles/apikey/ApiKeyBundleClientTest.java @@ -1,9 +1,12 @@ package io.dropwizard.bundles.apikey; import io.dropwizard.auth.Auth; -import io.dropwizard.auth.AuthFactory; -import io.dropwizard.auth.basic.BasicAuthFactory; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.auth.PermitAllAuthorizer; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; import io.dropwizard.testing.junit.ResourceTestRule; +import java.security.Principal; import java.util.Base64; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -31,7 +34,7 @@ public String insecure() { @GET @Path("/secure") - public String secure(@Auth String application) { + public String secure(@Auth Principal application) { return "secure"; } } @@ -49,13 +52,21 @@ public ApiKey get(String username) { } }; - private final BasicCredentialsAuthenticator authenticator = - new BasicCredentialsAuthenticator(provider); + private final BasicCredentialsAuthenticator authenticator = + new BasicCredentialsAuthenticator<>(provider, new DefaultPrincipalFactory()); + + private BasicCredentialAuthFilter authFilter = + new BasicCredentialAuthFilter.Builder() + .setAuthenticator(authenticator) + .setRealm("realm") + .setAuthorizer(new PermitAllAuthorizer()) + .buildAuthFilter(); @Rule public final ResourceTestRule resources = ResourceTestRule.builder() .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) - .addProvider(AuthFactory.binder(new BasicAuthFactory<>(authenticator, "realm", String.class))) + .addProvider(new AuthDynamicFeature(authFilter)) + .addProvider(new AuthValueFactoryProvider.Binder<>(Principal.class)) .addResource(new TestResource()) .build(); diff --git a/src/test/java/io/dropwizard/bundles/apikey/BasicCredentialsAuthenticatorTest.java b/src/test/java/io/dropwizard/bundles/apikey/BasicCredentialsAuthenticatorTest.java index ce834ff..3d987e2 100644 --- a/src/test/java/io/dropwizard/bundles/apikey/BasicCredentialsAuthenticatorTest.java +++ b/src/test/java/io/dropwizard/bundles/apikey/BasicCredentialsAuthenticatorTest.java @@ -3,6 +3,7 @@ import com.google.common.base.Optional; import io.dropwizard.auth.AuthenticationException; import io.dropwizard.auth.basic.BasicCredentials; +import java.security.Principal; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -13,11 +14,17 @@ public class BasicCredentialsAuthenticatorTest { private final ApiKeyProvider provider = mock(ApiKeyProvider.class); - private final BasicCredentialsAuthenticator auth = new BasicCredentialsAuthenticator(provider); + private final BasicCredentialsAuthenticator auth = + new BasicCredentialsAuthenticator<>(provider, new DefaultPrincipalFactory()); @Test(expected = NullPointerException.class) public void testNullProvider() { - new BasicCredentialsAuthenticator(null); + new BasicCredentialsAuthenticator(null, new DefaultPrincipalFactory()); + } + + @Test(expected = NullPointerException.class) + public void testNullFactory() { + new BasicCredentialsAuthenticator(provider, null); } @Test @@ -26,9 +33,9 @@ public void testValidCredentials() throws AuthenticationException { when(provider.get("username")).thenReturn(key); BasicCredentials credentials = new BasicCredentials("username", "secret"); - Optional actual = auth.authenticate(credentials); + Optional actual = auth.authenticate(credentials); assertTrue(actual.isPresent()); - assertEquals("username", actual.get()); + assertEquals("username", actual.get().getName()); } @Test @@ -37,7 +44,7 @@ public void testInvalidCredentials() throws AuthenticationException { when(provider.get("username")).thenReturn(key); BasicCredentials credentials = new BasicCredentials("username", "secret"); - Optional actual = auth.authenticate(credentials); + Optional actual = auth.authenticate(credentials); assertFalse(actual.isPresent()); } @@ -46,7 +53,7 @@ public void testNullCredentials() throws AuthenticationException { when(provider.get("username")).thenReturn(null); BasicCredentials credentials = new BasicCredentials("username", "secret"); - Optional actual = auth.authenticate(credentials); + Optional actual = auth.authenticate(credentials); assertFalse(actual.isPresent()); }