Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I test HMAC authentication using Dropwizard?

Tags:

dropwizard

I'm just getting started with Dropwizard 0.4.0, and I would like some help with HMAC authentication. Has anybody got any advice?

Thank you in advance.

like image 257
Joe Avatar asked May 28 '12 10:05

Joe


1 Answers

At present Dropwizard doesn't support HMAC authentication right out of the box, so you'd have to write your own authenticator. A typical choice for HMAC authentication is to use the HTTP Authorization header. The following code expects this header in the following format:

Authorization: <algorithm> <apiKey> <digest>

An example would be

Authorization: HmacSHA1 abcd-efgh-1234 sdafkljlkansdaflk2354jlkj5345345dflkmsdf

The digest is built from the content of the body (marshalled entity) prior to URL encoding with the HMAC shared secret appended as base64. For a non-body request, such as GET or HEAD, the content is taken as the complete URI path and parameters with the secret key appended.

To implement this in a way that Dropwizard can work with it requires you to copy the BasicAuthenticator code present in the dropwizard-auth module into your own code and modify it with something like this:

import com.google.common.base.Optional;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
import com.yammer.dropwizard.auth.AuthenticationException;
import com.yammer.dropwizard.auth.Authenticator;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

class HmacAuthInjectable<T> extends AbstractHttpContextInjectable<T> {
  private static final String PREFIX = "HmacSHA1";
  private static final String HEADER_VALUE = PREFIX + " realm=\"%s\"";

  private final Authenticator<HmacCredentials, T> authenticator;
  private final String realm;
  private final boolean required;

  HmacAuthInjectable(Authenticator<HmacCredentials, T> authenticator, String realm, boolean required) {
    this.authenticator = authenticator;
    this.realm = realm;
    this.required = required;
  }

  public Authenticator<HmacCredentials, T> getAuthenticator() {
    return authenticator;
  }

  public String getRealm() {
    return realm;
  }

  public boolean isRequired() {
    return required;
  }

  @Override
  public T getValue(HttpContext c) {

    try {
      final String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION);
      if (header != null) {

        final String[] authTokens = header.split(" ");

        if (authTokens.length != 3) {
          // Malformed
          HmacAuthProvider.LOG.debug("Error decoding credentials (length is {})", authTokens.length);
          throw new WebApplicationException(Response.Status.BAD_REQUEST);
        }

        final String algorithm = authTokens[0];
        final String apiKey = authTokens[1];
        final String signature = authTokens[2];
        final String contents;

        // Determine which part of the request will be used for the content
        final String method = c.getRequest().getMethod().toUpperCase();
        if ("GET".equals(method) ||
          "HEAD".equals(method) ||
          "DELETE".equals(method)) {
          // No entity so use the URI
          contents = c.getRequest().getRequestUri().toString();
        } else {
          // Potentially have an entity (even in OPTIONS) so use that
          contents = c.getRequest().getEntity(String.class);
        }

        final HmacCredentials credentials = new HmacCredentials(algorithm, apiKey, signature, contents);

        final Optional<T> result = authenticator.authenticate(credentials);
        if (result.isPresent()) {
          return result.get();
        }
      }
    } catch (IllegalArgumentException e) {
      HmacAuthProvider.LOG.debug(e, "Error decoding credentials");
    } catch (AuthenticationException e) {
      HmacAuthProvider.LOG.warn(e, "Error authenticating credentials");
      throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
    }

    if (required) {
      throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
        .header(HttpHeaders.AUTHORIZATION,
          String.format(HEADER_VALUE, realm))
        .entity("Credentials are required to access this resource.")
        .type(MediaType.TEXT_PLAIN_TYPE)
        .build());
    }
    return null;
  }
}

The above is not perfect, but it'll get you started. You may want to refer to the MultiBit Merchant release candidate source code (MIT license) for a more up to date version and the various supporting classes.

The next step is to integrate the authentication process into your ResourceTest subclass. Unfortunately, Dropwizard doesn't provide a good entry point for authentication providers in v0.4.0, so you may want to introduce your own base class, similar to this:

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.test.framework.AppDescriptor;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.LowLevelAppDescriptor;
import com.xeiam.xchange.utils.CryptoUtils;
import com.yammer.dropwizard.bundles.JavaBundle;
import com.yammer.dropwizard.jersey.DropwizardResourceConfig;
import com.yammer.dropwizard.jersey.JacksonMessageBodyProvider;
import com.yammer.dropwizard.json.Json;
import org.codehaus.jackson.map.Module;
import org.junit.After;
import org.junit.Before;
import org.multibit.mbm.auth.hmac.HmacAuthProvider;
import org.multibit.mbm.auth.hmac.HmacAuthenticator;
import org.multibit.mbm.persistence.dao.UserDao;
import org.multibit.mbm.persistence.dto.User;
import org.multibit.mbm.persistence.dto.UserBuilder;

import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.Set;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
* A base test class for testing Dropwizard resources.
*/
public abstract class BaseResourceTest {
  private final Set<Object> singletons = Sets.newHashSet();
  private final Set<Object> providers = Sets.newHashSet();
  private final List<Module> modules = Lists.newArrayList();

  private JerseyTest test;

  protected abstract void setUpResources() throws Exception;

  protected void addResource(Object resource) {
    singletons.add(resource);
  }

  public void addProvider(Object provider) {
    providers.add(provider);
  }

  protected void addJacksonModule(Module module) {
    modules.add(module);
  }

  protected Json getJson() {
    return new Json();
  }

  protected Client client() {
    return test.client();
  }

  @Before
  public void setUpJersey() throws Exception {
    setUpResources();
    this.test = new JerseyTest() {
      @Override
      protected AppDescriptor configure() {
        final DropwizardResourceConfig config = new DropwizardResourceConfig();
        for (Object provider : JavaBundle.DEFAULT_PROVIDERS) { // sorry, Scala folks
          config.getSingletons().add(provider);
        }
        for (Object provider : providers) {
          config.getSingletons().add(provider);
        }
        Json json = getJson();
        for (Module module : modules) {
          json.registerModule(module);
        }
        config.getSingletons().add(new JacksonMessageBodyProvider(json));
        config.getSingletons().addAll(singletons);
        return new LowLevelAppDescriptor.Builder(config).build();
      }
    };
    test.setUp();
  }

  @After
  public void tearDownJersey() throws Exception {
    if (test != null) {
      test.tearDown();
    }
  }

  /**
* @param contents The content to sign with the default HMAC process (POST body, GET resource path)
* @return
*/
  protected String buildHmacAuthorization(String contents, String apiKey, String secretKey) throws UnsupportedEncodingException, GeneralSecurityException {
    return String.format("HmacSHA1 %s %s",apiKey, CryptoUtils.computeSignature("HmacSHA1", contents, secretKey));
  }

  protected void setUpAuthenticator() {
    User user = UserBuilder
      .getInstance()
      .setUUID("abc123")
      .setSecretKey("def456")
      .build();

    //
    UserDao userDao = mock(UserDao.class);
    when(userDao.getUserByUUID("abc123")).thenReturn(user);

    HmacAuthenticator authenticator = new HmacAuthenticator();
    authenticator.setUserDao(userDao);

    addProvider(new HmacAuthProvider<User>(authenticator, "REST"));
  }
}

Again, the above code is not perfect, but the idea is to allow a mocked up UserDao to provide a standard user with a known shared secret key. You'd have to introduce your own UserBuilder implementation for testing purposes.

Finally, with the above code a Dropwizard Resource that had an endpoint like this:

import com.google.common.base.Optional;
import com.yammer.dropwizard.auth.Auth;
import com.yammer.metrics.annotation.Timed;
import org.multibit.mbm.core.Saying;
import org.multibit.mbm.persistence.dto.User;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.concurrent.atomic.AtomicLong;

@Path("/")
@Produces(MediaType.APPLICATION_JSON)
public class HelloWorldResource {
  private final String template;
  private final String defaultName;
  private final AtomicLong counter;

  public HelloWorldResource(String template, String defaultName) {
    this.template = template;
    this.defaultName = defaultName;
    this.counter = new AtomicLong();
  }

  @GET
  @Timed
  @Path("/hello-world")
  public Saying sayHello(@QueryParam("name") Optional<String> name) {
    return new Saying(counter.incrementAndGet(),
      String.format(template, name.or(defaultName)));
  }

  @GET
  @Timed
  @Path("/secret")
  public Saying saySecuredHello(@Auth User user) {
    return new Saying(counter.incrementAndGet(),
      "You cracked the code!");
  }

}

could be tested with a unit test that was configured like this:

import org.junit.Test;
import org.multibit.mbm.core.Saying;
import org.multibit.mbm.test.BaseResourceTest;

import javax.ws.rs.core.HttpHeaders;

import static org.junit.Assert.assertEquals;

public class HelloWorldResourceTest extends BaseResourceTest {


  @Override
  protected void setUpResources() {
    addResource(new HelloWorldResource("Hello, %s!","Stranger"));

    setUpAuthenticator();
  }

  @Test
  public void simpleResourceTest() throws Exception {

    Saying expectedSaying = new Saying(1,"Hello, Stranger!");

    Saying actualSaying = client()
      .resource("/hello-world")
      .get(Saying.class);

    assertEquals("GET hello-world returns a default",expectedSaying.getContent(),actualSaying.getContent());

  }


  @Test
  public void hmacResourceTest() throws Exception {

    String authorization = buildHmacAuthorization("/secret", "abc123", "def456");

    Saying actual = client()
      .resource("/secret")
      .header(HttpHeaders.AUTHORIZATION, authorization)
      .get(Saying.class);

    assertEquals("GET secret returns unauthorized","You cracked the code!", actual.getContent());

  }


}

Hope this helps you get started.

like image 81
Gary Rowe Avatar answered Jan 04 '23 10:01

Gary Rowe