Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Dropwizard 0.9. #8

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<groupId>io.dropwizard-bundles</groupId>
<artifactId>dropwizard-api-key-bundle</artifactId>
<version>0.8.5-1-SNAPSHOT</version> <!-- Make sure to keep this in sync with the dropwizard version below. -->
<version>0.9.2-SNAPSHOT</version> <!-- Make sure to keep this in sync with the dropwizard version below. -->
<packaging>jar</packaging>

<name>dropwizard-api-key-bundle</name>
Expand Down Expand Up @@ -48,10 +48,10 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<dropwizard.version>0.8.5</dropwizard.version>
<jersey.version>2.21</jersey.version>
<dropwizard.version>0.9.2</dropwizard.version>
<jersey.version>2.22.1</jersey.version>
<junit.version>4.12</junit.version>
<mockito.version>1.10.17</mockito.version>
<mockito.version>1.10.19</mockito.version>
</properties>

<build>
Expand Down
51 changes: 42 additions & 9 deletions readme.md
Original file line number Diff line number Diff line change
@@ -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)


Expand All @@ -15,17 +16,18 @@ Just add this maven dependency to get started:
<dependency>
<groupId>io.dropwizard-bundles</groupId>
<artifactId>dropwizard-api-key-bundle</artifactId>
<version>0.8.4-1</version>
<version>0.9.2-SNAPSHOT</version>
</dependency>
```

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<MyConfiguration> {
@Override
public void initialize(Bootstrap<MyConfiguration> bootstrap) {
bootstrap.addBundle(new ApiKeyBundle<>());
bootstrap.addBundle(new DefaultApiKeyBundle<>());
}

@Override
Expand All @@ -35,6 +37,37 @@ public class MyApplication extends Application<MyConfiguration> {
}
```

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<MyConfiguration> {
@Override
public void initialize(Bootstrap<MyConfiguration> bootstrap) {
bootstrap.addBundle(new ApiKeyBundle<>(User.class, new PrincipalFactory<User>() {
@Override
public User create(String name) {
// Do something interesting...
return new User(name);
}}, new Authorizer<User>() {
@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.

Expand All @@ -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.
Expand All @@ -75,4 +108,4 @@ authentication:
```

[dropwizard]: http://dropwizard.io
[authentication]: http://www.dropwizard.io/0.8.5/docs/manual/auth.html
[authentication]: http://www.dropwizard.io/0.9.2/docs/manual/auth.html
86 changes: 69 additions & 17 deletions src/main/java/io/dropwizard/bundles/apikey/ApiKeyBundle.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends ApiKeyBundleConfiguration> implements ConfiguredBundle<T> {
public class ApiKeyBundle<T extends ApiKeyBundleConfiguration, P extends Principal>
implements ConfiguredBundle<T> {
private final Class<P> principalClass;
private final PrincipalFactory<P> factory;
private final Authorizer<P> 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<P> principalClass, PrincipalFactory<P> factory,
Authorizer<P> 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<P> principalClass, PrincipalFactory<P> factory,
Authorizer<P> 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) {
}
Expand All @@ -32,25 +75,34 @@ public void run(T bundleConfiguration, Environment environment) throws Exception
Optional<AuthConfiguration> basic = configuration.getBasicConfiguration();
checkState(basic.isPresent(), "A basic-http configuration option must be specified");

AuthFactory<?, String> 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<P> createBasicCredentialAuthFilter(AuthConfiguration config,
MetricRegistry metrics) {
final BasicCredentialAuthFilter<P> authFilter =
new BasicCredentialAuthFilter.Builder<P>()
.setAuthenticator(createAuthenticator(config, metrics))
.setRealm(config.getRealm())
.setAuthorizer(authorizer)
.setUnauthorizedHandler(unauthorizedHandler)
.buildAuthFilter();
return authFilter;
}

private BasicAuthFactory<String> createBasicAuthFactory(AuthConfiguration config,
MetricRegistry metrics) {
Authenticator<BasicCredentials, String> authenticator = createAuthenticator(config);
private Authenticator<BasicCredentials, P> createAuthenticator(AuthConfiguration config,
MetricRegistry metrics) {
Map<String, ApiKey> keys = config.getApiKeys();
Authenticator<BasicCredentials, P> authenticator =
new BasicCredentialsAuthenticator<>(keys::get, factory);

Optional<String> 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<BasicCredentials, String> createAuthenticator(AuthConfiguration config) {
Map<String, ApiKey> keys = config.getApiKeys();
return new BasicCredentialsAuthenticator(keys::get);
return authenticator;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<BasicCredentials, String> {
public class BasicCredentialsAuthenticator<P extends Principal>
implements Authenticator<BasicCredentials, P> {
private final ApiKeyProvider provider;
private final PrincipalFactory<P> factory;

BasicCredentialsAuthenticator(ApiKeyProvider provider) {
BasicCredentialsAuthenticator(ApiKeyProvider provider, PrincipalFactory<P> factory) {
this.provider = checkNotNull(provider);
this.factory = checkNotNull(factory);
}

@Override
public Optional<String> authenticate(BasicCredentials credentials)
public Optional<P> authenticate(BasicCredentials credentials)
throws AuthenticationException {
checkNotNull(credentials);

Expand All @@ -34,6 +38,6 @@ public Optional<String> authenticate(BasicCredentials credentials)
return Optional.absent();
}

return Optional.of(key.getUsername());
return Optional.of(factory.create(key.getUsername()));
}
}
Original file line number Diff line number Diff line change
@@ -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<T extends ApiKeyBundleConfiguration>
extends ApiKeyBundle<T, Principal> {
public DefaultApiKeyBundle() {
super(Principal.class, new DefaultPrincipalFactory(), new PermitAllAuthorizer<>());
}
}
Original file line number Diff line number Diff line change
@@ -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<Principal> {
@Override
public Principal create(String name) {
return new PrincipalImpl(name);
}
}
15 changes: 15 additions & 0 deletions src/main/java/io/dropwizard/bundles/apikey/PrincipalFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.dropwizard.bundles.apikey;

import java.security.Principal;

/**
* An interface for classes which create principal objects.
*
* @param <P> the type of principal
*/
public interface PrincipalFactory<P extends Principal> {
/**
* Create an instance of P from the specified name.
*/
P create(String name);
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -31,7 +34,7 @@ public String insecure() {

@GET
@Path("/secure")
public String secure(@Auth String application) {
public String secure(@Auth Principal application) {
return "secure";
}
}
Expand All @@ -49,13 +52,21 @@ public ApiKey get(String username) {
}
};

private final BasicCredentialsAuthenticator authenticator =
new BasicCredentialsAuthenticator(provider);
private final BasicCredentialsAuthenticator<Principal> authenticator =
new BasicCredentialsAuthenticator<>(provider, new DefaultPrincipalFactory());

private BasicCredentialAuthFilter<Principal> authFilter =
new BasicCredentialAuthFilter.Builder<Principal>()
.setAuthenticator(authenticator)
.setRealm("realm")
.setAuthorizer(new PermitAllAuthorizer<Principal>())
.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();

Expand Down
Loading